diff --git a/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 8e3fb465c0..7c2529dae3 100644 --- a/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -468,7 +468,8 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements const WAIT_FOR_NEW_SESSION_TO_GET_USED = 5 * 60 * 1000; // 5 minutes export class CopilotCLIChatSessionParticipant extends Disposable { - private CLI_INCLUDE_CHANGES = vscode.l10n.t('Include Changes'); + private CLI_MOVE_CHANGES = vscode.l10n.t('Move Changes'); + private CLI_COPY_CHANGES = vscode.l10n.t('Copy Changes'); private CLI_SKIP_CHANGES = vscode.l10n.t('Skip Changes'); private CLI_CANCEL = vscode.l10n.t('Cancel'); private readonly untitledSessionIdMapping = new Map(); @@ -521,11 +522,10 @@ export class CopilotCLIChatSessionParticipant extends Disposable { }); const confirmationResults = this.getAcceptedRejectedConfirmationData(request); - if (!chatSessionContext) { - // Invoked from a 'normal' chat or 'cloud button' without CLI session context - // Or cases such as delegating from Regular chat to CLI chat - // Handle confirmation data - return await this.handlePushConfirmationData(request, context, stream, token); + // Check if it was delegated from chat or cloud button or if it's the first iteration + const response = await this.generateConfirmationResponseIfNeeded(request, context, stream, token); + if (!chatSessionContext || chatSessionContext.isUntitled) { + return response || {}; } const isUntitled = chatSessionContext.isUntitled; @@ -730,7 +730,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { } // Get model from request. - const preferredModelInRequest = preferModelInRequest && request?.model.id ? await this.copilotCLIModels.resolveModel(request.model.id) : undefined; + const preferredModelInRequest = preferModelInRequest && request?.model?.id ? await this.copilotCLIModels.resolveModel(request.model.id) : undefined; if (preferredModelInRequest) { return preferredModelInRequest; } @@ -788,12 +788,12 @@ export class CopilotCLIChatSessionParticipant extends Disposable { return {}; } - private async handlePushConfirmationData( + private async generateConfirmationResponseIfNeeded( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken - ): Promise { + ) { // Check if this is a confirmation response const confirmationResults = this.getAcceptedRejectedConfirmationData(request); if (confirmationResults.length > 0) { @@ -811,15 +811,25 @@ export class CopilotCLIChatSessionParticipant extends Disposable { if (!hasUncommittedChanges) { // No uncommitted changes, create worktree and proceed return await this.createCLISessionAndSubmitRequest(request, undefined, request.references, context, undefined, true, stream, token); + } else { + return this.generateUncommittedChangesConfirmation(request, context, stream, token); } + } + private generateUncommittedChangesConfirmation( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): vscode.ChatResult | void { const message = vscode.l10n.t('Background Agent will work in an isolated worktree to implement your requested changes.') + '\n\n' + vscode.l10n.t('This workspace has uncommitted changes. Should these changes be included in the new worktree?'); const buttons = [ - this.CLI_INCLUDE_CHANGES, + this.CLI_COPY_CHANGES, + this.CLI_MOVE_CHANGES, this.CLI_SKIP_CHANGES, this.CLI_CANCEL ]; @@ -861,10 +871,11 @@ export class CopilotCLIChatSessionParticipant extends Disposable { return {}; } - const includeChanges = selection.includes(this.CLI_INCLUDE_CHANGES.toUpperCase()); + const moveChanges = selection === this.CLI_MOVE_CHANGES.toUpperCase(); + const copyChanges = selection === this.CLI_COPY_CHANGES.toUpperCase(); const prompt = uncommittedChangesData.metadata.prompt; - if (includeChanges && this.worktreeManager.isSupported()) { + if ((moveChanges || copyChanges) && this.worktreeManager.isSupported()) { // Create worktree first stream.progress(vscode.l10n.t('Creating worktree...')); const worktreePathValue = await this.worktreeManager.createWorktree(stream); @@ -906,7 +917,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { } else { await this.gitService.migrateChanges(worktreeRepo.rootUri, activeRepository.rootUri, { confirmation: false, - deleteFromSource: true, + deleteFromSource: moveChanges, untracked: true }); stream.markdown(vscode.l10n.t('Changes migrated to worktree.')); diff --git a/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index 8d348f08e7..8b8be51dab 100644 --- a/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -61,6 +61,7 @@ class FakeWorktreeManager extends mock() { override getWorktreePath = vi.fn((_id: string) => undefined); override getIsolationPreference = vi.fn(() => false); override getDefaultIsolationPreference = vi.fn(() => false); + override isSupported = vi.fn(() => false); } class FakeModels implements ICopilotCLIModels { @@ -285,10 +286,23 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { expect(cloudProvider.delegate).toHaveBeenCalled(); }); - it('handles /delegate command for new session', async () => { + it('handles /delegate command for new untitled session with uncomitted changes', async () => { expect(manager.sessions.size).toBe(0); git.activeRepository = { get: () => ({ changes: { indexChanges: [{ path: 'file.ts' }] } }) } as unknown as IGitService['activeRepository']; const request = new TestChatRequest('/delegate Build feature'); + const context = { chatSessionContext: undefined } as vscode.ChatContext; + const stream = new MockChatResponseStream(); + const token = disposables.add(new CancellationTokenSource()).token; + + await participant.createHandler()(request, context, stream, token); + + expect(manager.sessions.size).toBe(0); + }); + + it('handles /delegate command for new session without uncommitted changes', async () => { + expect(manager.sessions.size).toBe(0); + git.activeRepository = { get: () => ({ changes: { indexChanges: [], workingTree: [] } }) } as unknown as IGitService['activeRepository']; + const request = new TestChatRequest('/delegate Build feature'); const context = createChatContext('existing-delegate', true); const stream = new MockChatResponseStream(); const token = disposables.add(new CancellationTokenSource()).token;