Skip to content

Commit dcacca1

Browse files
refactor: streamline session option group selection logic and improve test coverage (#308743)
* refactor: streamline session option group selection logic and improve test coverage * refactor: enhance chat session initialization with new options structure and improve input state handling * Update extensions/copilot/src/extension/chatSessions/vscode-node/test/sessionOptionGroupBuilder.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updates * Fixes * Fixes * Updates * Updates * More updates * Fix tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent cf92c7d commit dcacca1

10 files changed

+1327
-1202
lines changed

extensions/copilot/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6404,7 +6404,7 @@
64046404
"node-gyp": "npm:node-gyp@10.3.1",
64056405
"zod": "3.25.76"
64066406
},
6407-
"vscodeCommit": "eb014b61a9ac4d91acc39984167e2ca84c03b758",
6407+
"vscodeCommit": "afba0a4a1fc1e34dae9073d6787b6b541bda23eb",
64086408
"__metadata": {
64096409
"id": "7ec7d6e6-b89e-4cc5-a59b-d6c4d238246f",
64106410
"publisherId": {

extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionInitializer.ts

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ import { COPILOT_CLI_REASONING_EFFORT_PROPERTY, ICopilotCLIAgents, ICopilotCLIMo
2323
import { ICopilotCLISession } from '../copilotcli/node/copilotcliSession';
2424
import { ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService';
2525
import { buildMcpServerMappings, McpServerMappings } from '../copilotcli/node/mcpHandler';
26-
import { BRANCH_OPTION_ID, ISOLATION_OPTION_ID, REPOSITORY_OPTION_ID } from './sessionOptionGroupBuilder';
2726
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
2827

2928
function isReasoningEffortFeatureEnabled(configurationService: IConfigurationService): boolean {
3029
return configurationService.getConfig(ConfigKey.Advanced.CLIThinkingEffortEnabled);
3130
}
3231

32+
export interface SessionInitOptions {
33+
isolation?: IsolationMode;
34+
branch?: string;
35+
folder?: vscode.Uri;
36+
newBranch?: Promise<string | undefined>;
37+
stream: vscode.ChatResponseStream;
38+
}
39+
3340
export interface ICopilotCLIChatSessionInitializer {
3441
readonly _serviceBrand: undefined;
3542

@@ -41,9 +48,8 @@ export interface ICopilotCLIChatSessionInitializer {
4148
*/
4249
getOrCreateSession(
4350
request: vscode.ChatRequest,
44-
chatSessionContext: vscode.ChatSessionContext,
45-
stream: vscode.ChatResponseStream,
46-
options: { branchName: Promise<string | undefined> },
51+
chatResource: vscode.Uri,
52+
options: SessionInitOptions,
4753
disposables: DisposableStore,
4854
token: vscode.CancellationToken
4955
): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }>;
@@ -53,10 +59,8 @@ export interface ICopilotCLIChatSessionInitializer {
5359
* Used for both normal requests and delegation flows.
5460
*/
5561
initializeWorkingDirectory(
56-
chatSessionContext: vscode.ChatSessionContext | undefined,
57-
isolation: IsolationMode | undefined,
58-
branchName: Promise<string | undefined> | undefined,
59-
stream: vscode.ChatResponseStream,
62+
chatResource: vscode.Uri | undefined,
63+
options: SessionInitOptions,
6064
toolInvocationToken: vscode.ChatParticipantToolToken,
6165
token: vscode.CancellationToken
6266
): Promise<{ workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean }>;
@@ -94,18 +98,17 @@ export class CopilotCLIChatSessionInitializer implements ICopilotCLIChatSessionI
9498

9599
async getOrCreateSession(
96100
request: vscode.ChatRequest,
97-
chatSessionContext: vscode.ChatSessionContext,
98-
stream: vscode.ChatResponseStream,
99-
options: { branchName: Promise<string | undefined> },
101+
chatResource: vscode.Uri,
102+
options: SessionInitOptions,
100103
disposables: DisposableStore,
101104
token: vscode.CancellationToken
102105
): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }> {
103-
const { resource } = chatSessionContext.chatSessionItem;
104-
const sessionId = SessionIdForCLI.parse(resource);
106+
const sessionId = SessionIdForCLI.parse(chatResource);
105107
const isNewSession = this.sessionService.isNewSessionId(sessionId);
108+
const { stream } = options;
106109

107110
const [{ workspaceInfo, cancelled, trusted }, model, agent] = await Promise.all([
108-
this.initializeWorkingDirectory(chatSessionContext, undefined, options.branchName, stream, request.toolInvocationToken, token),
111+
this.initializeWorkingDirectory(chatResource, options, request.toolInvocationToken, token),
109112
this.resolveModel(request, token),
110113
this.resolveAgent(request, token),
111114
]);
@@ -144,46 +147,35 @@ export class CopilotCLIChatSessionInitializer implements ICopilotCLIChatSessionI
144147
}
145148

146149
async initializeWorkingDirectory(
147-
chatSessionContext: vscode.ChatSessionContext | undefined,
148-
isolation: IsolationMode | undefined,
149-
branchName: Promise<string | undefined> | undefined,
150-
stream: vscode.ChatResponseStream,
150+
chatResource: vscode.Uri | undefined,
151+
options: SessionInitOptions,
151152
toolInvocationToken: vscode.ChatParticipantToolToken,
152153
token: vscode.CancellationToken
153154
): Promise<{ workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean }> {
154155
let folderInfo: FolderRepositoryInfo;
155-
let folder: undefined | vscode.Uri = undefined;
156+
const { stream } = options;
157+
let folder: undefined | vscode.Uri = options?.folder;
156158
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
157-
if (workspaceFolders.length === 1) {
159+
if (workspaceFolders.length === 1 && !folder) {
158160
folder = workspaceFolders[0];
159161
}
160-
if (chatSessionContext) {
161-
const sessionId = SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource);
162+
if (chatResource) {
163+
const sessionId = SessionIdForCLI.parse(chatResource);
162164
const isNewSession = this.sessionService.isNewSessionId(sessionId);
163165

164166
if (isNewSession) {
165-
let isolation = IsolationMode.Workspace;
166-
let branch: string | undefined = undefined;
167-
for (const opt of (chatSessionContext.initialSessionOptions || [])) {
168-
const value = typeof opt.value === 'string' ? opt.value : opt.value.id;
169-
if (opt.optionId === REPOSITORY_OPTION_ID && value) {
170-
folder = vscode.Uri.file(value);
171-
} else if (opt.optionId === BRANCH_OPTION_ID && value) {
172-
branch = value;
173-
} else if (opt.optionId === ISOLATION_OPTION_ID && value) {
174-
isolation = value as IsolationMode;
175-
}
176-
}
167+
const isolation = options?.isolation ?? IsolationMode.Workspace;
168+
const branch = options?.branch;
177169

178170
// Use FolderRepositoryManager to initialize folder/repository with worktree creation
179-
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(sessionId, { stream, toolInvocationToken, branch, isolation, folder, newBranch: branchName }, token);
171+
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(sessionId, { stream, toolInvocationToken, branch, isolation, folder, newBranch: options?.newBranch }, token);
180172
} else {
181173
// Existing session - use getFolderRepository for resolution with trust check
182174
folderInfo = await this.folderRepositoryManager.getFolderRepository(sessionId, { promptForTrust: true, stream }, token);
183175
}
184176
} else {
185177
// No chat session context (e.g., delegation) - initialize with active repository
186-
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation, folder }, token);
178+
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: options?.isolation, folder }, token);
187179
}
188180

189181
if (folderInfo.trusted === false || folderInfo.cancelled) {

extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ import { builtinSlashSCommands, CopilotCLICommand, copilotCLICommands, ICopilotC
4343
import { ICopilotCLISessionItem, ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService';
4444
import { buildMcpServerMappings } from '../copilotcli/node/mcpHandler';
4545
import { ICopilotCLISessionTracker } from '../copilotcli/vscode-node/copilotCLISessionTracker';
46-
import { ICopilotCLIChatSessionInitializer } from './copilotCLIChatSessionInitializer';
46+
import { ICopilotCLIChatSessionInitializer, SessionInitOptions } from './copilotCLIChatSessionInitializer';
4747
import { convertReferenceToVariable } from './copilotCLIPromptReferences';
4848
import { ICopilotCLITerminalIntegration, TerminalOpenLocation } from './copilotCLITerminalIntegration';
4949
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
5050
import { IPullRequestDetectionService } from './pullRequestDetectionService';
51-
import { ISessionOptionGroupBuilder, OPEN_REPOSITORY_COMMAND_ID, toRepositoryOptionItem, toWorkspaceFolderOptionItem } from './sessionOptionGroupBuilder';
51+
import { getSelectedSessionOptions, ISessionOptionGroupBuilder, OPEN_REPOSITORY_COMMAND_ID, toRepositoryOptionItem, toWorkspaceFolderOptionItem } from './sessionOptionGroupBuilder';
5252
import { ISessionRequestLifecycle } from './sessionRequestLifecycle';
53+
import { UNTRUSTED_FOLDER_MESSAGE } from './folderRepositoryManagerImpl';
5354

5455
/**
5556
* ODO:
@@ -267,19 +268,22 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
267268

268269
const newInputStates: WeakRef<vscode.ChatSessionInputState>[] = [];
269270
controller.getChatSessionInputState = async (sessionResource, context, token) => {
270-
const groups = sessionResource ? await this._optionGroupBuilder.buildExistingSessionInputStateGroups(sessionResource, token) : await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.previousInputState);
271-
const state = controller.createChatSessionInputState(groups);
272-
if (!sessionResource) {
271+
const isExistingSession = sessionResource && !this.sessionService.isNewSessionId(SessionIdForCLI.parse(sessionResource));
272+
if (isExistingSession) {
273+
const groups = await this._optionGroupBuilder.buildExistingSessionInputStateGroups(sessionResource, token);
274+
return controller.createChatSessionInputState(groups);
275+
} else {
276+
const groups = await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.previousInputState);
277+
const state = controller.createChatSessionInputState(groups);
273278
// Only wire dynamic updates for new sessions (existing sessions are fully locked).
274279
// Note: don't use the getChatSessionInputState token here — it's a one-shot token
275280
// that may be disposed by the time the user interacts with the dropdowns.
276281
newInputStates.push(new WeakRef(state));
277-
278282
state.onDidChange(() => {
279283
void this._optionGroupBuilder.handleInputStateChange(state);
280284
});
285+
return state;
281286
}
282-
return state;
283287
};
284288

285289
// Refresh new-session dropdown groups when git or workspace state changes
@@ -304,8 +308,9 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
304308
this._register(this._workspaceService.onDidChangeWorkspaceFolders(refreshActiveInputState));
305309
}
306310

307-
provideHandleOptionsChange() {
308-
// This is required for Controller.createChatSessionInputState.onDidChange event to work.
311+
public async updateInputStateAfterFolderSelection(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): Promise<void> {
312+
this._optionGroupBuilder.setNewFolderForInputState(inputState, folderUri);
313+
await this._optionGroupBuilder.rebuildInputState(inputState, folderUri);
309314
}
310315

311316
public async refreshSession(refreshOptions: { reason: 'update'; sessionId: string } | { reason: 'update'; sessionIds: string[] } | { reason: 'delete'; sessionId: string }): Promise<void> {
@@ -485,7 +490,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
485490
requestHandler: undefined,
486491
title: session.label,
487492
activeResponseCallback: undefined,
488-
options: {},
489493
};
490494
} else {
491495
this.newSessions.delete(resource);
@@ -503,15 +507,23 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
503507
this._prDetectionService.detectPullRequest(copilotcliSessionId);
504508

505509
const folderRepo = await this.folderRepositoryManager.getFolderRepository(copilotcliSessionId, undefined, token);
506-
const [history, title] = await Promise.all([
510+
const [history, title, optionGroups] = await Promise.all([
507511
this.getSessionHistory(copilotcliSessionId, folderRepo, token),
508512
this.customSessionTitleService.getCustomSessionTitle(copilotcliSessionId),
513+
this._optionGroupBuilder.buildExistingSessionInputStateGroups(resource, token),
509514
]);
510515

516+
const options: Record<string, string | vscode.ChatSessionProviderOptionItem> = {};
517+
for (const group of optionGroups) {
518+
if (group.selected) {
519+
options[group.id] = { ...group.selected, locked: true };
520+
}
521+
}
522+
511523
return {
512524
title,
513525
history,
514-
activeResponseCallback: undefined,
526+
options,
515527
requestHandler: undefined,
516528
};
517529
}
@@ -536,9 +548,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
536548
}
537549
}
538550

539-
public async updateInputStateAfterFolderSelection(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): Promise<void> {
540-
return this._optionGroupBuilder.updateInputStateAfterFolderSelection(inputState, folderUri);
541-
}
542551
}
543552

544553
export class CopilotCLIChatSessionParticipant extends Disposable {
@@ -721,7 +730,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
721730
};
722731
const branchNamePromise = (isNewSession && request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);
723732

724-
const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { branchName: branchNamePromise }, disposables, token);
733+
const selectedOptions = getSelectedSessionOptions(chatSessionContext.inputState);
734+
const sessionResult = await this.getOrCreateSession(request, chatSessionContext.chatSessionItem.resource, { ...selectedOptions, newBranch: branchNamePromise, stream }, disposables, token);
725735
({ session } = sessionResult);
726736
const { model } = sessionResult;
727737
if (!session || token.isCancellationRequested) {
@@ -759,8 +769,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
759769
}
760770
}
761771

762-
private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { branchName: Promise<string | undefined> }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; trusted: boolean }> {
763-
const result = await this.sessionInitializer.getOrCreateSession(request, chatSessionContext, stream, options, disposables, token);
772+
private async getOrCreateSession(request: vscode.ChatRequest, chatResource: vscode.Uri, options: SessionInitOptions, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; trusted: boolean }> {
773+
const result = await this.sessionInitializer.getOrCreateSession(request, chatResource, options, disposables, token);
764774
const { session, isNewSession, model, trusted } = result;
765775
if (!session || token.isCancellationRequested) {
766776
return { session: undefined, isNewSession, model, trusted };
@@ -816,7 +826,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
816826
return summary ? `${userPrompt}\n${summary}` : userPrompt;
817827
})();
818828

819-
const { workspaceInfo, cancelled } = await this.sessionInitializer.initializeWorkingDirectory(undefined, undefined, undefined, stream, request.toolInvocationToken, token);
829+
const { workspaceInfo, cancelled } = await this.sessionInitializer.initializeWorkingDirectory(undefined, { stream }, request.toolInvocationToken, token);
820830

821831
if (cancelled || token.isCancellationRequested) {
822832
stream.markdown(l10n.t('Copilot CLI delegation cancelled.'));
@@ -1107,10 +1117,7 @@ export function registerCLIChatCommands(
11071117
}
11081118

11091119
// Command handler receives `{ inputState, sessionResource }` context args (new API)
1110-
disposableStore.add(vscode.commands.registerCommand(OPEN_REPOSITORY_COMMAND_ID, async (contextArg?: { inputState: vscode.ChatSessionInputState; sessionResource: vscode.Uri | undefined } | vscode.Uri) => {
1111-
// Support both new API shape and legacy Uri shape for backward compat
1112-
const inputState = contextArg && !isUri(contextArg) ? contextArg.inputState : undefined;
1113-
1120+
disposableStore.add(vscode.commands.registerCommand(OPEN_REPOSITORY_COMMAND_ID, async ({ inputState }: { inputState: vscode.ChatSessionInputState; sessionResource: vscode.Uri | undefined }) => {
11141121
let selectedFolderUri: Uri | undefined = undefined;
11151122
const mruItems = await copilotCLIFolderMruService.getRecentlyUsedFolders(CancellationToken.None);
11161123

@@ -1202,11 +1209,15 @@ export function registerCLIChatCommands(
12021209
return;
12031210
}
12041211

1205-
// // We need to check trust now, as we need to determine whether this is a Git repo or not.
1206-
// // Using the relevant services to check if its a git repo result in checking trust as well, might as well check now instead of complicating code later to handle both trusted and untrusted cases.
1207-
// if (!(await vscode.workspace.isResourceTrusted(selectedFolderUri))) {
1208-
// return;
1209-
// }
1212+
// First check if user trusts the folder.
1213+
const trusted = await vscode.workspace.requestResourceTrust({
1214+
uri: selectedFolderUri,
1215+
message: UNTRUSTED_FOLDER_MESSAGE
1216+
});
1217+
if (!trusted) {
1218+
return;
1219+
}
1220+
12101221

12111222
// Update inputState groups with newly selected folder and reload branches
12121223
if (inputState) {

extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1256,7 +1256,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
12561256
resource: request.sessionResource,
12571257
},
12581258
isUntitled: false,
1259-
initialSessionOptions: undefined
1259+
initialSessionOptions: undefined,
1260+
inputState: undefined as unknown as vscode.ChatSessionInputState
12601261
};
12611262
context = {
12621263
chatSessionContext,

extensions/copilot/src/extension/chatSessions/vscode-node/folderRepositoryManagerImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionS
3333
/**
3434
* Message shown when user needs to trust a folder to continue.
3535
*/
36-
const UNTRUSTED_FOLDER_MESSAGE = l10n.t('The selected folder is not trusted. Please trust the folder to continue with the {0}.', 'Copilot CLI');
36+
export const UNTRUSTED_FOLDER_MESSAGE = l10n.t('The selected folder is not trusted. Please trust the folder to continue with the {0}.', 'Copilot CLI');
3737

3838
// #region FolderRepositoryManager (abstract base)
3939

0 commit comments

Comments
 (0)