Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLIChatSessionInitializer.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { SweCustomAgent } from '@github/copilot/sdk';
7
import * as l10n from '@vscode/l10n';
8
import * as vscode from 'vscode';
9
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { IPromptsService, ParsedPromptFile } from '../../../../platform/promptFiles/common/promptsService';
12
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
13
import { createServiceIdentifier } from '../../../../util/common/services';
14
import { DisposableStore, IReference } from '../../../../util/vs/base/common/lifecycle';
15
import { URI } from '../../../../util/vs/base/common/uri';
16
import { ChatVariablesCollection, extractDebugTargetSessionIds, isPromptFile } from '../../../prompt/common/chatVariablesCollection';
17
import { FolderRepositoryInfo, IFolderRepositoryManager, IsolationMode } from '../../common/folderRepositoryManager';
18
import { emptyWorkspaceInfo, getWorkingDirectory, isIsolationEnabled, IWorkspaceInfo } from '../../common/workspaceInfo';
19
import { SessionIdForCLI } from '../../copilotcli/common/utils';
20
import { COPILOT_CLI_REASONING_EFFORT_PROPERTY, ICopilotCLIAgents, ICopilotCLIModels } from '../../copilotcli/node/copilotCli';
21
import { ICopilotCLISession } from '../../copilotcli/node/copilotcliSession';
22
import { ICopilotCLISessionService } from '../../copilotcli/node/copilotcliSessionService';
23
import { buildMcpServerMappings, McpServerMappings } from '../../copilotcli/node/mcpHandler';
24
25
function isReasoningEffortFeatureEnabled(configurationService: IConfigurationService): boolean {
26
return configurationService.getConfig(ConfigKey.Advanced.CLIThinkingEffortEnabled);
27
}
28
29
export interface SessionInitOptions {
30
isolation?: IsolationMode;
31
branch?: string;
32
folder?: vscode.Uri;
33
newBranch?: Promise<string | undefined>;
34
stream: vscode.ChatResponseStream;
35
}
36
37
export interface ICopilotCLIChatSessionInitializer {
38
readonly _serviceBrand: undefined;
39
40
/**
41
* Get or create a session for a chat request with a chat session context.
42
* Handles working directory initialization, model/agent resolution,
43
* session creation, worktree properties, workspace folder tracking,
44
* stream attachment, permission level, and request metadata recording.
45
*/
46
getOrCreateSession(
47
request: vscode.ChatRequest,
48
chatResource: vscode.Uri,
49
options: SessionInitOptions,
50
disposables: DisposableStore,
51
token: vscode.CancellationToken
52
): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }>;
53
54
/**
55
* Initialize a working directory, optionally based on a chat session context.
56
* Used for both normal requests and delegation flows.
57
*/
58
initializeWorkingDirectory(
59
chatResource: vscode.Uri | undefined,
60
options: SessionInitOptions,
61
toolInvocationToken: vscode.ChatParticipantToolToken,
62
token: vscode.CancellationToken
63
): Promise<{ workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean }>;
64
65
/**
66
* Create a new session for delegation and handle post-creation bookkeeping
67
* including request metadata recording.
68
*/
69
createDelegatedSession(
70
request: vscode.ChatRequest,
71
workspace: IWorkspaceInfo,
72
options: { mcpServerMappings: McpServerMappings },
73
token: vscode.CancellationToken
74
): Promise<IReference<ICopilotCLISession>>;
75
}
76
77
export const ICopilotCLIChatSessionInitializer = createServiceIdentifier<ICopilotCLIChatSessionInitializer>('ICopilotCLIChatSessionInitializer');
78
79
export class CopilotCLIChatSessionInitializer implements ICopilotCLIChatSessionInitializer {
80
declare readonly _serviceBrand: undefined;
81
private readonly delegatedSessionContext = new Map<string, { model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined }>();
82
83
constructor(
84
@ICopilotCLISessionService private readonly sessionService: ICopilotCLISessionService,
85
@IFolderRepositoryManager private readonly folderRepositoryManager: IFolderRepositoryManager,
86
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
87
@ICopilotCLIModels private readonly copilotCLIModels: ICopilotCLIModels,
88
@ICopilotCLIAgents private readonly copilotCLIAgents: ICopilotCLIAgents,
89
@IPromptsService private readonly promptsService: IPromptsService,
90
@ILogService private readonly logService: ILogService,
91
@IConfigurationService private readonly configurationService: IConfigurationService,
92
) { }
93
94
async getOrCreateSession(
95
request: vscode.ChatRequest,
96
chatResource: vscode.Uri,
97
options: SessionInitOptions,
98
disposables: DisposableStore,
99
token: vscode.CancellationToken
100
): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }> {
101
const sessionId = SessionIdForCLI.parse(chatResource);
102
const isNewSession = this.sessionService.isNewSessionId(sessionId);
103
const { stream } = options;
104
const delegatedSessionContext = this.delegatedSessionContext.get(sessionId);
105
this.delegatedSessionContext.delete(sessionId);
106
const [{ workspaceInfo, cancelled, trusted }, model, agent] = await Promise.all([
107
this.initializeWorkingDirectory(chatResource, options, request.toolInvocationToken, token),
108
delegatedSessionContext?.model ? Promise.resolve(delegatedSessionContext.model) : this.resolveModel(request, token),
109
delegatedSessionContext?.agent ? Promise.resolve(delegatedSessionContext.agent) : this.resolveAgent(request, token),
110
]);
111
const workingDirectory = getWorkingDirectory(workspaceInfo);
112
const worktreeProperties = workspaceInfo.worktreeProperties;
113
if (cancelled || token.isCancellationRequested) {
114
return { session: undefined, isNewSession, model, agent, trusted };
115
}
116
117
const debugTargetSessionIds = extractDebugTargetSessionIds(request.references);
118
const mcpServerMappings = buildMcpServerMappings(request.tools);
119
const session = isNewSession ?
120
await this.sessionService.createSession({ sessionId, model: model?.model, reasoningEffort: model?.reasoningEffort, workspace: workspaceInfo, agent, debugTargetSessionIds, mcpServerMappings }, token) :
121
await this.sessionService.getSession({ sessionId, model: model?.model, reasoningEffort: model?.reasoningEffort, workspace: workspaceInfo, agent, debugTargetSessionIds, mcpServerMappings }, token);
122
123
if (!session) {
124
stream.warning(l10n.t('Chat session not found.'));
125
return { session: undefined, isNewSession, model, agent, trusted };
126
}
127
this.logService.info(`Using Copilot CLI session: ${session.object.sessionId} (isNewSession: ${isNewSession}, isolationEnabled: ${isIsolationEnabled(workspaceInfo)}, workingDirectory: ${workingDirectory}, worktreePath: ${worktreeProperties?.worktreePath})`);
128
129
disposables.add(session);
130
disposables.add(session.object.attachStream(stream));
131
session.object.setPermissionLevel(request.permissionLevel);
132
133
return { session, isNewSession, model, agent, trusted };
134
}
135
136
async initializeWorkingDirectory(
137
chatResource: vscode.Uri | undefined,
138
options: SessionInitOptions,
139
toolInvocationToken: vscode.ChatParticipantToolToken,
140
token: vscode.CancellationToken
141
): Promise<{ workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean }> {
142
let folderInfo: FolderRepositoryInfo;
143
const { stream } = options;
144
let folder: undefined | vscode.Uri = options?.folder;
145
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
146
if (workspaceFolders.length === 1 && !folder) {
147
folder = workspaceFolders[0];
148
}
149
if (chatResource) {
150
const sessionId = SessionIdForCLI.parse(chatResource);
151
const isNewSession = this.sessionService.isNewSessionId(sessionId);
152
153
if (isNewSession) {
154
const isolation = options?.isolation ?? IsolationMode.Workspace;
155
const branch = options?.branch;
156
157
// Use FolderRepositoryManager to initialize folder/repository with worktree creation
158
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(sessionId, { stream, toolInvocationToken, branch, isolation, folder, newBranch: options?.newBranch }, token);
159
} else {
160
// Existing session - use getFolderRepository for resolution with trust check
161
folderInfo = await this.folderRepositoryManager.getFolderRepository(sessionId, { promptForTrust: true, stream }, token);
162
}
163
} else {
164
// No chat session context (e.g., delegation) - initialize with active repository
165
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: options?.isolation, folder, newBranch: options?.newBranch }, token);
166
}
167
168
if (folderInfo.trusted === false || folderInfo.cancelled) {
169
return { workspaceInfo: emptyWorkspaceInfo(), cancelled: true, trusted: folderInfo.trusted !== false };
170
}
171
172
const workspaceInfo = Object.assign({}, folderInfo);
173
return { workspaceInfo, cancelled: false, trusted: true };
174
}
175
176
async createDelegatedSession(
177
request: vscode.ChatRequest,
178
workspace: IWorkspaceInfo,
179
options: { mcpServerMappings: McpServerMappings },
180
token: vscode.CancellationToken
181
): Promise<IReference<ICopilotCLISession>> {
182
const [model, agent] = await Promise.all([
183
this.resolveModel(request, token),
184
this.resolveAgent(request, token),
185
]);
186
187
const session = await this.sessionService.createSession({ workspace, agent, model: model?.model, reasoningEffort: model?.reasoningEffort, mcpServerMappings: options.mcpServerMappings }, token);
188
this.delegatedSessionContext.set(session.object.sessionId, { model, agent });
189
return session;
190
}
191
192
/**
193
* Resolve the model ID to use for a request.
194
*/
195
async resolveModel(request: vscode.ChatRequest | undefined, token: vscode.CancellationToken): Promise<{ model: string; reasoningEffort?: string } | undefined> {
196
const promptFile = request ? await this.getPromptInfoFromRequest(request, token) : undefined;
197
const model = promptFile?.header?.model ? await this.getModelFromPromptFile(promptFile.header.model) : undefined;
198
if (token.isCancellationRequested) {
199
return undefined;
200
}
201
if (model) {
202
return { model };
203
}
204
// Get model from request.
205
const preferredModelInRequest = request?.model?.id ? await this.copilotCLIModels.resolveModel(request.model.id) : undefined;
206
if (preferredModelInRequest) {
207
const reasoningEffort = isReasoningEffortFeatureEnabled(this.configurationService) ? request?.modelConfiguration?.[COPILOT_CLI_REASONING_EFFORT_PROPERTY] : undefined;
208
return {
209
model: preferredModelInRequest,
210
reasoningEffort: typeof reasoningEffort === 'string' && reasoningEffort ? reasoningEffort : undefined
211
};
212
}
213
const defaultModel = await this.copilotCLIModels.getDefaultModel();
214
if (!defaultModel) {
215
return undefined;
216
}
217
return { model: defaultModel };
218
}
219
220
/**
221
* Resolve the agent to use for a request.
222
*/
223
async resolveAgent(request: vscode.ChatRequest | undefined, token: vscode.CancellationToken): Promise<SweCustomAgent | undefined> {
224
if (request?.modeInstructions2) {
225
const customAgent = request.modeInstructions2.uri ? await this.copilotCLIAgents.resolveAgent(request.modeInstructions2.uri.toString()) : await this.copilotCLIAgents.resolveAgent(request.modeInstructions2.name);
226
if (customAgent) {
227
const tools = (request.modeInstructions2.toolReferences || []).map(t => t.name);
228
if (tools.length > 0) {
229
customAgent.tools = tools;
230
}
231
return customAgent;
232
}
233
}
234
return undefined;
235
}
236
237
private async getPromptInfoFromRequest(request: vscode.ChatRequest, token: vscode.CancellationToken): Promise<ParsedPromptFile | undefined> {
238
const promptFile = new ChatVariablesCollection(request.references).find(isPromptFile);
239
if (!promptFile || !URI.isUri(promptFile.reference.value)) {
240
return undefined;
241
}
242
try {
243
return await this.promptsService.parseFile(promptFile.reference.value, token);
244
} catch (ex) {
245
this.logService.error(`Failed to parse the prompt file: ${promptFile.reference.value.toString()}`, ex);
246
return undefined;
247
}
248
}
249
250
private async getModelFromPromptFile(models: readonly string[]): Promise<string | undefined> {
251
for (const model of models) {
252
let modelId = await this.copilotCLIModels.resolveModel(model);
253
if (modelId) {
254
return modelId;
255
}
256
// Sometimes the models can contain ` (Copilot)` suffix, try stripping that and resolving again.
257
if (!model.includes('(')) {
258
continue;
259
}
260
modelId = await this.copilotCLIModels.resolveModel(model.substring(0, model.indexOf('(')).trim());
261
if (modelId) {
262
return modelId;
263
}
264
}
265
return undefined;
266
}
267
}
268
269