diff --git a/extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeAgent.ts b/extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeAgent.ts index 3328eae748db4..537b033549d51 100644 --- a/extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeAgent.ts +++ b/extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeAgent.ts @@ -182,6 +182,21 @@ export class ClaudeCodeSession extends Disposable { } } + /** + * Sets the reasoning effort on the active SDK session, or stores it for the next session start. + */ + private async _setEffort(effort: EffortLevel | undefined): Promise { + if (effort === this._currentEffort) { + return; + } + this._currentEffort = effort; + if (this._queryGenerator) { + this.logService.trace(`[ClaudeCodeSession] Setting effort to ${effort} on active session`); + // Settings.effortLevel does not include 'max'; the SDK treats it as a 'high' fallback. + await this._queryGenerator.applyFlagSettings({ effortLevel: effort as 'low' | 'medium' | 'high' | 'xhigh' | undefined }); + } + } + constructor( private readonly langModelServer: ClaudeLanguageModelServer, public readonly sessionId: string, @@ -520,16 +535,17 @@ export class ClaudeCodeSession extends Disposable { } // Check non-hot-swappable changes that require a session restart - if (request.effort !== this._currentEffort || !this._toolsMatch(request.toolsSnapshot)) { + if (!this._toolsMatch(request.toolsSnapshot)) { this._queuedRequests.unshift(request); this._pendingRestart = true; this._isResumed = true; return; } - // Hot-swap model and permission mode on the active session + // Hot-swap model, permission mode, and effort on the active session await this._setModel(request.modelId); await this._setPermissionMode(request.permissionMode); + await this._setEffort(request.effort); // Mark this request as yielded to the SDK; it becomes the current request. this._inFlightRequests.push(request); @@ -652,7 +668,7 @@ export class ClaudeCodeSession extends Disposable { throw new Error('Session ended unexpectedly'); } catch (error) { // Graceful restart: the prompt iterable detected a non-hot-swappable change - // (effort or tools). Preserve queued requests and start a fresh session. + // (tools). Preserve queued requests and start a fresh session. if (this._pendingRestart) { this._pendingRestart = false; this._restartSession(); diff --git a/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeAgent.spec.ts b/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeAgent.spec.ts index 926e36325ce5e..254dc5eb239db 100644 --- a/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeAgent.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeAgent.spec.ts @@ -506,10 +506,11 @@ describe('ClaudeCodeSession', () => { expect(mockService.lastQueryOptions?.effort).toBeUndefined(); }); - it('restarts session when effort level changes', async () => { + it('calls applyFlagSettings when effort level changes instead of restarting session', async () => { const mockServer = createMockLangModelServer(); const mockService = instantiationService.invokeFunction(accessor => accessor.get(IClaudeCodeSdkService)) as MockClaudeCodeSdkService; mockService.queryCallCount = 0; + mockService.applyFlagSettingsCallCount = 0; commitTestState(sessionStateService, 'test-session', TEST_MODEL_ID); const session = store.add(instantiationService.createInstance(ClaudeCodeSession, mockServer, 'test-session', true)); @@ -522,16 +523,19 @@ describe('ClaudeCodeSession', () => { // Change effort level sessionStateService.setReasoningEffortForSession('test-session', 'high'); - // Second request should restart session (new query created) + // Second request should hot-swap effort, not restart the session const stream2 = new MockChatResponseStream(); await session.invoke(createMockChatRequest('Hello again'), stream2, undefined, CancellationToken.None); - expect(mockService.queryCallCount).toBe(2); + expect(mockService.queryCallCount).toBe(1); + expect(mockService.applyFlagSettingsCallCount).toBe(1); + expect(mockService.lastAppliedFlagSettings).toEqual({ effortLevel: 'high' }); }); - it('does not restart session when effort level is unchanged', async () => { + it('does not call applyFlagSettings when effort level is unchanged', async () => { const mockServer = createMockLangModelServer(); const mockService = instantiationService.invokeFunction(accessor => accessor.get(IClaudeCodeSdkService)) as MockClaudeCodeSdkService; mockService.queryCallCount = 0; + mockService.applyFlagSettingsCallCount = 0; commitTestState(sessionStateService, 'test-session', TEST_MODEL_ID); sessionStateService.setReasoningEffortForSession('test-session', 'medium'); @@ -546,6 +550,7 @@ describe('ClaudeCodeSession', () => { const stream2 = new MockChatResponseStream(); await session.invoke(createMockChatRequest('Hello again'), stream2, undefined, CancellationToken.None); expect(mockService.queryCallCount).toBe(1); + expect(mockService.applyFlagSettingsCallCount).toBe(0); }); }); @@ -733,7 +738,7 @@ describe('ClaudeCodeSession - settings change restart', () => { }); }); -describe('ClaudeCodeSession - effort and tools restart', () => { +describe('ClaudeCodeSession - tools restart', () => { const store = new DisposableStore(); let instantiationService: IInstantiationService; let sessionStateService: IClaudeSessionStateService; @@ -753,7 +758,7 @@ describe('ClaudeCodeSession - effort and tools restart', () => { vi.resetAllMocks(); }); - it('uses resume after effort change restart', async () => { + it('hot-swaps effort instead of restarting the session', async () => { const mockServer = createMockLangModelServer(); commitTestState(sessionStateService, 'test-session'); const session = store.add(instantiationService.createInstance(ClaudeCodeSession, mockServer, 'test-session', true)); @@ -762,15 +767,17 @@ describe('ClaudeCodeSession - effort and tools restart', () => { const stream1 = new MockChatResponseStream(); await session.invoke(createMockChatRequest('Hello'), stream1, undefined, CancellationToken.None); expect(mockService.lastQueryOptions?.sessionId).toBe('test-session'); + expect(mockService.queryCallCount).toBe(1); // Change effort sessionStateService.setReasoningEffortForSession('test-session', 'high'); - // Restarted session should use resume + // Same query is reused via applyFlagSettings const stream2 = new MockChatResponseStream(); await session.invoke(createMockChatRequest('Hello again'), stream2, undefined, CancellationToken.None); - expect(mockService.lastQueryOptions?.resume).toBe('test-session'); - expect(mockService.lastQueryOptions?.effort).toBe('high'); + expect(mockService.queryCallCount).toBe(1); + expect(mockService.applyFlagSettingsCallCount).toBe(1); + expect(mockService.lastAppliedFlagSettings).toEqual({ effortLevel: 'high' }); }); it('restarts session when MCP tools change', async () => { diff --git a/extensions/copilot/src/extension/chatSessions/claude/node/test/mockClaudeCodeSdkService.ts b/extensions/copilot/src/extension/chatSessions/claude/node/test/mockClaudeCodeSdkService.ts index d30cbf4799905..40b4bd7b42336 100644 --- a/extensions/copilot/src/extension/chatSessions/claude/node/test/mockClaudeCodeSdkService.ts +++ b/extensions/copilot/src/extension/chatSessions/claude/node/test/mockClaudeCodeSdkService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ForkSessionOptions, ForkSessionResult, GetSubagentMessagesOptions, ListSubagentsOptions, Options, Query, SDKAssistantMessage, SDKResultMessage, SDKSessionInfo, SDKUserMessage, SessionMessage } from '@anthropic-ai/claude-agent-sdk'; +import type { ForkSessionOptions, ForkSessionResult, GetSubagentMessagesOptions, ListSubagentsOptions, Options, Query, SDKAssistantMessage, SDKResultMessage, SDKSessionInfo, SDKUserMessage, SessionMessage, Settings } from '@anthropic-ai/claude-agent-sdk'; import type { IClaudeCodeSdkService } from '../claudeCodeSdkService'; /** @@ -16,6 +16,8 @@ export class MockClaudeCodeSdkService implements IClaudeCodeSdkService { public lastSetModel: string | undefined; public setPermissionModeCallCount = 0; public lastSetPermissionMode: string | undefined; + public applyFlagSettingsCallCount = 0; + public lastAppliedFlagSettings: Settings | undefined; public lastQueryOptions: Options | undefined; public readonly receivedMessages: SDKUserMessage[] = []; @@ -82,6 +84,10 @@ export class MockClaudeCodeSdkService implements IClaudeCodeSdkService { this.setPermissionModeCallCount++; this.lastSetPermissionMode = mode; }, + applyFlagSettings: async (settings: Settings) => { + this.applyFlagSettingsCallCount++; + this.lastAppliedFlagSettings = settings; + }, abort: () => { /* no-op for mock */ }, } as unknown as Query; }