Skip to content

Add worktree isolation support for Claude sessions#309691

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/research-agents-app-session-targets
Draft

Add worktree isolation support for Claude sessions#309691
Copilot wants to merge 2 commits intomainfrom
copilot/research-agents-app-session-targets

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 14, 2026

Wire IChatSessionWorktreeService into ClaudeChatSessionContentProvider so 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 routing getFolderInfoForSession() through the worktree service when isolation is enabled.

Changes

claudeChatSessionContentProvider.ts

  • Inject IChatSessionWorktreeService; getFolderInfoForSession() now checks for stored worktree properties and uses the worktree path as cwd
  • New _initializeWorktreeForNewSession() calls initializeFolderRepository() with IsolationMode.Worktree for new sessions; returns false to abort the request when the user cancels or denies trust
  • createHandler() checks the return value of _initializeWorktreeForNewSession() and aborts early on cancellation or trust denial
  • createHandler() calls handleRequestCompleted() after each turn to auto-commit worktree changes, gated on !token.isCancellationRequested to avoid committing changes for cancelled turns
  • Add "Isolation" option group (Worktree/Workspace) to provideChatSessionProviderOptions(), defaulting to Worktree
  • Extend session metadata to track isolationMode; lock option for existing sessions
  • Expose controller getter and onDidChangeChatSessionItemState for lifecycle hooks

chatSessions.ts

  • Subscribe to Claude controller's onDidChangeChatSessionItemState to call cleanupWorktreeOnArchive / recreateWorktreeOnUnarchive (mirrors existing Copilot CLI pattern)
  • Use correct logService.error(error as Error, message) argument order for structured logging

Test updates

  • Add IChatSessionWorktreeService mock and onDidChangeChatSessionItemState to test fixtures; all 77 existing tests pass

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>
Copilot AI review requested due to automatic review settings April 14, 2026 01:14
Copilot AI review requested due to automatic review settings April 14, 2026 01:14
@TylerLeonhardt TylerLeonhardt requested a review from Copilot April 14, 2026 01:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 cwd resolution 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 to handleRequest() 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.error signature is (error: string | Error, message?: string), but this call passes the message first and the Error object second. This should be logService.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

Comment on lines +301 to +302
await this._initializeWorktreeForNewSession(effectiveSessionId, stream, request.toolInvocationToken, token);
if (token.isCancellationRequested) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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) {

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +206
const folderInfo = await this.folderRepositoryManager.initializeFolderRepository(
sessionId,
{
stream,
toolInvocationToken,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +177
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);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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}`);

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +330
// 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}`);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses AI. Check for mistakes.
@TylerLeonhardt
Copy link
Copy Markdown
Member

@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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

@copilot address the comments how you see fit

Addressed all review comments in 8d635ae:

  1. logService.error argument order — Swapped to logService.error(error as Error, message) in both chatSessions.ts lifecycle hooks
  2. cancelled/trusted checks_initializeWorktreeForNewSession now returns false when folderInfo.cancelled || folderInfo.trusted === false, and createHandler aborts the request accordingly
  3. Gate handleRequestCompleted on cancellation — Wrapped in if (!token.isCancellationRequested) and switched from logService.warn with string interpolation to logService.error with a proper Error object

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants