diff --git a/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index b8d8b2d46a..930d16d505 100644 --- a/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -70,6 +70,20 @@ export class CopilotCLIWorktreeManager { }); } + async createWorktreeSilently(): Promise { + try { + const repository = this.gitService.activeRepository.get(); + if (!repository) { + return undefined; + } + const worktreePath = await this.gitService.createWorktree(repository.rootUri); + return worktreePath; + } catch (error) { + // Silent failure - no warnings displayed + return undefined; + } + } + async storeWorktreePath(sessionId: string, workingDirectory: string): Promise { this._sessionWorktrees.set(sessionId, workingDirectory); const sessionWorktrees = this.extensionContext.globalState.get>(CopilotCLIWorktreeManager.COPILOT_CLI_SESSION_WORKTREE_MEMENTO_KEY, {}); @@ -534,8 +548,23 @@ export class CopilotCLIChatSessionParticipant extends Disposable { const history = await this.summarizer.provideChatSummary(context, token); const requestPrompt = history ? `${prompt}\n**Summary**\n${history}` : prompt; - const session = await this.sessionService.createSession(requestPrompt, {}, token); + + // Create a temporary session ID to check isolation preference + const tempSessionId = `untitled-${Date.now()}`; + const isolationEnabled = this.worktreeManager.getIsolationPreference(tempSessionId); + + // Create worktree silently if isolation is enabled, before creating the session + const workingDirectory = isolationEnabled + ? await this.worktreeManager.createWorktreeSilently() ?? await this.getDefaultWorkingDirectory() + : await this.getDefaultWorkingDirectory(); + + const session = await this.sessionService.createSession(requestPrompt, { workingDirectory, isolationEnabled }, token); try { + // Store the worktree path for this session + if (workingDirectory) { + await this.worktreeManager.storeWorktreePath(session.object.sessionId, workingDirectory); + } + await this.commandExecutionService.executeCommand('vscode.open', SessionIdForCLI.getResource(session.object.sessionId)); await this.commandExecutionService.executeCommand('workbench.action.chat.submit', { inputValue: requestPrompt }); return {}; diff --git a/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index d0f30d88df..652a0745a2 100644 --- a/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -51,6 +51,7 @@ vi.mock('../copilotCLITerminalIntegration', () => { class FakeWorktreeManager extends mock() { override createWorktree = vi.fn(async () => undefined); + override createWorktreeSilently = vi.fn(async (): Promise => undefined); override storeWorktreePath = vi.fn(async () => { }); override getWorktreePath = vi.fn((_id: string) => undefined); override getIsolationPreference = vi.fn(() => false); @@ -293,6 +294,28 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { expect(execSpy.mock.calls[1]).toEqual(['workbench.action.chat.submit', { inputValue: expectedPrompt }]); }); + it('invokes handlePushConfirmationData with isolation enabled creates worktree silently', async () => { + // Enable isolation preference + worktree.getIsolationPreference = vi.fn(() => true); + worktree.createWorktreeSilently = vi.fn(async () => '/tmp/worktree-path'); + + const request = new TestChatRequest('Push this'); + const context = { chatSessionContext: undefined, chatSummary: undefined } as unknown as vscode.ChatContext; + const stream = new MockChatResponseStream(); + const token = disposables.add(new CancellationTokenSource()).token; + + await participant.createHandler()(request, context, stream, token); + + // Verify worktree was created silently (not with stream parameter) + expect(worktree.createWorktreeSilently).toHaveBeenCalledTimes(1); + expect(worktree.createWorktree).not.toHaveBeenCalled(); + + // Verify worktree path was stored + expect(worktree.storeWorktreePath).toHaveBeenCalledTimes(1); + const sessionId = Array.from(manager.sessions.keys())[0]; + expect(worktree.storeWorktreePath).toHaveBeenCalledWith(sessionId, '/tmp/worktree-path'); + }); + it('handleConfirmationData accepts uncommitted-changes and records push', async () => { // Existing session (non-untitled) so confirmation path is hit const sessionId = 'existing-confirm';