Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
7 changes: 4 additions & 3 deletions src/extension/agents/claude/node/claudeCodeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.ChatResult & { claudeSessionId?: string }> {
public async handleRequest(claudeSessionId: string | undefined, request: vscode.ChatRequest, _context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken, workingDirectory?: string): Promise<vscode.ChatResult & { claudeSessionId?: string }> {
try {
// Get server config, start server if needed
const serverConfig = (await this.getLangModelServer()).getConfig();
Expand All @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down
12 changes: 6 additions & 6 deletions src/extension/agents/claude/node/test/claudeCodeAgent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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');
Expand Down
12 changes: 10 additions & 2 deletions src/extension/chatSessions/vscode-node/chatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string, any> = {};
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
}

export class ClaudeSessionDataStore {
private static StorageKey = 'claudeSessionIds';
private _internalSessionToInitialRequest: Map<string, vscode.ChatRequest> = new Map();
private _internalSessionToInitialRequest: Map<string, ClaudeChatRequest> = new Map();
private _unresolvedNewSessions = new Map<string, { id: string; label: string }>();

constructor(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Loading