diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index d0d5c47642e1c..efc7d41633939 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -1295,6 +1295,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { initialSessionOptions: undefined, inputState: { groups: [], + sessionResource: undefined, onDidChange: Event.None } }; diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts index 4a5dfc8221cf5..6ae9071eaeaac 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts @@ -654,7 +654,7 @@ describe('ChatSessionContentProvider', () => { label: 'Test Session', }, initialSessionOptions, - inputState: { groups: [], onDidChange: Event.None }, + inputState: { groups: [], sessionResource: undefined, onDidChange: Event.None }, }, } as vscode.ChatContext; } @@ -764,7 +764,7 @@ describe('ChatSessionContentProvider', () => { label: 'Test Session', }, initialSessionOptions, - inputState: { groups: [], onDidChange: Event.None }, + inputState: { groups: [], sessionResource: undefined, onDidChange: Event.None }, }, } as vscode.ChatContext; } @@ -835,7 +835,7 @@ describe('ChatSessionContentProvider', () => { resource: ClaudeSessionUri.forSessionId(sessionId), label: 'Test Session', }, - inputState: { groups: [], onDidChange: Event.None }, + inputState: { groups: [], sessionResource: undefined, onDidChange: Event.None }, }, } as vscode.ChatContext; } @@ -935,7 +935,7 @@ describe('ChatSessionContentProvider', () => { resource: ClaudeSessionUri.forSessionId(sessionId), label: 'Test Session', }, - inputState: { groups: [], onDidChange: Event.None }, + inputState: { groups: [], sessionResource: undefined, onDidChange: Event.None }, }, } as vscode.ChatContext; } diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/sessionOptionGroupBuilder.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/sessionOptionGroupBuilder.spec.ts index b36e9d044231b..270be0dde4965 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/sessionOptionGroupBuilder.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/sessionOptionGroupBuilder.spec.ts @@ -104,6 +104,14 @@ function makeRef(name: string, type: number = 0 /* Head */): { name: string; typ return { name, type }; } +function createMockChatSessionInputState(groups: readonly vscode.ChatSessionProviderOptionGroup[]): vscode.ChatSessionInputState { + return { + onDidChange: Event.None, + groups, + sessionResource: undefined + }; +} + // ─── Pure function tests ───────────────────────────────────────── describe('SessionOptionGroupBuilder', () => { @@ -130,14 +138,11 @@ describe('SessionOptionGroupBuilder', () => { describe('getSelectedSessionOptions', () => { it('extracts folder, branch, and isolation from input state groups', () => { - const inputState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { id: REPOSITORY_OPTION_ID, name: 'Folder', items: [{ id: '/my-repo', name: 'my-repo' }], selected: { id: '/my-repo', name: 'my-repo' } }, - { id: BRANCH_OPTION_ID, name: 'Branch', items: [{ id: 'main', name: 'main' }], selected: { id: 'main', name: 'main' } }, - { id: ISOLATION_OPTION_ID, name: 'Isolation', items: [{ id: IsolationMode.Worktree, name: 'Worktree' }], selected: { id: IsolationMode.Worktree, name: 'Worktree' } }, - ], - }; + const inputState = createMockChatSessionInputState([ + { id: REPOSITORY_OPTION_ID, name: 'Folder', items: [{ id: '/my-repo', name: 'my-repo' }], selected: { id: '/my-repo', name: 'my-repo' } }, + { id: BRANCH_OPTION_ID, name: 'Branch', items: [{ id: 'main', name: 'main' }], selected: { id: 'main', name: 'main' } }, + { id: ISOLATION_OPTION_ID, name: 'Isolation', items: [{ id: IsolationMode.Worktree, name: 'Worktree' }], selected: { id: IsolationMode.Worktree, name: 'Worktree' } }, + ]); const result = getSelectedSessionOptions(inputState); expect(result.folder?.fsPath).toBe(URI.file('/my-repo').fsPath); expect(result.branch).toBe('main'); @@ -145,10 +150,7 @@ describe('SessionOptionGroupBuilder', () => { }); it('returns undefined values when no groups are present', () => { - const inputState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [], - }; + const inputState = createMockChatSessionInputState([]); const result = getSelectedSessionOptions(inputState); expect(result.folder).toBeUndefined(); expect(result.branch).toBeUndefined(); @@ -156,14 +158,11 @@ describe('SessionOptionGroupBuilder', () => { }); it('returns undefined values when groups have no selection', () => { - const inputState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { id: REPOSITORY_OPTION_ID, name: 'Folder', items: [] }, - { id: BRANCH_OPTION_ID, name: 'Branch', items: [] }, - { id: ISOLATION_OPTION_ID, name: 'Isolation', items: [] }, - ], - }; + const inputState = createMockChatSessionInputState([ + { id: REPOSITORY_OPTION_ID, name: 'Folder', items: [] }, + { id: BRANCH_OPTION_ID, name: 'Branch', items: [] }, + { id: ISOLATION_OPTION_ID, name: 'Isolation', items: [] }, + ]); const result = getSelectedSessionOptions(inputState); expect(result.folder).toBeUndefined(); expect(result.branch).toBeUndefined(); @@ -571,16 +570,13 @@ describe('SessionOptionGroupBuilder', () => { gitService.getRepository.mockResolvedValue(makeRepo('/repo2')); await configurationService.setConfig(ConfigKey.Advanced.CLIBranchSupport, false); - const previousState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: URI.file('/repo1').fsPath, name: 'repo1' }, - }], - }; + const previousState = createMockChatSessionInputState([{ + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: URI.file('/repo1').fsPath, name: 'repo1' }, + }]); const groups = await builder.provideChatSessionProviderOptionGroups(previousState, URI.file('/repo2') as any); const repoGroup = groups.find(g => g.id === REPOSITORY_OPTION_ID); @@ -647,16 +643,13 @@ describe('SessionOptionGroupBuilder', () => { gitService.repositories = [repo]; gitService.getRepository.mockResolvedValue(repo); - const previousState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: ISOLATION_OPTION_ID, - name: 'Isolation', - description: '', - items: [], - selected: { id: IsolationMode.Worktree, name: 'Worktree' }, - }], - }; + const previousState = createMockChatSessionInputState([{ + id: ISOLATION_OPTION_ID, + name: 'Isolation', + description: '', + items: [], + selected: { id: IsolationMode.Worktree, name: 'Worktree' }, + }]); const groups = await builder.provideChatSessionProviderOptionGroups(previousState); const isolationGroup = groups.find(g => g.id === ISOLATION_OPTION_ID); @@ -734,16 +727,13 @@ describe('SessionOptionGroupBuilder', () => { { folder: mruUri2, repository: mruUri2, lastAccessed: Date.now() - 1000 }, ]); - const previousState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: mruUri1.fsPath, name: 'repo-a' }, - }], - }; + const previousState = createMockChatSessionInputState([{ + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: mruUri1.fsPath, name: 'repo-a' }, + }]); const groups = await builder.provideChatSessionProviderOptionGroups(previousState, mruUri2 as any); const repoGroup = groups.find(g => g.id === REPOSITORY_OPTION_ID); @@ -801,16 +791,13 @@ describe('SessionOptionGroupBuilder', () => { ]); gitService.getRepository.mockResolvedValue(undefined); - const previousState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: removedUri.fsPath, name: 'removed-repo' }, - }], - }; + const previousState = createMockChatSessionInputState([{ + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: removedUri.fsPath, name: 'removed-repo' }, + }]); const groups = await builder.provideChatSessionProviderOptionGroups(previousState); const repoGroup = groups.find(g => g.id === REPOSITORY_OPTION_ID); @@ -934,16 +921,13 @@ describe('SessionOptionGroupBuilder', () => { const prevRepo = makeRepo(prevUri.fsPath); gitService.getRepository.mockResolvedValue(prevRepo); - const previousState: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: prevUri.fsPath, name: 'prev-repo' }, - }], - }; + const previousState = createMockChatSessionInputState([{ + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: prevUri.fsPath, name: 'prev-repo' }, + }]); const groups = await builder.provideChatSessionProviderOptionGroups(previousState); const repoGroup = groups.find(g => g.id === REPOSITORY_OPTION_ID); @@ -961,32 +945,29 @@ describe('SessionOptionGroupBuilder', () => { gitService.getRepository.mockResolvedValue(repo); gitService.getRefs.mockResolvedValue([makeRef('main'), makeRef('develop')]); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { - id: ISOLATION_OPTION_ID, - name: 'Isolation', - description: '', - items: [], - selected: { id: IsolationMode.Worktree, name: 'Worktree' }, - }, - { - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: URI.file('/new-repo').fsPath, name: 'new-repo' }, - }, - { - id: BRANCH_OPTION_ID, - name: 'Branch', - description: '', - items: [{ id: 'old-branch', name: 'old-branch' }], - selected: { id: 'old-branch', name: 'old-branch' }, - }, - ], - }; + const state = createMockChatSessionInputState([ + { + id: ISOLATION_OPTION_ID, + name: 'Isolation', + description: '', + items: [], + selected: { id: IsolationMode.Worktree, name: 'Worktree' }, + }, + { + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: URI.file('/new-repo').fsPath, name: 'new-repo' }, + }, + { + id: BRANCH_OPTION_ID, + name: 'Branch', + description: '', + items: [{ id: 'old-branch', name: 'old-branch' }], + selected: { id: 'old-branch', name: 'old-branch' }, + }, + ]); await builder.handleInputStateChange(state); const branchGroup = state.groups.find(g => g.id === BRANCH_OPTION_ID); @@ -999,24 +980,21 @@ describe('SessionOptionGroupBuilder', () => { gitService.getRepository.mockResolvedValue(makeRepo('/repo')); gitService.getRefs.mockResolvedValue([]); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: URI.file('/repo').fsPath, name: 'repo' }, - }, - { - id: BRANCH_OPTION_ID, - name: 'Branch', - description: '', - items: [{ id: 'old', name: 'old' }], - }, - ], - }; + const state = createMockChatSessionInputState([ + { + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: URI.file('/repo').fsPath, name: 'repo' }, + }, + { + id: BRANCH_OPTION_ID, + name: 'Branch', + description: '', + items: [{ id: 'old', name: 'old' }], + }, + ]); await builder.handleInputStateChange(state); const branchGroup = state.groups.find(g => g.id === BRANCH_OPTION_ID); @@ -1026,16 +1004,13 @@ describe('SessionOptionGroupBuilder', () => { it('does not add branch group when branch feature is disabled', async () => { await configurationService.setConfig(ConfigKey.Advanced.CLIBranchSupport, false); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: URI.file('/repo').fsPath, name: 'repo' }, - }], - }; + const state = createMockChatSessionInputState([{ + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: URI.file('/repo').fsPath, name: 'repo' }, + }]); await builder.handleInputStateChange(state); expect(state.groups.find(g => g.id === BRANCH_OPTION_ID)).toBeUndefined(); @@ -1045,16 +1020,13 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); gitService.getRepository.mockResolvedValue(makeRepo('/workspace')); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [{ - id: ISOLATION_OPTION_ID, - name: 'Isolation', - description: '', - items: [], - selected: { id: IsolationMode.Worktree, name: 'Worktree' }, - }], - }; + const state = createMockChatSessionInputState([{ + id: ISOLATION_OPTION_ID, + name: 'Isolation', + description: '', + items: [], + selected: { id: IsolationMode.Worktree, name: 'Worktree' }, + }]); await builder.handleInputStateChange(state); expect(context.globalState.get('github.copilot.cli.lastUsedIsolationOption')).toBe(IsolationMode.Worktree); @@ -1064,28 +1036,25 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); gitService.getRepository.mockResolvedValue(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { - id: ISOLATION_OPTION_ID, - name: 'Isolation', - description: '', - items: [ - { id: IsolationMode.Workspace, name: 'Workspace' }, - { id: IsolationMode.Worktree, name: 'Worktree' }, - ], - selected: { id: IsolationMode.Worktree, name: 'Worktree' }, - }, - { - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: URI.file('/non-git').fsPath, name: 'non-git' }, - }, - ], - }; + const state = createMockChatSessionInputState([ + { + id: ISOLATION_OPTION_ID, + name: 'Isolation', + description: '', + items: [ + { id: IsolationMode.Workspace, name: 'Workspace' }, + { id: IsolationMode.Worktree, name: 'Worktree' }, + ], + selected: { id: IsolationMode.Worktree, name: 'Worktree' }, + }, + { + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: URI.file('/non-git').fsPath, name: 'non-git' }, + }, + ]); await builder.handleInputStateChange(state); @@ -1098,28 +1067,25 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); gitService.getRepository.mockResolvedValue(makeRepo('/workspace')); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { - id: ISOLATION_OPTION_ID, - name: 'Isolation', - description: '', - items: [ - { id: IsolationMode.Workspace, name: 'Workspace', locked: true }, - { id: IsolationMode.Worktree, name: 'Worktree', locked: true }, - ], - selected: { id: IsolationMode.Workspace, name: 'Workspace', locked: true }, - }, - { - id: REPOSITORY_OPTION_ID, - name: 'Folder', - description: '', - items: [], - selected: { id: URI.file('/workspace').fsPath, name: 'workspace' }, - }, - ], - }; + const state = createMockChatSessionInputState([ + { + id: ISOLATION_OPTION_ID, + name: 'Isolation', + description: '', + items: [ + { id: IsolationMode.Workspace, name: 'Workspace', locked: true }, + { id: IsolationMode.Worktree, name: 'Worktree', locked: true }, + ], + selected: { id: IsolationMode.Workspace, name: 'Workspace', locked: true }, + }, + { + id: REPOSITORY_OPTION_ID, + name: 'Folder', + description: '', + items: [], + selected: { id: URI.file('/workspace').fsPath, name: 'workspace' }, + }, + ]); await builder.handleInputStateChange(state); @@ -1282,10 +1248,7 @@ describe('SessionOptionGroupBuilder', () => { const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); expect(initialGroups.find(g => g.id === REPOSITORY_OPTION_ID)).toBeUndefined(); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Simulate adding a second workspace folder workspaceService = new NullWorkspaceService([URI.file('/workspace'), URI.file('/workspace2')]); @@ -1317,10 +1280,7 @@ describe('SessionOptionGroupBuilder', () => { const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); expect(initialGroups.find(g => g.id === REPOSITORY_OPTION_ID)).toBeDefined(); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Simulate removing a workspace folder workspaceService = new NullWorkspaceService([URI.file('/repo1')]); @@ -1345,10 +1305,7 @@ describe('SessionOptionGroupBuilder', () => { const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); expect(initialGroups.find(g => g.id === BRANCH_OPTION_ID)).toBeUndefined(); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Simulate git init — repo now discovered const repo = makeRepo('/workspace'); @@ -1381,10 +1338,7 @@ describe('SessionOptionGroupBuilder', () => { const repoGroup = initialGroups[repoGroupIndex]; initialGroups[repoGroupIndex] = { ...repoGroup, selected: repoGroup.items.find(i => i.id === URI.file('/repo2').fsPath) }; - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Add a third folder workspaceService = new NullWorkspaceService([URI.file('/repo1'), URI.file('/repo2'), URI.file('/repo3')]); @@ -1415,10 +1369,7 @@ describe('SessionOptionGroupBuilder', () => { // Should be locked to workspace for non-git expect(isolationGroup!.selected?.locked).toBe(true); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Simulate git init const repo = makeRepo('/workspace'); @@ -1445,10 +1396,7 @@ describe('SessionOptionGroupBuilder', () => { // Build initial groups (worktree isolation → branch editable) const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Simulate selecting worktree isolation const isolationIdx = state.groups.findIndex(g => g.id === ISOLATION_OPTION_ID); @@ -1494,10 +1442,7 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Default isolation is workspace → branch should be locked builder.lockInputStateGroups(state); @@ -1516,10 +1461,7 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); builder.lockInputStateGroups(state); await builder.rebuildInputState(state); @@ -1551,10 +1493,7 @@ describe('SessionOptionGroupBuilder', () => { // Initial build — empty const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Simulate "Browse folders…" — rebuild with the browsed folder await builder.rebuildInputState(state, browsedUri as any); @@ -1579,10 +1518,7 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Verify some items are unlocked before locking const isolationBefore = state.groups.find(g => g.id === ISOLATION_OPTION_ID); @@ -1611,10 +1547,7 @@ describe('SessionOptionGroupBuilder', () => { const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); const groupIds = initialGroups.map(g => g.id); const selectedIds = initialGroups.map(g => g.selected?.id); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); builder.lockInputStateGroups(state); @@ -1623,12 +1556,9 @@ describe('SessionOptionGroupBuilder', () => { }); it('handles groups with no selected item', () => { - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { id: 'test', name: 'Test', items: [{ id: 'a', name: 'A' }] }, - ], - }; + const state = createMockChatSessionInputState([ + { id: 'test', name: 'Test', items: [{ id: 'a', name: 'A' }] }, + ]); builder.lockInputStateGroups(state); @@ -1637,10 +1567,7 @@ describe('SessionOptionGroupBuilder', () => { }); it('handles empty groups array', () => { - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [], - }; + const state = createMockChatSessionInputState([]); builder.lockInputStateGroups(state); @@ -1661,10 +1588,7 @@ describe('SessionOptionGroupBuilder', () => { await context.globalState.update('github.copilot.cli.lastUsedIsolationOption', IsolationMode.Worktree); const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); // Verify branch group exists with multiple items (worktree → editable) const branchBefore = state.groups.find(g => g.id === BRANCH_OPTION_ID); @@ -1683,17 +1607,14 @@ describe('SessionOptionGroupBuilder', () => { }); it('does not add branch group when none exists', () => { - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: [ - { - id: ISOLATION_OPTION_ID, - name: 'Isolation', - items: [{ id: IsolationMode.Workspace, name: 'Workspace' }], - selected: { id: IsolationMode.Workspace, name: 'Workspace' }, - }, - ], - }; + const state = createMockChatSessionInputState([ + { + id: ISOLATION_OPTION_ID, + name: 'Isolation', + items: [{ id: IsolationMode.Workspace, name: 'Workspace' }], + selected: { id: IsolationMode.Workspace, name: 'Workspace' }, + }, + ]); builder.updateBranchInInputState(state, 'copilot/my-feature'); @@ -1711,10 +1632,7 @@ describe('SessionOptionGroupBuilder', () => { await configurationService.setConfig(ConfigKey.Advanced.CLIIsolationOption, true); const initialGroups = await builder.provideChatSessionProviderOptionGroups(undefined); - const state: vscode.ChatSessionInputState = { - onDidChange: Event.None, - groups: initialGroups, - }; + const state = createMockChatSessionInputState(initialGroups); const isolationBefore = state.groups.find(g => g.id === ISOLATION_OPTION_ID); diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 94f2a91c1b563..b48586a40014d 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -44,6 +44,8 @@ class ChatSessionInputStateImpl implements vscode.ChatSessionInputState { readonly #onDidChangeEmitter = new Emitter(); readonly onDidChange = this.#onDidChangeEmitter.event; + sessionResource: vscode.Uri | undefined; + constructor(groups: readonly vscode.ChatSessionProviderOptionGroup[], onChangedDelegate?: () => void) { this.#groups = groups; this.#onChangedDelegate = onChangedDelegate; @@ -605,6 +607,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio controllerData?.optionGroups ?? [], context.initialSessionOptions ); + if (inputState instanceof ChatSessionInputStateImpl) { + inputState.sessionResource = isUntitledChatSession(sessionResource) ? undefined : sessionResource; + } + const session = await provider.provider.provideChatSessionContent(sessionResource, token, { sessionOptions: context?.initialSessionOptions ?? [], inputState, @@ -862,17 +868,23 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio ): Promise { const scheme = sessionResource?.scheme; const controllerData = scheme ? this.getChatSessionItemController(scheme) : undefined; + const resolvedResource = sessionResource && !isUntitledChatSession(sessionResource) ? sessionResource : undefined; if (controllerData?.controller.getChatSessionInputState) { const result = await controllerData.controller.getChatSessionInputState( - sessionResource && !isUntitledChatSession(sessionResource) ? sessionResource : undefined, + resolvedResource, { previousInputState: this._createInputStateFromOptions(controllerData.optionGroups ?? [], initialSessionOptions) }, token, ); if (result) { + if (result instanceof ChatSessionInputStateImpl) { + result.sessionResource = resolvedResource; + } return result; } } - return this._createInputStateFromOptions(controllerData?.optionGroups ?? [], initialSessionOptions); + const fallback = this._createInputStateFromOptions(controllerData?.optionGroups ?? [], initialSessionOptions); + fallback.sessionResource = resolvedResource; + return fallback; } /** @@ -1099,6 +1111,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return undefined; } + if (inputState instanceof ChatSessionInputStateImpl) { + inputState.sessionResource = sessionResource; + } + // Store the option groups for onSearch callbacks controllerData.optionGroups = inputState.groups; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 6a394c158b2ae..eaaf153b3ab3a 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -728,9 +728,18 @@ declare module 'vscode' { export interface ChatSessionInputState { /** * Fired when the input state is changed by the user. + * + * Move to controller? */ readonly onDidChange: Event; + /** + * The resource associated with this chat session. + * + * This is `undefined` for chat sessions that have not yet started. + */ + readonly sessionResource: Uri | undefined; + /** * The groups of options to show in the UI for user input. *