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