@@ -14,12 +14,10 @@ import { raceCancellation } from '../../../util/vs/base/common/async';
1414import { Disposable , DisposableStore } from '../../../util/vs/base/common/lifecycle' ;
1515import { ResourceSet } from '../../../util/vs/base/common/map' ;
1616import { isEqual } from '../../../util/vs/base/common/resources' ;
17- import { isWelcomeView } from '../copilotcli/node/copilotCli' ;
18- import { ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService' ;
1917import { createTimeout } from '../../inlineEdits/common/common' ;
2018import { IToolsService } from '../../tools/common/toolsService' ;
2119import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService' ;
22- import { ChatSessionWorktreeProperties , IChatSessionWorktreeService } from '../common/chatSessionWorktreeService' ;
20+ import { ChatSessionWorktreeFile , ChatSessionWorktreeProperties , IChatSessionWorktreeService } from '../common/chatSessionWorktreeService' ;
2321import {
2422 FolderRepositoryInfo ,
2523 FolderRepositoryMRUEntry ,
@@ -28,6 +26,8 @@ import {
2826 InitializeFolderRepositoryOptions
2927} from '../common/folderRepositoryManager' ;
3028import { isUntitledSessionId } from '../common/utils' ;
29+ import { isWelcomeView } from '../copilotcli/node/copilotCli' ;
30+ import { ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService' ;
3131
3232/**
3333 * Message shown when user needs to trust a folder to continue.
@@ -274,11 +274,9 @@ export abstract class FolderRepositoryManager extends Disposable implements IFol
274274 // Check for uncommitted changes and prompt user before creating worktree
275275 let uncommittedChangesAction : 'move' | 'copy' | 'skip' | 'cancel' | undefined = undefined ;
276276 if ( ( ! sessionId || isUntitledSessionId ( sessionId ) ) && ! worktreeProperties ) {
277- if ( await this . checkIfRepoHasUncommittedChanges ( sessionId , token ) ) {
278- uncommittedChangesAction = await this . promptForUncommittedChangesAction ( sessionId , toolInvocationToken , token ) ;
279- if ( uncommittedChangesAction === 'cancel' ) {
280- return { folder, repository, worktree, worktreeProperties, trusted : true , cancelled : true } ;
281- }
277+ uncommittedChangesAction = await this . promptForUncommittedChangesAction ( sessionId , toolInvocationToken , token ) ;
278+ if ( uncommittedChangesAction === 'cancel' ) {
279+ return { folder, repository, worktree, worktreeProperties, trusted : true , cancelled : true } ;
282280 }
283281 }
284282
@@ -406,11 +404,56 @@ export abstract class FolderRepositoryManager extends Disposable implements IFol
406404 toolInvocationToken : vscode . ChatParticipantToolToken ,
407405 token : vscode . CancellationToken
408406 ) : Promise < 'move' | 'copy' | 'skip' | 'cancel' | undefined > {
409- const hasUncommittedChanges = await this . checkIfRepoHasUncommittedChanges ( sessionId , token ) ;
410- if ( ! hasUncommittedChanges ) {
407+ const uncommittedChanges = await this . getUncommittedChangesPromptData ( sessionId , token ) ;
408+ if ( ! uncommittedChanges ) {
411409 return undefined ;
412410 }
413411
412+ if ( ! this . toolsService . getTool ( 'vscode_get_modified_files_confirmation' ) ) {
413+ return this . promptForUncommittedChangesActionOld ( sessionId , toolInvocationToken , token ) ;
414+ }
415+
416+ const isDelegation = ! sessionId ;
417+ const title = isDelegation
418+ ? l10n . t ( 'Delegate to Background Agent' )
419+ : l10n . t ( 'Uncommitted Changes' ) ;
420+ const message = isDelegation
421+ ? l10n . t ( 'Background Agent will work in an isolated worktree to implement your requested changes.' )
422+ + '\n\n'
423+ + l10n . t ( 'The selected repository has uncommitted changes. Should these changes be included in the new worktree?' )
424+ : l10n . t ( 'The selected repository has uncommitted changes. Should these changes be included in the new worktree?' ) ;
425+
426+ const copyChanges = l10n . t ( 'Copy Changes' ) ;
427+ const moveChanges = l10n . t ( 'Move Changes' ) ;
428+ const skipChanges = l10n . t ( 'Skip Changes' ) ;
429+ const options = [ copyChanges , moveChanges , skipChanges ] ;
430+ const input = {
431+ title,
432+ message,
433+ options,
434+ modifiedFiles : uncommittedChanges . modifiedFiles
435+ } ;
436+ const result = await this . toolsService . invokeTool ( 'vscode_get_modified_files_confirmation' , { input, toolInvocationToken } , token ) ;
437+
438+ const selection = this . getSelectedUncommittedChangesAction ( result , options ) ;
439+
440+ switch ( selection ?. toUpperCase ( ) ) {
441+ case moveChanges . toUpperCase ( ) :
442+ return 'move' ;
443+ case copyChanges . toUpperCase ( ) :
444+ return 'copy' ;
445+ case skipChanges . toUpperCase ( ) :
446+ return 'skip' ;
447+ default :
448+ return 'cancel' ;
449+ }
450+ }
451+
452+ private async promptForUncommittedChangesActionOld (
453+ sessionId : string | undefined ,
454+ toolInvocationToken : vscode . ChatParticipantToolToken ,
455+ token : vscode . CancellationToken
456+ ) : Promise < 'move' | 'copy' | 'skip' | 'cancel' | undefined > {
414457 const isDelegation = ! sessionId ;
415458 const title = isDelegation
416459 ? l10n . t ( 'Delegate to Background Agent' )
@@ -448,35 +491,103 @@ export abstract class FolderRepositoryManager extends Disposable implements IFol
448491 }
449492 }
450493
451- /**
452- * Check if the repository associated with a session has uncommitted changes.
453- */
454- private async checkIfRepoHasUncommittedChanges ( sessionId : string | undefined , _token : vscode . CancellationToken ) : Promise < boolean > {
494+ private getSelectedUncommittedChangesAction (
495+ result : vscode . LanguageModelToolResult ,
496+ options : readonly string [ ]
497+ ) : string | undefined {
498+ for ( const part of result . content ) {
499+ if ( ! ( part instanceof LanguageModelTextPart ) ) {
500+ continue ;
501+ }
502+
503+ const matchedOption = options . find ( option => option . toUpperCase ( ) === part . value . toUpperCase ( ) ) ;
504+ if ( matchedOption ) {
505+ return matchedOption ;
506+ }
507+ }
508+
509+ return undefined ;
510+ }
511+
512+ private async getUncommittedChangesPromptData (
513+ sessionId : string | undefined ,
514+ token : vscode . CancellationToken
515+ ) : Promise < { repository : vscode . Uri ; modifiedFiles : Array < { uri : vscode . Uri ; originalUri ?: vscode . Uri ; insertions ?: number ; deletions ?: number } > } | undefined > {
516+ const repository = await this . getRepositoryForUncommittedChanges ( sessionId ) ;
517+ if ( ! repository ) {
518+ return undefined ;
519+ }
520+
521+ const modifiedFiles = await this . getModifiedFilesForConfirmation ( repository . rootUri , repository , token ) ;
522+ if ( modifiedFiles . length === 0 ) {
523+ return undefined ;
524+ }
525+
526+ return {
527+ repository : repository . rootUri ,
528+ modifiedFiles
529+ } ;
530+ }
531+
532+ private async getRepositoryForUncommittedChanges ( sessionId : string | undefined ) : Promise < ReturnType < IGitService [ 'activeRepository' ] [ 'get' ] > | undefined > {
455533 if ( sessionId && isUntitledSessionId ( sessionId ) ) {
456534 const folder = this . _untitledSessionFolders . get ( sessionId ) ?. uri
457535 ?? await this . workspaceFolderService . getSessionWorkspaceFolder ( sessionId ) ;
458536 if ( folder ) {
459- const repo = await this . gitService . getRepository ( folder , false ) ;
460- return repo ?. changes
461- ? ( repo . changes . indexChanges . length > 0 || repo . changes . workingTree . length > 0 )
462- : false ;
537+ return await this . gitService . getRepository ( folder , false ) ;
463538 }
464- // No folder selected, fall through to active repo check
465- } else if ( sessionId ) {
466- // Non-untitled session, no need to check
467- return false ;
539+ // No folder selected, fall through to the active repository check.
540+ }
541+
542+ if ( sessionId && ! isUntitledSessionId ( sessionId ) ) {
543+ return undefined ;
468544 }
469545
470- // For delegation (no session) or untitled session without explicit folder selection,
471- // check active repository if there's a single workspace folder
472546 if ( ! isWelcomeView ( this . workspaceService ) && this . workspaceService . getWorkspaceFolders ( ) . length === 1 ) {
473- const repo = this . gitService . activeRepository . get ( ) ;
474- return repo ?. changes
475- ? ( repo . changes . indexChanges . length > 0 || repo . changes . workingTree . length > 0 )
476- : false ;
547+ return this . gitService . activeRepository . get ( ) ;
477548 }
478549
479- return false ;
550+ return undefined ;
551+ }
552+
553+ private async getModifiedFilesForConfirmation (
554+ repositoryUri : vscode . Uri ,
555+ repository : NonNullable < ReturnType < IGitService [ 'activeRepository' ] [ 'get' ] > > ,
556+ token : vscode . CancellationToken
557+ ) : Promise < Array < { uri : vscode . Uri ; originalUri ?: vscode . Uri ; insertions ?: number ; deletions ?: number } > > {
558+ const workspaceChanges = await this . workspaceFolderService . getWorkspaceChanges ( repositoryUri ) ?? [ ] ;
559+ if ( workspaceChanges . length > 0 ) {
560+ return workspaceChanges . map ( change => this . toModifiedFileConfirmationEntry ( change ) ) ;
561+ }
562+
563+ if ( token . isCancellationRequested || ! repository . changes ) {
564+ return [ ] ;
565+ }
566+
567+ const modifiedFiles = new Map < string , { uri : vscode . Uri ; originalUri ?: vscode . Uri ; insertions ?: number ; deletions ?: number } > ( ) ;
568+ for ( const change of [ ...repository . changes . indexChanges , ...repository . changes . workingTree ] ) {
569+ const changePath = ( change as { path ?: string } ) . path ;
570+ const fileUri = change . uri ?? ( changePath ? vscode . Uri . joinPath ( repositoryUri , changePath ) : undefined ) ;
571+ if ( ! fileUri ) {
572+ continue ;
573+ }
574+ modifiedFiles . set ( fileUri . toString ( ) , {
575+ uri : fileUri ,
576+ originalUri : change . originalUri
577+ } ) ;
578+ }
579+
580+ return [ ...modifiedFiles . values ( ) ] ;
581+ }
582+
583+ private toModifiedFileConfirmationEntry ( change : ChatSessionWorktreeFile ) : { uri : vscode . Uri ; originalUri ?: vscode . Uri ; insertions ?: number ; deletions ?: number } {
584+ const uri = vscode . Uri . file ( change . modifiedFilePath ?? change . filePath ) ;
585+ return {
586+ uri : uri ,
587+ originalUri : change . originalFilePath ? vscode . Uri . file ( change . originalFilePath ) : undefined ,
588+ insertions : change . statistics . additions ,
589+ deletions : change . statistics . deletions
590+ } ;
480591 }
481592
482593 /**
0 commit comments