@@ -18,6 +18,8 @@ import { type AutoMergeMethod, type CreatedPullRequest, IAgentHostOctoKitService
1818import type { InvokeChangesetOperationParams , InvokeChangesetOperationResult } from '../common/state/protocol/channels-changeset/commands.js' ;
1919import { ICopilotApiService , type ICopilotUtilityChatMessage } from './shared/copilotApiService.js' ;
2020import { buildConversationContext } from '../common/agentHostConversationContext.js' ;
21+ import { IAgentBranchNameGenerator } from './shared/agentBranchNameGenerator.js' ;
22+ import { SessionConfigKey } from '../common/sessionConfigKeys.js' ;
2123
2224/**
2325 * Soft upper bound, in characters, for the conversation context fed to the
@@ -51,13 +53,14 @@ export interface PullRequestCreatedEvent {
5153 *
5254 * 1. Resolve session → working directory + current/base branch from
5355 * {@link ISessionGitState}.
54- * 2. Commit any uncommitted working-tree changes.
55- * 3. Push the current branch to its GitHub upstream remote (with `--set-upstream` when missing).
56- * 4. Resolve `owner` / `repo` from {@link ISessionGitState.githubOwner}
56+ * 2. If the current branch is the base branch, create a generated session branch.
57+ * 3. Commit any uncommitted working-tree changes.
58+ * 4. Push the current branch to its GitHub upstream remote (with `--set-upstream` when missing).
59+ * 5. Resolve `owner` / `repo` from {@link ISessionGitState.githubOwner}
5760 * / {@link ISessionGitState.githubRepo } (populated by the git probe).
58- * 5 . Reuse an existing PR for the branch, or POST `/repos/{owner}/{repo}/pulls`
61+ * 6 . Reuse an existing PR for the branch, or POST `/repos/{owner}/{repo}/pulls`
5962 * via {@link IAgentHostOctoKitService}.
60- * 6 . Return the PR URL as an {@link InvokeChangesetOperationResult.followUp}.
63+ * 7 . Return the PR URL as an {@link InvokeChangesetOperationResult.followUp}.
6164 */
6265export class AgentHostPullRequestOperationHandler implements IChangesetOperationHandler {
6366
@@ -78,6 +81,7 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
7881 @IAgentHostOctoKitService private readonly _octoKitService : IAgentHostOctoKitService ,
7982 @IAgentHostGitHubEndpointService private readonly _gitHubEndpointService : IAgentHostGitHubEndpointService ,
8083 @ICopilotApiService private readonly _copilotApiService : ICopilotApiService ,
84+ @IAgentBranchNameGenerator private readonly _branchNameGenerator : IAgentBranchNameGenerator ,
8185 @ILogService private readonly _logService : ILogService ,
8286 ) { }
8387
@@ -100,8 +104,8 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
100104 throw new ProtocolError ( JsonRpcErrorCodes . InvalidParams , `Not a changeset URI: ${ params . channel } ` ) ;
101105 }
102106 this . _throwIfCancelled ( token ) ;
103- const sessionUri = parsed . sessionUri ;
104107
108+ const sessionUri = parsed . sessionUri ;
105109 const sessionState = this . _getSessionState ( sessionUri ) ;
106110 if ( ! sessionState ) {
107111 throw new ProtocolError ( AHP_SESSION_NOT_FOUND , `Session not found: ${ sessionUri } ` ) ;
@@ -123,17 +127,18 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
123127 const workingDirectory = URI . parse ( workingDirectoryStr ) ;
124128 const storedGitState = readSessionGitState ( sessionState . _meta ) ;
125129 const effectiveBaseBranch = await this . _resolveBaseBranchName ( sessionUri ) ;
126- const gitState = await this . _gitService . getSessionGitState ( workingDirectory , effectiveBaseBranch ) ?? storedGitState ;
127- const branchName = gitState ?. branchName ?? await this . _gitService . getCurrentBranch ( workingDirectory ) ;
130+
131+ let gitState = await this . _gitService . getSessionGitState ( workingDirectory , effectiveBaseBranch ) ?? storedGitState ;
132+ let branchName = gitState ?. branchName ?? await this . _gitService . getCurrentBranch ( workingDirectory ) ;
128133 if ( ! branchName ) {
129134 throw new ProtocolError ( JsonRpcErrorCodes . InternalError , `Could not determine current branch for ${ workingDirectory } ` ) ;
130135 }
131136
132- const baseBranchName = effectiveBaseBranch ?? gitState ?. baseBranchName ?? ( await this . _gitService . getDefaultBranch ( workingDirectory ) ) ?. name ;
137+ const defaultBranch = await this . _gitService . getDefaultBranch ( workingDirectory ) ;
138+ const baseBranchName = effectiveBaseBranch ?? gitState ?. baseBranchName ?? defaultBranch ?. name ;
133139 if ( ! baseBranchName ) {
134140 throw new ProtocolError ( JsonRpcErrorCodes . InternalError , `Could not determine base branch for ${ workingDirectory } ` ) ;
135141 }
136- const base = baseBranchName ;
137142
138143 const repoResource = this . _gitHubEndpointService . getRepoResource ( ) ;
139144 const authToken = this . _authenticationService . getAuthToken ( {
@@ -149,9 +154,39 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
149154 }
150155
151156 const hasUncommitted = await this . _gitService . hasUncommittedChanges ( workingDirectory ) ;
157+
158+ // Create a new branch if the current branch is the same
159+ // as the base branch and there are uncommitted changes
160+ if ( hasUncommitted && branchName === baseBranchName ) {
161+ const branchPrefix = sessionState . config ?. values [ SessionConfigKey . WorktreeBranchPrefix ] ;
162+
163+ try {
164+ const generatedBranchName = await this . _branchNameGenerator . generateBranchName ( {
165+ sessionId : URI . parse ( sessionUri ) . path . split ( '/' ) . filter ( Boolean ) . pop ( ) ?? sessionUri ,
166+ message : sessionState . turns . find ( turn => turn . message . text . trim ( ) ) ?. message . text ,
167+ githubToken : authToken ,
168+ signal,
169+ branchPrefix : typeof branchPrefix === 'string' ? branchPrefix : undefined ,
170+ branchNameCollides : candidate => this . _gitService . branchExists ( workingDirectory , candidate ) . catch ( ( ) => true ) ,
171+ } ) ;
172+
173+ this . _throwIfCancelled ( token ) ;
174+ this . _logService . info ( `[AgentHostPullRequestOperationHandler] Creating branch ${ generatedBranchName } for session ${ sessionUri } ` ) ;
175+
176+ await this . _gitService . createBranch ( workingDirectory , generatedBranchName , { checkout : true } ) ;
177+ branchName = generatedBranchName ;
178+
179+ gitState = await this . _gitService . getSessionGitState ( workingDirectory , effectiveBaseBranch ) ;
180+ } catch ( err ) {
181+ this . _throwIfCancelled ( token ) ;
182+ throw new ProtocolError ( JsonRpcErrorCodes . InternalError , `Failed to create a branch before creating a pull request: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
183+ }
184+ }
185+
152186 if ( hasUncommitted ) {
153187 this . _throwIfCancelled ( token ) ;
154188 this . _logService . info ( `[AgentHostPullRequestOperationHandler] Committing uncommitted changes for session ${ sessionUri } ` ) ;
189+
155190 try {
156191 await this . _gitService . commitAll ( workingDirectory , this . _formatCommitMessage ( branchName ) ) ;
157192 } catch ( err ) {
@@ -161,7 +196,7 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
161196 }
162197 this . _throwIfCancelled ( token ) ;
163198
164- const branchChanges = await this . _gitService . computeSessionFileDiffs ( workingDirectory , { sessionUri, baseBranch : base } ) ;
199+ const branchChanges = await this . _gitService . computeSessionFileDiffs ( workingDirectory , { sessionUri, baseBranch : baseBranchName } ) ;
165200 if ( branchChanges === undefined ) {
166201 throw new ProtocolError ( JsonRpcErrorCodes . InternalError , localize ( 'agentHost.changeset.pr.computeChangesFailed' , "Could not compute branch changes to create a pull request." ) ) ;
167202 }
@@ -171,7 +206,7 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
171206 this . _throwIfCancelled ( token ) ;
172207
173208 const githubHeadOwner = gitState ?. githubHeadOwner ;
174- const upstreamBranch = githubHeadOwner ? parseUpstreamBranchName ( gitState . upstreamBranchName ) : undefined ;
209+ const upstreamBranch = githubHeadOwner ? parseUpstreamBranchName ( gitState ? .upstreamBranchName ) : undefined ;
175210 const headOwner = upstreamBranch && githubHeadOwner ? githubHeadOwner : gitHubState . owner ;
176211 const headBranch = upstreamBranch ?. branch ?? branchName ;
177212 const pushRef = headBranch === branchName ? branchName : `${ branchName } :${ headBranch } ` ;
@@ -195,12 +230,12 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
195230 }
196231 this . _throwIfCancelled ( token ) ;
197232
198- const generated = await this . _generateTitleAndDescription ( sessionState , branchName , base , branchChanges , signal , token ) ;
199- this . _throwIfCancelled ( token ) ;
233+ const generated = await this . _generateTitleAndDescription ( sessionState , branchName , baseBranchName , branchChanges , signal , token ) ;
200234 const title = generated ?. title ?? this . _formatTitle ( branchName ) ;
201- const body = generated ?. description ?? this . _formatBody ( branchName , base ) ;
235+ const body = generated ?. description ?? this . _formatBody ( branchName , baseBranchName ) ;
236+ this . _throwIfCancelled ( token ) ;
202237
203- this . _logService . info ( `[AgentHostPullRequestOperationHandler] Creating ${ this . _draft ? 'draft ' : '' } PR ${ gitHubState . owner } /${ gitHubState . repo } ${ createHead } -> ${ base } ` ) ;
238+ this . _logService . info ( `[AgentHostPullRequestOperationHandler] Creating ${ this . _draft ? 'draft ' : '' } PR ${ gitHubState . owner } /${ gitHubState . repo } ${ createHead } -> ${ baseBranchName } ` ) ;
204239 let created : CreatedPullRequest ;
205240 try {
206241 created = await this . _octoKitService . createPullRequest (
@@ -209,7 +244,7 @@ export class AgentHostPullRequestOperationHandler implements IChangesetOperation
209244 title ,
210245 body ,
211246 createHead ,
212- base ,
247+ baseBranchName ,
213248 this . _draft ,
214249 authToken ,
215250 signal ,
0 commit comments