Add worktree isolation support for Claude sessions#309691
Add worktree isolation support for Claude sessions#309691
Conversation
Wire IChatSessionWorktreeService into ClaudeChatSessionContentProvider: - getFolderInfoForSession() checks for existing worktree properties - New sessions with worktree isolation call initializeFolderRepository() - handleRequestCompleted() auto-commits worktree changes after each turn - Add isolation mode option to session provider options UI - Wire archive/unarchive lifecycle hooks in chatSessions.ts - Update metadata to track isolationMode per session Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/67fcc2e4-9c3d-4fd5-bed6-fa5297bd5106 Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds worktree-isolation support to Claude chat sessions by wiring IChatSessionWorktreeService into the Claude session content provider and hooking Claude session archive/unarchive lifecycle to worktree cleanup/recreation (mirroring the existing Copilot CLI worktree flow).
Changes:
- Add an “Isolation” session option (Worktree/Workspace) and initialize a worktree for new Claude sessions when Worktree isolation is selected.
- Route Claude session
cwdresolution through stored worktree properties (use worktree path when present) and auto-commit worktree changes after each turn. - Subscribe to Claude session archived-state changes to clean up/recreate worktrees on archive/unarchive.
Show a summary per file
| File | Description |
|---|---|
| extensions/copilot/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts | Adds worktree service injection, isolation option handling, worktree initialization for new sessions, and per-turn worktree completion handling. |
| extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts | Hooks Claude session archived-state changes to worktree cleanup/recreation. |
| extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts | Updates test fixtures/mocks to include IChatSessionWorktreeService and the new controller event. |
Copilot's findings
Comments suppressed due to low confidence (2)
extensions/copilot/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts:305
- Worktree initialization is invoked for new sessions, but the handler doesn't currently have a path to stop request processing when folder/worktree initialization was cancelled or trust was denied (see
FolderRepositoryInfo.cancelled/trusted === false). Add an early-return in the handler after initialization (and ideally emit a user-facing message) so we don't proceed tohandleRequest()when setup didn't complete.
// Initialize worktree for new sessions with worktree isolation
if (isNewSession) {
await this._initializeWorktreeForNewSession(effectiveSessionId, stream, request.toolInvocationToken, token);
if (token.isCancellationRequested) {
return {};
}
}
extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts:178
ILogService.errorsignature is(error: string | Error, message?: string), but this call passes the message first and the Error object second. This should belogService.error(error as Error, <message>)to compile and to preserve exception details correctly.
logService.error(`[Claude] Failed to recreate worktree for unarchived session ${sessionId}:`, error);
}
- Files reviewed: 3/3 changed files
- Comments generated: 4
| await this._initializeWorktreeForNewSession(effectiveSessionId, stream, request.toolInvocationToken, token); | ||
| if (token.isCancellationRequested) { |
There was a problem hiding this comment.
New behavior (worktree initialization for new sessions and isolation selection) isn't covered by this spec file yet. Since this file already has extensive createHandler() tests, it would be good to add cases asserting that: (1) initializeFolderRepository(..., isolation: Worktree) is called when isolation is Worktree, (2) setWorktreeProperties is called when worktreeProperties are returned, and (3) request handling aborts when initializeFolderRepository returns cancelled: true or trusted: false.
| await this._initializeWorktreeForNewSession(effectiveSessionId, stream, request.toolInvocationToken, token); | |
| if (token.isCancellationRequested) { | |
| const initializationResult = await this._initializeWorktreeForNewSession(effectiveSessionId, stream, request.toolInvocationToken, token) as { cancelled?: boolean; trusted?: boolean } | void; | |
| if (token.isCancellationRequested || initializationResult?.cancelled || initializationResult?.trusted === false) { |
| const folderInfo = await this.folderRepositoryManager.initializeFolderRepository( | ||
| sessionId, | ||
| { | ||
| stream, | ||
| toolInvocationToken, |
There was a problem hiding this comment.
initializeFolderRepository can return { cancelled: true } (user canceled uncommitted-changes prompt) or trusted: false (trust denied). The current implementation ignores those signals and continues the request, which can run the Claude session in the wrong folder/workspace even though the user explicitly cancelled/denied trust. Consider checking folderInfo.cancelled / folderInfo.trusted === false here and propagating an abort signal back to createHandler() (e.g., return a boolean) so the request returns early without invoking the agent manager.
This issue also appears on line 299 of the same file.
| logService.error(`[Claude] Failed to cleanup worktree for archived session ${sessionId}:`, error); | ||
| } | ||
| } else { | ||
| try { | ||
| const result = await claudeWorktreeService.recreateWorktreeOnUnarchive(sessionId); | ||
| logService.trace(`[Claude] Worktree recreation for session ${sessionId}: ${result.recreated ? 'recreated' : result.reason}`); | ||
| } catch (error) { | ||
| logService.error(`[Claude] Failed to recreate worktree for unarchived session ${sessionId}:`, error); |
There was a problem hiding this comment.
ILogService.error takes (error: string | Error, message?: string). This call passes the message as the first argument and the Error object as the second, which is a type error and will also drop the stack/exception details from structured logging. Swap the arguments to logService.error(error as Error, <message>) (and keep the message free of error details per the ILogService.error contract).
This issue also appears on line 177 of the same file.
| logService.error(`[Claude] Failed to cleanup worktree for archived session ${sessionId}:`, error); | |
| } | |
| } else { | |
| try { | |
| const result = await claudeWorktreeService.recreateWorktreeOnUnarchive(sessionId); | |
| logService.trace(`[Claude] Worktree recreation for session ${sessionId}: ${result.recreated ? 'recreated' : result.reason}`); | |
| } catch (error) { | |
| logService.error(`[Claude] Failed to recreate worktree for unarchived session ${sessionId}:`, error); | |
| logService.error(error as Error, `[Claude] Failed to cleanup worktree for archived session ${sessionId}`); | |
| } | |
| } else { | |
| try { | |
| const result = await claudeWorktreeService.recreateWorktreeOnUnarchive(sessionId); | |
| logService.trace(`[Claude] Worktree recreation for session ${sessionId}: ${result.recreated ? 'recreated' : result.reason}`); | |
| } catch (error) { | |
| logService.error(error as Error, `[Claude] Failed to recreate worktree for unarchived session ${sessionId}`); |
| // Auto-commit worktree changes after the turn completes | ||
| try { | ||
| await this.worktreeService.handleRequestCompleted(effectiveSessionId); | ||
| } catch (error) { | ||
| this.logService.warn(`[Claude] Failed to handle worktree request completion for session ${effectiveSessionId}: ${error}`); |
There was a problem hiding this comment.
handleRequestCompleted() is executed unconditionally after handleRequest(). This can commit worktree changes even when the request was cancelled (ClaudeAgentManager treats cancellation as a non-error and returns normally) and it also string-interpolates the caught error into a warning (losing stack/details). Consider gating this on !token.isCancellationRequested (and possibly on successful completion) and logging via logService.error(error as Error, ...) rather than embedding ${error} into the message.
See below for a potential fix:
try {
const result = await this.claudeAgentManager.handleRequest(effectiveSessionId, request, context, stream, token, isNewSession, yieldRequested);
this._controller.updateItemStatus(effectiveSessionId, vscode.ChatSessionStatus.Completed, prompt);
// Auto-commit worktree changes after the turn completes
if (!token.isCancellationRequested) {
try {
await this.worktreeService.handleRequestCompleted(effectiveSessionId);
} catch (error) {
const completionError = error instanceof Error ? error : new Error(String(error));
this.logService.error(completionError, `[Claude] Failed to handle worktree request completion for session ${effectiveSessionId}`);
}
}
return result.errorDetails ? { errorDetails: result.errorDetails } : {};
} finally {
// Clear usage handler after request completes
this.sessionStateService.setUsageHandlerForSession(effectiveSessionId, undefined);
}
|
@copilot address the comments how you see fit |
…d, gate worktree commit on cancellation - Fix logService.error argument order in chatSessions.ts lifecycle hooks - _initializeWorktreeForNewSession now returns false when user cancels or denies trust, and createHandler aborts the request accordingly - Gate handleRequestCompleted on !token.isCancellationRequested so worktree changes are not committed for cancelled turns - Use logService.error with proper Error object instead of string interpolation Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/394a2e27-d88d-4b5c-a9ce-ea9463e5a5a8 Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
Addressed all review comments in 8d635ae:
|
Wire
IChatSessionWorktreeServiceintoClaudeChatSessionContentProviderso Claude sessions can run in isolated git worktrees, matching the existing Copilot CLI worktree flow.All worktree infrastructure already exists —
IChatSessionWorktreeService,FolderRepositoryManager, metadata storage — but Claude's content provider never used it. The core change is routinggetFolderInfoForSession()through the worktree service when isolation is enabled.Changes
claudeChatSessionContentProvider.tsIChatSessionWorktreeService;getFolderInfoForSession()now checks for stored worktree properties and uses the worktree path ascwd_initializeWorktreeForNewSession()callsinitializeFolderRepository()withIsolationMode.Worktreefor new sessions; returnsfalseto abort the request when the user cancels or denies trustcreateHandler()checks the return value of_initializeWorktreeForNewSession()and aborts early on cancellation or trust denialcreateHandler()callshandleRequestCompleted()after each turn to auto-commit worktree changes, gated on!token.isCancellationRequestedto avoid committing changes for cancelled turnsprovideChatSessionProviderOptions(), defaulting to WorktreeisolationMode; lock option for existing sessionscontrollergetter andonDidChangeChatSessionItemStatefor lifecycle hookschatSessions.tsonDidChangeChatSessionItemStateto callcleanupWorktreeOnArchive/recreateWorktreeOnUnarchive(mirrors existing Copilot CLI pattern)logService.error(error as Error, message)argument order for structured loggingTest updates
IChatSessionWorktreeServicemock andonDidChangeChatSessionItemStateto test fixtures; all 77 existing tests pass