豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit b7ed22e

Browse files
Handle initialSessionOptions (#4407)
* Implement permission mode validation and initial session handling in ClaudeChatSessionContentProvider * lint
1 parent f728c43 commit b7ed22e

File tree

2 files changed

+278
-5
lines changed

2 files changed

+278
-5
lines changed

src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ import { IClaudeSlashCommandService } from '../claude/vscode-node/claudeSlashCom
2929
import { FolderRepositoryMRUEntry, IFolderRepositoryManager } from '../common/folderRepositoryManager';
3030
import { buildChatHistory, collectSdkModelIds } from './chatHistoryBuilder';
3131

32+
const permissionModes: ReadonlySet<string> = new Set<PermissionMode>(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk']);
33+
34+
function isPermissionMode(value: string): value is PermissionMode {
35+
return permissionModes.has(value);
36+
}
37+
3238
// Import the tool permission handlers
3339
import '../claude/vscode-node/toolPermissionHandlers/index';
3440

@@ -252,6 +258,25 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
252258
const existingSession = await this.sessionService.getSession(sessionUri, token);
253259
const isNewSession = !existingSession;
254260

261+
// TODO: move these to newChatSessionItemHandler when that API is given the initial options
262+
if (isNewSession) {
263+
if (!this._sessionPermissionModes.has(effectiveSessionId)) {
264+
const initialPermissionMode = chatSessionContext.initialSessionOptions?.find(o => o.optionId === PERMISSION_MODE_OPTION_ID);
265+
if (initialPermissionMode) {
266+
this._sessionPermissionModes.set(effectiveSessionId, initialPermissionMode.value as PermissionMode);
267+
} else {
268+
// Default permission mode if not set via options or session state
269+
this._sessionPermissionModes.set(effectiveSessionId, this._lastUsedPermissionMode);
270+
}
271+
}
272+
if (!this._sessionFolders.has(effectiveSessionId)) {
273+
const initialFolderOption = chatSessionContext.initialSessionOptions?.find(o => o.optionId === FOLDER_OPTION_ID);
274+
if (initialFolderOption && typeof initialFolderOption.value === 'string') {
275+
this._sessionFolders.set(effectiveSessionId, URI.file(initialFolderOption.value));
276+
}
277+
}
278+
}
279+
255280
const modelId = request.model.id;
256281
const permissionMode = this.getPermissionModeForSession(effectiveSessionId);
257282
const folderInfo = await this.getFolderInfoForSession(effectiveSessionId);
@@ -340,12 +365,12 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
340365
let hadUpdate = false;
341366
for (const update of updates) {
342367
if (update.optionId === PERMISSION_MODE_OPTION_ID) {
343-
if (!update.value) {
368+
if (!update.value || !isPermissionMode(update.value)) {
344369
continue;
345370
}
346371
// Store locally; committed to session state service when handling the next request
347-
this._sessionPermissionModes.set(sessionId, update.value as PermissionMode);
348-
this._lastUsedPermissionMode = update.value as PermissionMode;
372+
this._sessionPermissionModes.set(sessionId, update.value);
373+
this._lastUsedPermissionMode = update.value;
349374
hadUpdate = true;
350375
} else if (update.optionId === FOLDER_OPTION_ID && typeof update.value === 'string') {
351376
this._sessionFolders.set(sessionId, URI.file(update.value));

src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts

Lines changed: 250 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { URI } from '../../../../util/vs/base/common/uri';
2727
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
2828
import { ServiceCollection } from '../../../../util/vs/platform/instantiation/common/serviceCollection';
2929
import { ChatRequestTurn, ChatRequestTurn2, ChatResponseMarkdownPart, ChatResponseTurn2, ChatSessionStatus, ChatToolInvocationPart, MarkdownString, ThemeIcon } from '../../../../vscodeTypes';
30+
import { createExtensionUnitTestingServices } from '../../../test/node/services';
31+
import { MockChatResponseStream, TestChatRequest } from '../../../test/node/testHelpers';
3032
import { ClaudeSessionUri } from '../../claude/common/claudeSessionUri';
3133
import type { ClaudeAgentManager } from '../../claude/node/claudeCodeAgent';
3234
import { IClaudeCodeModels } from '../../claude/node/claudeCodeModels';
@@ -35,8 +37,6 @@ import { IClaudeSessionTitleService } from '../../claude/node/claudeSessionTitle
3537
import { ClaudeCodeSessionService, IClaudeCodeSessionService } from '../../claude/node/sessionParser/claudeCodeSessionService';
3638
import { IClaudeCodeSessionInfo } from '../../claude/node/sessionParser/claudeSessionSchema';
3739
import { IClaudeSlashCommandService } from '../../claude/vscode-node/claudeSlashCommandService';
38-
import { createExtensionUnitTestingServices } from '../../../test/node/services';
39-
import { MockChatResponseStream, TestChatRequest } from '../../../test/node/testHelpers';
4040
import { FolderRepositoryMRUEntry, IFolderRepositoryManager } from '../../common/folderRepositoryManager';
4141
import { ClaudeChatSessionContentProvider, ClaudeChatSessionItemController } from '../claudeChatSessionContentProvider';
4242

@@ -626,6 +626,254 @@ describe('ChatSessionContentProvider', () => {
626626
const permissionMode = provider.getPermissionModeForSession('test-session');
627627
expect(permissionMode).toBe('plan');
628628
});
629+
630+
it('ignores invalid permission mode values in provideHandleOptionsChange', async () => {
631+
const sessionUri = createClaudeSessionUri('test-session');
632+
633+
await provider.provideHandleOptionsChange(
634+
sessionUri,
635+
[{ optionId: 'permissionMode', value: 'not-a-real-mode' }],
636+
CancellationToken.None,
637+
);
638+
639+
// Should fall through to session state service default, not store the invalid value
640+
const permissionMode = provider.getPermissionModeForSession('test-session');
641+
expect(permissionMode).not.toBe('not-a-real-mode');
642+
});
643+
644+
it('ignores empty permission mode value in provideHandleOptionsChange', async () => {
645+
const sessionUri = createClaudeSessionUri('test-session');
646+
647+
await provider.provideHandleOptionsChange(
648+
sessionUri,
649+
[{ optionId: 'permissionMode', value: '' }],
650+
CancellationToken.None,
651+
);
652+
653+
// Should not store empty string as permission mode
654+
const permissionMode = provider.getPermissionModeForSession('test-session');
655+
expect(permissionMode).not.toBe('');
656+
});
657+
658+
it('accepts all valid permission modes in provideHandleOptionsChange', async () => {
659+
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk'] as const;
660+
661+
for (const mode of validModes) {
662+
const sessionUri = createClaudeSessionUri(`test-session-${mode}`);
663+
await provider.provideHandleOptionsChange(
664+
sessionUri,
665+
[{ optionId: 'permissionMode', value: mode }],
666+
CancellationToken.None,
667+
);
668+
669+
const permissionMode = provider.getPermissionModeForSession(`test-session-${mode}`);
670+
expect(permissionMode).toBe(mode);
671+
}
672+
});
673+
674+
it('does not update _lastUsedPermissionMode when invalid mode is provided', async () => {
675+
// First set a valid mode
676+
const sessionUri1 = createClaudeSessionUri('session-valid');
677+
await provider.provideHandleOptionsChange(
678+
sessionUri1,
679+
[{ optionId: 'permissionMode', value: 'plan' }],
680+
CancellationToken.None,
681+
);
682+
683+
// Try to set an invalid mode on a different session
684+
const sessionUri2 = createClaudeSessionUri('session-invalid');
685+
await provider.provideHandleOptionsChange(
686+
sessionUri2,
687+
[{ optionId: 'permissionMode', value: 'bogus' }],
688+
CancellationToken.None,
689+
);
690+
691+
// newSessionOptions should still reflect the last valid mode
692+
const options = await provider.provideChatSessionProviderOptions();
693+
expect(options.newSessionOptions!['permissionMode']).toBe('plan');
694+
});
695+
});
696+
697+
// #endregion
698+
699+
// #region Initial Session Options
700+
701+
describe('initial session options on new sessions', () => {
702+
let mockAgentManager: ClaudeAgentManager;
703+
let handlerProvider: ClaudeChatSessionContentProvider;
704+
705+
function createChatContext(sessionId: string, initialSessionOptions?: Array<{ optionId: string; value: string }>): vscode.ChatContext {
706+
return {
707+
history: [],
708+
yieldRequested: false,
709+
chatSessionContext: {
710+
isUntitled: false,
711+
chatSessionItem: {
712+
resource: ClaudeSessionUri.forSessionId(sessionId),
713+
label: 'Test Session',
714+
},
715+
initialSessionOptions,
716+
},
717+
} as vscode.ChatContext;
718+
}
719+
720+
beforeEach(() => {
721+
const mocks = createDefaultMocks();
722+
mockSessionService = mocks.mockSessionService;
723+
mockClaudeCodeModels = mocks.mockClaudeCodeModels;
724+
mockFolderRepositoryManager = mocks.mockFolderRepositoryManager;
725+
mockAgentManager = createMockAgentManager();
726+
727+
const result = createProviderWithServices(store, [workspaceFolderUri], mocks, mockAgentManager);
728+
handlerProvider = result.provider;
729+
});
730+
731+
it('sets permission mode from initialSessionOptions on new session', async () => {
732+
vi.mocked(mockSessionService.getSession).mockResolvedValue(undefined);
733+
734+
const handler = handlerProvider.createHandler();
735+
const context = createChatContext('new-session-1', [
736+
{ optionId: 'permissionMode', value: 'plan' },
737+
]);
738+
const stream = new MockChatResponseStream();
739+
740+
await handler(createTestRequest('hello'), context, stream, CancellationToken.None);
741+
742+
// The handler commits state — verify the permission mode was used
743+
expect(handlerProvider.getPermissionModeForSession('new-session-1')).toBe('plan');
744+
});
745+
746+
it('defaults to _lastUsedPermissionMode when initialSessionOptions has no permission mode', async () => {
747+
vi.mocked(mockSessionService.getSession).mockResolvedValue(undefined);
748+
749+
// First, set the last used mode to 'plan' via a different session
750+
const setupUri = createClaudeSessionUri('setup-session');
751+
await handlerProvider.provideHandleOptionsChange(
752+
setupUri,
753+
[{ optionId: 'permissionMode', value: 'plan' }],
754+
CancellationToken.None,
755+
);
756+
757+
const handler = handlerProvider.createHandler();
758+
const context = createChatContext('new-session-2');
759+
const stream = new MockChatResponseStream();
760+
761+
await handler(createTestRequest('hello'), context, stream, CancellationToken.None);
762+
763+
expect(handlerProvider.getPermissionModeForSession('new-session-2')).toBe('plan');
764+
});
765+
766+
it('does not overwrite permission mode if already set for the session', async () => {
767+
vi.mocked(mockSessionService.getSession).mockResolvedValue(undefined);
768+
769+
// Pre-set permission mode via provideHandleOptionsChange
770+
const sessionUri = createClaudeSessionUri('pre-set-session');
771+
await handlerProvider.provideHandleOptionsChange(
772+
sessionUri,
773+
[{ optionId: 'permissionMode', value: 'default' }],
774+
CancellationToken.None,
775+
);
776+
777+
const handler = handlerProvider.createHandler();
778+
const context = createChatContext('pre-set-session', [
779+
{ optionId: 'permissionMode', value: 'plan' },
780+
]);
781+
const stream = new MockChatResponseStream();
782+
783+
await handler(createTestRequest('hello'), context, stream, CancellationToken.None);
784+
785+
// Should keep the pre-set value, not overwrite with initialSessionOptions
786+
expect(handlerProvider.getPermissionModeForSession('pre-set-session')).toBe('default');
787+
});
788+
789+
it('does not apply initialSessionOptions on resumed sessions', async () => {
790+
// Session exists on disk → not new
791+
vi.mocked(mockSessionService.getSession).mockResolvedValue({
792+
id: 'existing-session',
793+
messages: [{ type: 'user', message: { role: 'user', content: 'Hello' } }],
794+
subagents: [],
795+
} as any);
796+
797+
const handler = handlerProvider.createHandler();
798+
const context = createChatContext('existing-session', [
799+
{ optionId: 'permissionMode', value: 'bypassPermissions' },
800+
]);
801+
const stream = new MockChatResponseStream();
802+
803+
await handler(createTestRequest('hello'), context, stream, CancellationToken.None);
804+
805+
// Should not have been set from initialSessionOptions since session is not new
806+
expect(handlerProvider.getPermissionModeForSession('existing-session')).not.toBe('bypassPermissions');
807+
});
808+
});
809+
810+
describe('initial folder option on new sessions', () => {
811+
const folderA = URI.file('/project-a');
812+
const folderB = URI.file('/project-b');
813+
let mockAgentManager: ClaudeAgentManager;
814+
let multiRootProvider: ClaudeChatSessionContentProvider;
815+
816+
function createChatContext(sessionId: string, initialSessionOptions?: Array<{ optionId: string; value: string }>): vscode.ChatContext {
817+
return {
818+
history: [],
819+
yieldRequested: false,
820+
chatSessionContext: {
821+
isUntitled: false,
822+
chatSessionItem: {
823+
resource: ClaudeSessionUri.forSessionId(sessionId),
824+
label: 'Test Session',
825+
},
826+
initialSessionOptions,
827+
},
828+
} as vscode.ChatContext;
829+
}
830+
831+
beforeEach(() => {
832+
const mocks = createDefaultMocks();
833+
mockSessionService = mocks.mockSessionService;
834+
mockAgentManager = createMockAgentManager();
835+
836+
const result = createProviderWithServices(store, [folderA, folderB], mocks, mockAgentManager);
837+
multiRootProvider = result.provider;
838+
});
839+
840+
it('sets folder from initialSessionOptions on new session', async () => {
841+
vi.mocked(mockSessionService.getSession).mockResolvedValue(undefined);
842+
843+
const handler = multiRootProvider.createHandler();
844+
const context = createChatContext('new-folder-session', [
845+
{ optionId: 'folder', value: folderB.fsPath },
846+
]);
847+
const stream = new MockChatResponseStream();
848+
849+
await handler(createTestRequest('hello'), context, stream, CancellationToken.None);
850+
851+
const folderInfo = await multiRootProvider.getFolderInfoForSession('new-folder-session');
852+
expect(folderInfo.cwd).toBe(folderB.fsPath);
853+
});
854+
855+
it('does not overwrite folder if already set for the session', async () => {
856+
vi.mocked(mockSessionService.getSession).mockResolvedValue(undefined);
857+
858+
// Pre-set folder via provideHandleOptionsChange
859+
const sessionUri = createClaudeSessionUri('pre-folder-session');
860+
await multiRootProvider.provideHandleOptionsChange(
861+
sessionUri,
862+
[{ optionId: 'folder', value: folderA.fsPath }],
863+
CancellationToken.None,
864+
);
865+
866+
const handler = multiRootProvider.createHandler();
867+
const context = createChatContext('pre-folder-session', [
868+
{ optionId: 'folder', value: folderB.fsPath },
869+
]);
870+
const stream = new MockChatResponseStream();
871+
872+
await handler(createTestRequest('hello'), context, stream, CancellationToken.None);
873+
874+
const folderInfo = await multiRootProvider.getFolderInfoForSession('pre-folder-session');
875+
expect(folderInfo.cwd).toBe(folderA.fsPath);
876+
});
629877
});
630878

631879
// #endregion

0 commit comments

Comments
 (0)