diff --git a/package.json b/package.json index 0c11c68b3..b512b0d53 100644 --- a/package.json +++ b/package.json @@ -1799,6 +1799,11 @@ "title": "Refresh Claude Code Sessions", "icon": "$(refresh)", "category": "Claude Code" + }, + { + "command": "github.copilot.claude.sessions.create.worktree", + "title": "%github.copilot.command.claude.session.create.worktree%", + "category": "Claude Code" }, { "command": "github.copilot.chat.replay", @@ -3630,6 +3635,12 @@ "group": "z_chat@1", "when": "config.git.enabled && !git.missing && !isInDiffEditor && !isMergeEditor && resource in git.mergeChanges && mergeConflictsCount && mergeConflictsCount != 0" } + ], + "chat/chatSessions": [ + { + "command": "github.copilot.claude.sessions.create.worktree", + "group": "submenu" + } ] }, "icons": { diff --git a/package.nls.json b/package.nls.json index da2b64ce7..615da9f96 100644 --- a/package.nls.json +++ b/package.nls.json @@ -31,6 +31,7 @@ "github.copilot.command.sendChatFeedback": "Send Chat Feedback", "github.copilot.command.buildLocalWorkspaceIndex": "Build Local Workspace Index", "github.copilot.command.buildRemoteWorkspaceIndex": "Build Remote Workspace Index", + "github.copilot.command.claude.session.create.worktree": "Create Claude Code Session in a Worktree", "github.copilot.viewsWelcome.signIn": { "message": "Sign in to enable features powered by GitHub Copilot.\n\n[Sign in](command:workbench.action.chat.triggerSetupForceSignIn)", "comment": [ diff --git a/src/extension/agents/claude/node/claudeCodeAgent.ts b/src/extension/agents/claude/node/claudeCodeAgent.ts index 0dbeddb26..3198d7185 100644 --- a/src/extension/agents/claude/node/claudeCodeAgent.ts +++ b/src/extension/agents/claude/node/claudeCodeAgent.ts @@ -47,7 +47,7 @@ export class ClaudeAgentManager extends Disposable { super(); } - public async handleRequest(claudeSessionId: string | undefined, request: vscode.ChatRequest, _context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise { + public async handleRequest(claudeSessionId: string | undefined, request: vscode.ChatRequest, _context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken, workingDirectory?: string): Promise { try { // Get server config, start server if needed const serverConfig = (await this.getLangModelServer()).getConfig(); @@ -60,7 +60,7 @@ export class ClaudeAgentManager extends Disposable { session = this._sessions.get(claudeSessionId)!; } else { this.logService.trace(`[ClaudeAgentManager] Creating Claude session for sessionId=${sessionIdForLog}.`); - const newSession = this.instantiationService.createInstance(ClaudeCodeSession, serverConfig, claudeSessionId); + const newSession = this.instantiationService.createInstance(ClaudeCodeSession, serverConfig, claudeSessionId, workingDirectory); if (newSession.sessionId) { this._sessions.set(newSession.sessionId, newSession); } @@ -157,6 +157,7 @@ export class ClaudeCodeSession extends Disposable { constructor( private readonly serverConfig: ILanguageModelServerConfig, public sessionId: string | undefined, + private readonly workingDirectory: string | undefined, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configService: IConfigurationService, @IWorkspaceService private readonly workspaceService: IWorkspaceService, @@ -239,7 +240,7 @@ export class ClaudeCodeSession extends Disposable { this.logService.trace(`appRoot: ${this.envService.appRoot}`); const pathSep = isWindows ? ';' : ':'; const options: Options = { - cwd: this.workspaceService.getWorkspaceFolders().at(0)?.fsPath, + cwd: this.workingDirectory || this.workspaceService.getWorkspaceFolders().at(0)?.fsPath, abortController: this._abortController, executable: process.execPath as 'node', // get it to fork the EH node process env: { diff --git a/src/extension/agents/claude/node/test/claudeCodeAgent.spec.ts b/src/extension/agents/claude/node/test/claudeCodeAgent.spec.ts index 6ac3eddb3..bba34e0cd 100644 --- a/src/extension/agents/claude/node/test/claudeCodeAgent.spec.ts +++ b/src/extension/agents/claude/node/test/claudeCodeAgent.spec.ts @@ -80,7 +80,7 @@ describe('ClaudeCodeSession', () => { it('processes a single request correctly', async () => { const serverConfig = { port: 8080, nonce: 'test-nonce' }; - const session = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session')); + const session = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session', undefined)); const stream = new MockChatResponseStream(); await session.invoke('Hello', {} as vscode.ChatParticipantToolToken, stream, CancellationToken.None); @@ -90,7 +90,7 @@ describe('ClaudeCodeSession', () => { it('queues multiple requests and processes them sequentially', async () => { const serverConfig = { port: 8080, nonce: 'test-nonce' }; - const session = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session')); + const session = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session', undefined)); const stream1 = new MockChatResponseStream(); const stream2 = new MockChatResponseStream(); @@ -109,7 +109,7 @@ describe('ClaudeCodeSession', () => { it('cancels pending requests when cancelled', async () => { const serverConfig = { port: 8080, nonce: 'test-nonce' }; - const session = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session')); + const session = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session', undefined)); const stream = new MockChatResponseStream(); const source = new CancellationTokenSource(); source.cancel(); @@ -119,7 +119,7 @@ describe('ClaudeCodeSession', () => { it('cleans up resources when disposed', async () => { const serverConfig = { port: 8080, nonce: 'test-nonce' }; - const session = instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session'); + const session = instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'test-session', undefined); // Dispose the session immediately session.dispose(); @@ -132,8 +132,8 @@ describe('ClaudeCodeSession', () => { it('handles multiple sessions with different session IDs', async () => { const serverConfig = { port: 8080, nonce: 'test-nonce' }; - const session1 = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'session-1')); - const session2 = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'session-2')); + const session1 = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'session-1', undefined)); + const session2 = store.add(instantiationService.createInstance(ClaudeCodeSession, serverConfig, 'session-2', undefined)); expect(session1.sessionId).toBe('session-1'); expect(session2.sessionId).toBe('session-2'); diff --git a/src/extension/chatSessions/vscode-node/chatSessions.ts b/src/extension/chatSessions/vscode-node/chatSessions.ts index d8155c186..29a3539dd 100644 --- a/src/extension/chatSessions/vscode-node/chatSessions.ts +++ b/src/extension/chatSessions/vscode-node/chatSessions.ts @@ -17,7 +17,7 @@ import { ClaudeChatSessionContentProvider } from './claudeChatSessionContentProv import { ClaudeChatSessionItemProvider, ClaudeSessionDataStore } from './claudeChatSessionItemProvider'; export class ChatSessionsContrib extends Disposable implements IExtensionContribution { - readonly id = 'chatSessions'; + readonly chatSessionType = 'claude-code'; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -33,10 +33,18 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib const sessionStore = claudeAgentInstaService.createInstance(ClaudeSessionDataStore); const sessionItemProvider = this._register(claudeAgentInstaService.createInstance(ClaudeChatSessionItemProvider, sessionStore)); - this._register(vscode.chat.registerChatSessionItemProvider('claude-code', sessionItemProvider)); + this._register(vscode.chat.registerChatSessionItemProvider(this.chatSessionType, sessionItemProvider)); this._register(vscode.commands.registerCommand('github.copilot.claude.sessions.refresh', () => { sessionItemProvider.refresh(); })); + this._register(vscode.commands.registerCommand('github.copilot.claude.sessions.create.worktree', async () => { + const workingDirectory = await vscode.commands.executeCommand('git.createWorktreeWithDefaults'); + const metadata: Record = {}; + if (workingDirectory) { + metadata.workingDirectory = workingDirectory; + } + await vscode.commands.executeCommand('workbench.action.chat.openNewSessionEditorWithMetadata', this.chatSessionType, metadata); + })); const claudeAgentManager = this._register(claudeAgentInstaService.createInstance(ClaudeAgentManager)); const chatSessionContentProvider = claudeAgentInstaService.createInstance(ClaudeChatSessionContentProvider, claudeAgentManager, sessionStore); diff --git a/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts b/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts index d0b2776f4..7a03abd97 100644 --- a/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts +++ b/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts @@ -47,7 +47,7 @@ export class ClaudeChatSessionContentProvider implements vscode.ChatSessionConte async (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => { this._log(`Starting activeResponseCallback, internalID: ${internalSessionId}`); const request = this._createInitialChatRequest(initialRequest, internalSessionId); - const result = await this.claudeAgentManager.handleRequest(undefined, request, { history: [] }, stream, token); + const result = await this.claudeAgentManager.handleRequest(undefined, request, { history: [] }, stream, token, initialRequest.metadata?.workingDirectory); if (result.claudeSessionId) { this._log(`activeResponseCallback, setClaudeSessionId: ${internalSessionId} -> ${result.claudeSessionId}`); this.sessionStore.setClaudeSessionId(internalSessionId, result.claudeSessionId); diff --git a/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts b/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts index fee0d2fd3..02bc17f96 100644 --- a/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts +++ b/src/extension/chatSessions/vscode-node/claudeChatSessionItemProvider.ts @@ -10,9 +10,13 @@ import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import { IClaudeCodeSessionService } from '../../agents/claude/node/claudeCodeSessionService'; +export interface ClaudeChatRequest extends vscode.ChatRequest { + metadata?: Record; +} + export class ClaudeSessionDataStore { private static StorageKey = 'claudeSessionIds'; - private _internalSessionToInitialRequest: Map = new Map(); + private _internalSessionToInitialRequest: Map = new Map(); private _unresolvedNewSessions = new Map(); constructor( @@ -43,11 +47,11 @@ export class ClaudeSessionDataStore { return id; } - public setInitialRequest(internalSessionId: string, request: vscode.ChatRequest) { + public setInitialRequest(internalSessionId: string, request: ClaudeChatRequest) { this._internalSessionToInitialRequest.set(internalSessionId, request); } - public getAndConsumeInitialRequest(sessionId: string): vscode.ChatRequest | undefined { + public getAndConsumeInitialRequest(sessionId: string): ClaudeChatRequest | undefined { const prompt = this._internalSessionToInitialRequest.get(sessionId); this._internalSessionToInitialRequest.delete(sessionId); return prompt; @@ -116,7 +120,7 @@ export class ClaudeChatSessionItemProvider extends Disposable implements vscode. const internal = this.sessionStore.registerNewSession(label); this._onDidChangeChatSessionItems.fire(); if (options.request) { - this.sessionStore.setInitialRequest(internal, options.request); + this.sessionStore.setInitialRequest(internal, { ...options.request, metadata: options.metadata }); } return {