diff --git a/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/test/claudeCodeSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/test/claudeCodeSessionService.spec.ts index 9d517637364..f1dfe36107a 100644 --- a/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/test/claudeCodeSessionService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/test/claudeCodeSessionService.spec.ts @@ -33,8 +33,8 @@ class MockFolderRepositoryManager implements IFolderRepositoryManager { setUntitledSessionFolder(): void { } getUntitledSessionFolder(): undefined { return undefined; } deleteUntitledSessionFolder(): void { } - async getFolderRepository(): Promise { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } - async initializeFolderRepository(): Promise { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } + async getFolderRepository(): Promise<{ folder: undefined; repository: undefined; worktree: undefined; worktreeProperties: undefined; trusted: undefined }> { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } + async initializeFolderRepository(): Promise<{ folder: undefined; repository: undefined; worktree: undefined; worktreeProperties: undefined; trusted: undefined }> { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } async getRepositoryInfo(): Promise { return { repository: undefined, headBranchName: undefined }; } async getFolderMRU(): Promise { return this._mruEntries; } async deleteMRUEntry(): Promise { } diff --git a/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeProjectFolders.spec.ts b/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeProjectFolders.spec.ts index da4d56813c3..dc496e9d25f 100644 --- a/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeProjectFolders.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeProjectFolders.spec.ts @@ -22,8 +22,8 @@ class MockFolderRepositoryManager implements IFolderRepositoryManager { setUntitledSessionFolder(): void { } getUntitledSessionFolder(): undefined { return undefined; } deleteUntitledSessionFolder(): void { } - async getFolderRepository(): Promise { return undefined; } - async initializeFolderRepository(): Promise { return undefined; } + async getFolderRepository(): Promise<{ folder: undefined; repository: undefined; worktree: undefined; worktreeProperties: undefined; trusted: undefined }> { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } + async initializeFolderRepository(): Promise<{ folder: undefined; repository: undefined; worktree: undefined; worktreeProperties: undefined; trusted: undefined }> { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } async getRepositoryInfo(): Promise { return undefined; } async getFolderMRU(): Promise { return this._mruEntries; } async deleteMRUEntry(): Promise { } diff --git a/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeSessionTitleService.spec.ts b/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeSessionTitleService.spec.ts index f76a98957c5..2c71ac5d3c8 100644 --- a/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeSessionTitleService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeSessionTitleService.spec.ts @@ -36,8 +36,8 @@ class MockFolderRepositoryManager implements IFolderRepositoryManager { setUntitledSessionFolder(): void { } getUntitledSessionFolder(): undefined { return undefined; } deleteUntitledSessionFolder(): void { } - async getFolderRepository(): Promise { return undefined; } - async initializeFolderRepository(): Promise { return undefined; } + async getFolderRepository(): Promise<{ folder: undefined; repository: undefined; worktree: undefined; worktreeProperties: undefined; trusted: undefined }> { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } + async initializeFolderRepository(): Promise<{ folder: undefined; repository: undefined; worktree: undefined; worktreeProperties: undefined; trusted: undefined }> { return { folder: undefined, repository: undefined, worktree: undefined, worktreeProperties: undefined, trusted: undefined }; } async getRepositoryInfo(): Promise { return undefined; } async getFolderMRU(): Promise { return this._mruEntries; } async deleteMRUEntry(): Promise { } diff --git a/extensions/copilot/src/extension/chatSessions/common/folderRepositoryManager.ts b/extensions/copilot/src/extension/chatSessions/common/folderRepositoryManager.ts index 707cf350e02..79d1de10ec8 100644 --- a/extensions/copilot/src/extension/chatSessions/common/folderRepositoryManager.ts +++ b/extensions/copilot/src/extension/chatSessions/common/folderRepositoryManager.ts @@ -5,7 +5,7 @@ import type * as vscode from 'vscode'; import { createServiceIdentifier } from '../../../util/common/services'; -import { ChatSessionWorktreeProperties } from './chatSessionWorktreeService'; +import { IWorkspaceInfo } from './workspaceInfo'; /** * The isolation mode for a chat session. @@ -27,30 +27,7 @@ export interface InitializeFolderRepositoryOptions { /** * Result of folder/repository resolution for a chat session. */ -export interface FolderRepositoryInfo { - /** - * The folder URI selected for this session. - * This could be a workspace folder or a git repository root. - */ - readonly folder: vscode.Uri | undefined; - - /** - * The git repository root URI if the selected folder contains a git repository. - * `undefined` if the folder is not a git repository. - */ - readonly repository: vscode.Uri | undefined; - - /** - * The worktree path if a worktree was created for this session. - * `undefined` if no worktree exists (e.g., plain folder or worktree creation failed). - */ - readonly worktree: vscode.Uri | undefined; - - /** - * The worktree properties associated with this session. - */ - readonly worktreeProperties: ChatSessionWorktreeProperties | undefined; - +export interface FolderRepositoryInfo extends IWorkspaceInfo { /** * Trust status of the folder/repository. * - `true`: The folder/repository is trusted diff --git a/extensions/copilot/src/extension/chatSessions/common/workspaceInfo.ts b/extensions/copilot/src/extension/chatSessions/common/workspaceInfo.ts new file mode 100644 index 00000000000..97319d433ff --- /dev/null +++ b/extensions/copilot/src/extension/chatSessions/common/workspaceInfo.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { ChatSessionWorktreeProperties } from './chatSessionWorktreeService'; + +export interface IWorkspaceInfo { + /** + * The folder URI selected for this session. + * This could be a workspace folder or a git repository root. + */ + readonly folder: vscode.Uri | undefined; + + /** + * The git repository root URI if the selected folder contains a git repository. + * `undefined` if the folder is not a git repository. + */ + readonly repository: vscode.Uri | undefined; + + /** + * The worktree path if a worktree was created for this session. + * `undefined` if no worktree exists (e.g., plain folder or worktree creation failed). + */ + readonly worktree: vscode.Uri | undefined; + + /** + * The worktree properties associated with this session. + */ + readonly worktreeProperties: ChatSessionWorktreeProperties | undefined; +} + +export function getWorkingDirectory(workspaceInfo: IWorkspaceInfo): vscode.Uri | undefined { + // Give the folder higher priority over repository, as the user may have selected the folder directly, + // & if we don't create a worktree, then the folder is the working directory. + return workspaceInfo.worktree ?? workspaceInfo.folder ?? workspaceInfo.repository; +} + +export function isIsolationEnabled(workspaceInfo: IWorkspaceInfo): boolean { + return !!workspaceInfo.worktreeProperties; +} + +export function emptyWorkspaceInfo(): IWorkspaceInfo { + return { + folder: undefined, + repository: undefined, + worktree: undefined, + worktreeProperties: undefined, + }; +} diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts index 75c0751ba52..e9b7f4e19a3 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts @@ -22,6 +22,7 @@ import { Emitter, Event } from '../../../../util/vs/base/common/event'; import { Lazy } from '../../../../util/vs/base/common/lazy'; import { Disposable, DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; +import { getWorkingDirectory, IWorkspaceInfo } from '../../common/workspaceInfo'; import { getCopilotLogger } from './logger'; import { ensureNodePtyShim } from './nodePtyShim'; import { ensureRipgrepShim } from './ripgrepShim'; @@ -39,17 +40,15 @@ const COPILOT_CLI_SESSION_AGENTS_MEMENTO_KEY = 'github.copilot.cli.sessionAgents export const COPILOT_CLI_DEFAULT_AGENT_ID = '___vscode_default___'; export class CopilotCLISessionOptions { - public readonly isolationEnabled: boolean; - public readonly workingDirectory?: Uri; + public readonly workspaceInfo: IWorkspaceInfo; private readonly model?: string; private readonly agent?: SweCustomAgent; private readonly customAgents?: SweCustomAgent[]; private readonly mcpServers?: SessionOptions['mcpServers']; private readonly copilotUrl?: string; private readonly skillLocations?: Uri[]; - constructor(options: { model?: string; isolationEnabled?: boolean; workingDirectory?: Uri; mcpServers?: SessionOptions['mcpServers']; agent?: SweCustomAgent; customAgents?: SweCustomAgent[]; copilotUrl?: string; skillLocations?: Uri[] }, private readonly logService: ILogService) { - this.isolationEnabled = !!options.isolationEnabled; - this.workingDirectory = options.workingDirectory; + constructor(options: { model?: string; workspaceInfo: IWorkspaceInfo; mcpServers?: SessionOptions['mcpServers']; agent?: SweCustomAgent; customAgents?: SweCustomAgent[]; copilotUrl?: string; skillLocations?: Uri[] }, private readonly logService: ILogService) { + this.workspaceInfo = options.workspaceInfo; this.model = options.model; this.mcpServers = options.mcpServers; this.agent = options.agent; @@ -63,8 +62,9 @@ export class CopilotCLISessionOptions { clientName: 'vscode', }; - if (this.workingDirectory) { - allOptions.workingDirectory = this.workingDirectory.fsPath; + const workingDirectory = getWorkingDirectory(this.workspaceInfo); + if (workingDirectory) { + allOptions.workingDirectory = workingDirectory.fsPath; } if (this.model) { allOptions.model = this.model as unknown as SessionOptions['model']; diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliPromptResolver.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliPromptResolver.ts index df348e83deb..7121fc6919e 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliPromptResolver.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliPromptResolver.ts @@ -17,6 +17,7 @@ import { relativePath } from '../../../../util/vs/base/common/resources'; import { URI } from '../../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { ChatReferenceBinaryData, ChatReferenceDiagnostic, FileType, Location } from '../../../../vscodeTypes'; +import { getWorkingDirectory, IWorkspaceInfo, isIsolationEnabled } from '../../common/workspaceInfo'; import { ChatVariablesCollection, isPromptInstruction, PromptVariable } from '../../../prompt/common/chatVariablesCollection'; import { generateUserPrompt } from '../../../prompts/node/agent/copilotCLIPrompt'; import { ICopilotCLIImageSupport, isImageMimeType } from './copilotCLIImageSupport'; @@ -35,13 +36,15 @@ export class CopilotCLIPromptResolver { * Generates the final prompt for the Copilot CLI agent, resolving variables and preparing attachments. * @param prompt Provide a prompt to override the request prompt */ - public async resolvePrompt(request: vscode.ChatRequest, prompt: string | undefined, additionalReferences: vscode.ChatPromptReference[], isIsolationEnabled: boolean, workingDirectory: vscode.Uri | undefined, token: vscode.CancellationToken): Promise<{ prompt: string; attachments: Attachment[]; references: vscode.ChatPromptReference[] }> { + public async resolvePrompt(request: vscode.ChatRequest, prompt: string | undefined, additionalReferences: vscode.ChatPromptReference[], workspaceInfo: IWorkspaceInfo, token: vscode.CancellationToken): Promise<{ prompt: string; attachments: Attachment[]; references: vscode.ChatPromptReference[] }> { const allReferences = request.references.concat(additionalReferences.filter(ref => !request.references.includes(ref))); + const isolationEnabled = isIsolationEnabled(workspaceInfo); + const workingDirectory = getWorkingDirectory(workspaceInfo); prompt = prompt ?? request.prompt; if (prompt.startsWith('/')) { return { prompt, attachments: [], references: [] }; // likely a slash command, don't modify } - const [variables, attachments] = await this.constructChatVariablesAndAttachments(new ChatVariablesCollection(allReferences), isIsolationEnabled, workingDirectory, token); + const [variables, attachments] = await this.constructChatVariablesAndAttachments(new ChatVariablesCollection(allReferences), isolationEnabled, workingDirectory, token); if (token.isCancellationRequested) { return { prompt, attachments: [], references: [] }; } diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts index 5849311951b..6a125732600 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts @@ -25,6 +25,7 @@ import { IToolsService } from '../../../tools/common/toolsService'; import { ExternalEditTracker } from '../../common/externalEditTracker'; import { buildChatHistoryFromEvents, getAffectedUrisForEditTool, isCopilotCliEditToolCall, isCopilotCLIToolThatCouldRequirePermissions, processToolExecutionComplete, processToolExecutionStart, ToolCall, UnknownToolCall, updateTodoList } from '../common/copilotCLITools'; import { IChatDelegationSummaryService } from '../common/delegationSummaryService'; +import { IWorkspaceInfo, getWorkingDirectory, isIsolationEnabled } from '../../common/workspaceInfo'; import { getCopilotCLISessionStateDir } from './cliHelpers'; import { CopilotCLISessionOptions, ICopilotCLISDK } from './copilotCli'; import { ICopilotCLIImageSupport } from './copilotCLIImageSupport'; @@ -71,10 +72,7 @@ export interface ICopilotCLISession extends IDisposable { readonly onDidChangeStatus: vscode.Event; readonly permissionRequested?: PermissionRequest; readonly onPermissionRequested: vscode.Event; - readonly options: { - readonly isolationEnabled: boolean; - readonly workingDirectory?: Uri; - }; + readonly workspace: IWorkspaceInfo; readonly pendingPrompt: string | undefined; attachPermissionHandler(handler: PermissionHandler): IDisposable; attachStream(stream: vscode.ChatResponseStream): IDisposable; @@ -129,11 +127,8 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes public get sdkSession() { return this._sdkSession; } - public get options() { - return { - isolationEnabled: this._options.isolationEnabled, - workingDirectory: this._options.workingDirectory, - }; + public get workspace() { + return this._options.workspaceInfo; } private _lastUsedModel: string | undefined; private _pendingPrompt: string | undefined; @@ -441,7 +436,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes }); } } else { - const responsePart = processToolExecutionStart(event, pendingToolInvocations, this.options.workingDirectory); + const responsePart = processToolExecutionStart(event, pendingToolInvocations, getWorkingDirectory(this._options.workspaceInfo)); if (responsePart instanceof ChatResponseThinkingProgressPart) { flushPendingInvocationMessages(); this._stream?.push(responsePart); @@ -481,7 +476,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes } // Just complete the tool invocation - the part was already pushed with partial updates enabled - const [responsePart,] = processToolExecutionComplete(event, pendingToolInvocations, this.logService, this.options.workingDirectory) ?? []; + const [responsePart,] = processToolExecutionComplete(event, pendingToolInvocations, this.logService, getWorkingDirectory(this._options.workspaceInfo)) ?? []; if (responsePart) { flushPendingInvocationMessages(); if (responsePart instanceof ChatToolInvocationPart) { @@ -675,7 +670,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes return this.copilotCLISDK.getRequestId(sdkRequestId); }; const modelId = await this.getSelectedModelId(); - return buildChatHistoryFromEvents(this.sessionId, modelId, events, getVSCodeRequestId, this._delegationSummaryService, this.logService, this.options.workingDirectory); + return buildChatHistoryFromEvents(this.sessionId, modelId, events, getVSCodeRequestId, this._delegationSummaryService, this.logService, getWorkingDirectory(this._options.workspaceInfo)); } private async requestPermission( @@ -684,7 +679,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes getToolCall: (toolCallId: string) => ToolCall | undefined, token: vscode.CancellationToken ): Promise<{ kind: 'approved' } | { kind: 'denied-interactively-by-user' }> { - const workingDirectory = this.options.workingDirectory; + const workingDirectory = getWorkingDirectory(this._options.workspaceInfo); if (permissionRequest.kind === 'read') { // If user is reading a file in the working directory or workspace, auto-approve // read requests. Outside workspace reads (e.g., /etc/passwd) will still require @@ -730,7 +725,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes let autoApprove = false; // If isolation is enabled, we only auto-approve writes within the working directory. - if (this._options.isolationEnabled && isWorkingDirectoryFile) { + if (isIsolationEnabled(this._options.workspaceInfo) && isWorkingDirectoryFile) { autoApprove = true; } // If its a workspace file, and not editing protected files, we auto-approve. @@ -829,8 +824,8 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes result.push(`~~~`); result.push(`sessionId : ${this.sessionId}`); result.push(`modelId : ${modelId}`); - result.push(`isolation : ${this.options.isolationEnabled ? 'enabled' : 'disabled'}`); - result.push(`working dir : ${this.options.workingDirectory?.fsPath || ''}`); + result.push(`isolation : ${isIsolationEnabled(this.workspace) ? 'enabled' : 'disabled'}`); + result.push(`working dir : ${getWorkingDirectory(this._options.workspaceInfo)?.fsPath || ''}`); result.push(`startTime : ${new Date(startTimeMs).toISOString()}`); result.push(`~~~`); result.push(``); @@ -923,8 +918,8 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes result.push(`sessionId : ${this.sessionId}`); result.push(`status : ${status}`); result.push(`modelId : ${modelId}`); - result.push(`isolation : ${this.options.isolationEnabled ? 'enabled' : 'disabled'}`); - result.push(`working dir : ${this.options.workingDirectory?.fsPath || ''}`); + result.push(`isolation : ${isIsolationEnabled(this.workspace) ? 'enabled' : 'disabled'}`); + result.push(`working dir : ${getWorkingDirectory(this._options.workspaceInfo)?.fsPath || ''}`); result.push(`startTime : ${new Date(startTimeMs).toISOString()}`); result.push(`endTime : ${new Date().toISOString()}`); result.push(`duration : ${Date.now() - startTimeMs}ms`); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts index 781533e7fc8..fb350efb681 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts @@ -26,6 +26,7 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio import { ChatSessionStatus } from '../../../../vscodeTypes'; import { stripReminders } from '../common/copilotCLITools'; import { ICustomSessionTitleService } from '../common/customSessionTitleService'; +import { emptyWorkspaceInfo, IWorkspaceInfo } from '../../common/workspaceInfo'; import { CopilotCLISessionOptions, ICopilotCLIAgents, ICopilotCLISDK } from './copilotCli'; import { CopilotCLISession, ICopilotCLISession } from './copilotcliSession'; import { ICopilotCLIMCPHandler } from './mcpHandler'; @@ -60,8 +61,8 @@ export interface ICopilotCLISessionService { renameSession(sessionId: string, title: string): Promise; // Session wrapper tracking - getSession(sessionId: string, options: { model?: string; workingDirectory?: Uri; isolationEnabled?: boolean; readonly: boolean; agent?: SweCustomAgent }, token: CancellationToken): Promise | undefined>; - createSession(options: { model?: string; workingDirectory?: Uri; isolationEnabled?: boolean; agent?: SweCustomAgent }, token: CancellationToken): Promise>; + getSession(sessionId: string, options: { model?: string; workspaceInfo: IWorkspaceInfo; readonly: boolean; agent?: SweCustomAgent }, token: CancellationToken): Promise | undefined>; + createSession(options: { model?: string; workspaceInfo: IWorkspaceInfo; agent?: SweCustomAgent }, token: CancellationToken): Promise>; } export const ICopilotCLISessionService = createServiceIdentifier('ICopilotCLISessionService'); @@ -202,7 +203,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS } try { // Get the full session to access chat messages - const session = await this.getSession(metadata.sessionId, { readonly: true }, token); + const session = await this.getSession(metadata.sessionId, { readonly: true, workspaceInfo: emptyWorkspaceInfo() }, token); const firstUserMessage = session?.object ? session.object.sdkSession.getEvents().find((msg: SessionEvent) => msg.type === 'user.message')?.data.content : undefined; session?.dispose(); @@ -260,10 +261,10 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS } } - public async createSession({ model, workingDirectory, isolationEnabled, agent }: { model?: string; workingDirectory?: Uri; isolationEnabled?: boolean; agent?: SweCustomAgent }, token: CancellationToken): Promise { + public async createSession({ model, workspaceInfo, agent }: { model?: string; workspaceInfo: IWorkspaceInfo; agent?: SweCustomAgent }, token: CancellationToken): Promise { const mcpServers = await this.mcpHandler.loadMcpConfig(); const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined; - const options = await this.createSessionsOptions({ model, workingDirectory, isolationEnabled, mcpServers, agent, copilotUrl }); + const options = await this.createSessionsOptions({ model, workspaceInfo, mcpServers, agent, copilotUrl }); const sessionManager = await raceCancellationError(this.getSessionManager(), token); const sdkSession = await sessionManager.createSession(options.toSessionOptions()); if (copilotUrl) { @@ -284,7 +285,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return this.createCopilotSession(sdkSession, options, sessionManager); } - protected async createSessionsOptions(options: { model?: string; isolationEnabled?: boolean; workingDirectory?: Uri; mcpServers?: SessionOptions['mcpServers']; agent: SweCustomAgent | undefined; copilotUrl?: string }, readonly?: boolean): Promise { + protected async createSessionsOptions(options: { model?: string; workspaceInfo: IWorkspaceInfo; mcpServers?: SessionOptions['mcpServers']; agent: SweCustomAgent | undefined; copilotUrl?: string }, readonly?: boolean): Promise { const [customAgents, skillLocations] = await Promise.all([ this.agents.getAgents(), readonly ? Promise.resolve([]) : this.copilotCLISkills.getSkillsLocations(), @@ -292,7 +293,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS return new CopilotCLISessionOptions({ ...options, customAgents, skillLocations }, this.logService); } - public async getSession(sessionId: string, { model, workingDirectory, isolationEnabled, readonly, agent }: { model?: string; workingDirectory?: Uri; isolationEnabled?: boolean; readonly: boolean; agent?: SweCustomAgent }, token: CancellationToken): Promise { + public async getSession(sessionId: string, { model, workspaceInfo, readonly, agent }: { model?: string; workspaceInfo: IWorkspaceInfo; readonly: boolean; agent?: SweCustomAgent }, token: CancellationToken): Promise { // https://github.com/microsoft/vscode/issues/276573 const lock = this.sessionMutexForGetSession.get(sessionId) ?? new Mutex(); this.sessionMutexForGetSession.set(sessionId, lock); @@ -324,7 +325,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS this.mcpHandler.loadMcpConfig(), ]); const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined; - const options = await this.createSessionsOptions({ model, workingDirectory, agent, isolationEnabled, mcpServers, copilotUrl }, readonly); + const options = await this.createSessionsOptions({ model, agent, workspaceInfo, mcpServers, copilotUrl }, readonly); const sdkSession = await sessionManager.getSession({ ...options.toSessionOptions(), sessionId }, !readonly); if (!sdkSession) { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts index 8616d1a05ab..9d5a98a91a1 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts @@ -25,6 +25,7 @@ import { IInstantiationService } from '../../../../../util/vs/platform/instantia import { createExtensionUnitTestingServices } from '../../../../test/node/services'; import { FakeToolsService } from '../../common/copilotCLITools'; import { IChatDelegationSummaryService } from '../../common/delegationSummaryService'; +import { IWorkspaceInfo } from '../../../common/workspaceInfo'; import { COPILOT_CLI_DEFAULT_AGENT_ID, ICopilotCLIAgents, ICopilotCLISDK } from '../copilotCli'; import { ICopilotCLIImageSupport } from '../copilotCLIImageSupport'; import { CopilotCLISession, ICopilotCLISession } from '../copilotcliSession'; @@ -123,6 +124,21 @@ export class NullCopilotCLIMCPHandler implements ICopilotCLIMCPHandler { } } +function workspaceInfoFor(workingDirectory: Uri | undefined): IWorkspaceInfo { + return { + folder: workingDirectory, + repository: undefined, + worktree: undefined, + worktreeProperties: undefined, + }; +} + +function sessionOptionsFor(workingDirectory?: Uri) { + return { + workspaceInfo: workspaceInfoFor(workingDirectory), + }; +} + describe('CopilotCLISessionService', () => { const disposables = new DisposableStore(); let logService: ILogService; @@ -191,18 +207,18 @@ describe('CopilotCLISessionService', () => { describe('CopilotCLISessionService.createSession', () => { it('get session will return the same session created using createSession', async () => { - const session = await service.createSession({ model: 'gpt-test', workingDirectory: URI.file('/tmp') }, CancellationToken.None); + const session = await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None); - const existingSession = await service.getSession(session.object.sessionId, { readonly: false }, CancellationToken.None); + const existingSession = await service.getSession(session.object.sessionId, { readonly: false, ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None); expect(existingSession).toBe(session); }); it('get session will return new once previous session is disposed', async () => { - const session = await service.createSession({ model: 'gpt-test', workingDirectory: URI.file('/tmp') }, CancellationToken.None); + const session = await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None); session.dispose(); await new Promise(resolve => setTimeout(resolve, 0)); // allow dispose async cleanup to run - const existingSession = await service.getSession(session.object.sessionId, { readonly: false }, CancellationToken.None); + const existingSession = await service.getSession(session.object.sessionId, { readonly: false, ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None); expect(existingSession?.object).toBeDefined(); expect(existingSession?.object).not.toBe(session); @@ -211,7 +227,7 @@ describe('CopilotCLISessionService', () => { it('passes clientName: vscode to session manager', async () => { const createSessionSpy = vi.spyOn(manager, 'createSession'); - await service.createSession({ model: 'gpt-test', workingDirectory: URI.file('/tmp') }, CancellationToken.None); + await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None); expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({ clientName: 'vscode' @@ -233,7 +249,7 @@ describe('CopilotCLISessionService', () => { const promises: Promise | undefined>[] = []; for (let i = 0; i < 10; i++) { - promises.push(service.getSession(targetId, { readonly: false }, CancellationToken.None)); + promises.push(service.getSession(targetId, { readonly: false, ...sessionOptionsFor() }, CancellationToken.None)); } const results = await Promise.all(promises); // All results refer to same instance @@ -269,8 +285,8 @@ describe('CopilotCLISessionService', () => { return originalGetSession(opts, writable); }) as unknown as typeof manager.getSession; - const slowPromise = service.getSession(slowId, { readonly: false }, CancellationToken.None).then(() => 'slow'); - const fastPromise = service.getSession(fastId, { readonly: false }, CancellationToken.None).then(() => 'fast'); + const slowPromise = service.getSession(slowId, { readonly: false, ...sessionOptionsFor() }, CancellationToken.None).then(() => 'slow'); + const fastPromise = service.getSession(fastId, { readonly: false, ...sessionOptionsFor() }, CancellationToken.None).then(() => 'fast'); const firstResolved = await Promise.race([slowPromise, fastPromise]); expect(firstResolved).toBe('fast'); }); @@ -281,7 +297,7 @@ describe('CopilotCLISessionService', () => { // Acquire 5 times sequentially const sessions: IReference[] = []; for (let i = 0; i < 5; i++) { - sessions.push((await service.getSession(id, { readonly: false }, CancellationToken.None))!); + sessions.push((await service.getSession(id, { readonly: false, ...sessionOptionsFor() }, CancellationToken.None))!); } const base = sessions[0]; for (const s of sessions) { @@ -301,7 +317,7 @@ describe('CopilotCLISessionService', () => { describe('CopilotCLISessionService.getSession missing', () => { it('returns undefined when underlying manager has no session', async () => { - const session = await service.getSession('does-not-exist', { readonly: true }, CancellationToken.None); + const session = await service.getSession('does-not-exist', { readonly: true, ...sessionOptionsFor() }, CancellationToken.None); disposables.add(session!); expect(session).toBeUndefined(); }); @@ -309,7 +325,7 @@ describe('CopilotCLISessionService', () => { describe('CopilotCLISessionService.getAllSessions', () => { it('will not list created sessions', async () => { - const session = await service.createSession({ model: 'gpt-test', workingDirectory: URI.file('/tmp') }, CancellationToken.None); + const session = await service.createSession({ model: 'gpt-test', ...sessionOptionsFor(URI.file('/tmp')) }, CancellationToken.None); disposables.add(session); const s1 = new MockCliSdkSession('s1', new Date(0)); @@ -327,7 +343,7 @@ describe('CopilotCLISessionService', () => { describe('CopilotCLISessionService.deleteSession', () => { it('disposes active wrapper, removes from manager and fires change event', async () => { - const session = await service.createSession({}, CancellationToken.None); + const session = await service.createSession({ ...sessionOptionsFor() }, CancellationToken.None); const id = session!.object.sessionId; let fired = false; disposables.add(session); @@ -337,7 +353,7 @@ describe('CopilotCLISessionService', () => { expect(manager.sessions.has(id)).toBe(false); expect(fired).toBe(true); - expect(await service.getSession(id, { readonly: false }, CancellationToken.None)).toBeUndefined(); + expect(await service.getSession(id, { readonly: false, ...sessionOptionsFor() }, CancellationToken.None)).toBeUndefined(); }); }); @@ -451,7 +467,7 @@ describe('CopilotCLISessionService', () => { describe('CopilotCLISessionService.auto disposal timeout', () => { it.skip('disposes session after completion timeout and aborts underlying sdk session', async () => { vi.useFakeTimers(); - const session = await service.createSession({}, CancellationToken.None); + const session = await service.createSession({ ...sessionOptionsFor() }, CancellationToken.None); vi.advanceTimersByTime(31000); await Promise.resolve(); // allow any pending promises to run diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts index 061cbb1f10f..76aca17e76a 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts @@ -23,6 +23,7 @@ import { MockChatResponseStream } from '../../../../test/node/testHelpers'; import { ExternalEditTracker } from '../../../common/externalEditTracker'; import { FakeToolsService, ToolCall } from '../../common/copilotCLITools'; import { IChatDelegationSummaryService } from '../../common/delegationSummaryService'; +import { IWorkspaceInfo } from '../../../common/workspaceInfo'; import { CopilotCLISessionOptions, ICopilotCLISDK } from '../copilotCli'; import { CopilotCLISession } from '../copilotcliSession'; import { PermissionRequest } from '../permissionHelpers'; @@ -119,6 +120,15 @@ function createWorkspaceService(root: string): IWorkspaceService { }; } +function workspaceInfoFor(workingDirectory: Uri | undefined): IWorkspaceInfo { + return { + folder: workingDirectory, + repository: undefined, + worktree: undefined, + worktreeProperties: undefined, + }; +} + describe('CopilotCLISession', () => { const disposables = new DisposableStore(); @@ -152,7 +162,7 @@ describe('CopilotCLISession', () => { }; sdkSession = new MockSdkSession(); workspaceService = createWorkspaceService('/workspace'); - sessionOptions = new CopilotCLISessionOptions({ workingDirectory: workspaceService.getWorkspaceFolders()![0] }, logger); + sessionOptions = new CopilotCLISessionOptions({ workspaceInfo: workspaceInfoFor(workspaceService.getWorkspaceFolders()![0]) }, logger); instaService = services.seal(); }); @@ -334,7 +344,7 @@ describe('CopilotCLISession', () => { it('auto-approves read permission inside working directory without external handler', async () => { let result: unknown; - sessionOptions = new CopilotCLISessionOptions({ workingDirectory: URI.file('/workingDirectory') }, logger); + sessionOptions = new CopilotCLISessionOptions({ workspaceInfo: workspaceInfoFor(URI.file('/workingDirectory')) }, logger); sdkSession.send = async ({ prompt }: any) => { sdkSession.emit('assistant.turn_start', {}); sdkSession.emit('assistant.message', { content: `Echo: ${prompt}` }); diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 459765d95b7..b4b4b0afa99 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -31,8 +31,9 @@ import { IInstantiationService } from '../../../util/vs/platform/instantiation/c import { ChatVariablesCollection, isPromptFile } from '../../prompt/common/chatVariablesCollection'; import { IToolsService } from '../../tools/common/toolsService'; import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService'; -import { ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../common/chatSessionWorktreeService'; -import { FolderRepositoryMRUEntry, IFolderRepositoryManager, IsolationMode } from '../common/folderRepositoryManager'; +import { IChatSessionWorktreeService } from '../common/chatSessionWorktreeService'; +import { FolderRepositoryMRUEntry, IFolderRepositoryManager, IsolationMode, FolderRepositoryInfo } from '../common/folderRepositoryManager'; +import { emptyWorkspaceInfo, getWorkingDirectory, isIsolationEnabled, IWorkspaceInfo } from '../common/workspaceInfo'; import { isUntitledSessionId } from '../common/utils'; import { ToolCall } from '../copilotcli/common/copilotCLITools'; import { IChatDelegationSummaryService } from '../copilotcli/common/delegationSummaryService'; @@ -410,13 +411,11 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements const copilotcliSessionId = SessionIdForCLI.parse(resource); this._currentSessionId = copilotcliSessionId; const folderRepo = await this.folderRepositoryManager.getFolderRepository(copilotcliSessionId, undefined, token); - const workingDirectory = folderRepo.worktree ?? folderRepo.repository ?? folderRepo.folder; - const isolationEnabled = folderRepo.worktree ? true : false; // If theres' a worktree, that means isolation was enabled. const [sessionAgent, defaultAgent, existingSession] = await Promise.all([ this.copilotCLIAgents.getSessionAgent(copilotcliSessionId), this.copilotCLIAgents.getDefaultAgent(), - isUntitledSessionId(copilotcliSessionId) ? Promise.resolve(undefined) : this.sessionService.getSession(copilotcliSessionId, { workingDirectory, isolationEnabled, readonly: true }, token), + isUntitledSessionId(copilotcliSessionId) ? Promise.resolve(undefined) : this.sessionService.getSession(copilotcliSessionId, { workspaceInfo: folderRepo, readonly: true }, token), ]); const repositories = this.isUntitledWorkspace() ? folderMRUToChatProviderOptions(await this.folderRepositoryManager.getFolderMRU()) : this.getRepositoryOptionItems(); @@ -1063,7 +1062,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { } else { // Construct the full prompt with references to be sent to CLI. const plan = request.modeInstructions2 ? isCopilotCLIPlanAgent(request.modeInstructions2) : false; - const { prompt, attachments } = await this.promptResolver.resolvePrompt(request, undefined, [], session.object.options.isolationEnabled, session.object.options.workingDirectory, token); + const { prompt, attachments } = await this.promptResolver.resolvePrompt(request, undefined, [], session.object.workspace, token); await session.object.handleRequest(request, { prompt, plan }, attachments, modelId, authInfo, token); await this.commitWorktreeChangesIfNeeded(session.object, token); } @@ -1153,15 +1152,16 @@ export class CopilotCLIChatSessionParticipant extends Disposable { private async commitWorktreeChangesIfNeeded(session: ICopilotCLISession, token: vscode.CancellationToken): Promise { if (session.status === vscode.ChatSessionStatus.Completed && !token.isCancellationRequested) { - if (session.options.isolationEnabled) { + const workingDirectory = getWorkingDirectory(session.workspace); + if (isIsolationEnabled(session.workspace)) { // When isolation is enabled and we are using a git worktree, so we commit // all the changes in the worktree directory when the session is completed await this.copilotCLIWorktreeManagerService.handleRequestCompleted(session.sessionId); - } else if (session.options.workingDirectory) { + } else if (workingDirectory) { // When isolation is not enabled, we are operating in the workspace directly, // so we stage all the changes in the workspace directory when the session is // completed - await this.workspaceFolderService.handleRequestCompleted(session.options.workingDirectory); + await this.workspaceFolderService.handleRequestCompleted(workingDirectory); } } } @@ -1211,29 +1211,32 @@ export class CopilotCLIChatSessionParticipant extends Disposable { const id = existingSessionId ?? SessionIdForCLI.parse(resource); const isNewSession = chatSessionContext.isUntitled && !existingSessionId; - const { isolationEnabled, workingDirectory, worktreeProperties, cancelled, trusted } = await this.getOrInitializeWorkingDirectory(chatSessionContext, stream, request.toolInvocationToken, token); + const { workspaceInfo, cancelled, trusted } = await this.getOrInitializeWorkingDirectory(chatSessionContext, stream, request.toolInvocationToken, token); + const workingDirectory = getWorkingDirectory(workspaceInfo); + const worktreeProperties = workspaceInfo.worktreeProperties; if (cancelled || token.isCancellationRequested) { return { session: undefined, trusted }; } const session = isNewSession ? - await this.sessionService.createSession({ model, workingDirectory, isolationEnabled, agent }, token) : - await this.sessionService.getSession(id, { model, workingDirectory, isolationEnabled, readonly: false, agent }, token); + await this.sessionService.createSession({ model, workspaceInfo, agent }, token) : + await this.sessionService.getSession(id, { model, workspaceInfo, readonly: false, agent }, token); this.sessionItemProvider.notifySessionsChange(); if (!session) { stream.warning(l10n.t('Chat session not found.')); return { session: undefined, trusted }; } - this.logService.info(`Using Copilot CLI session: ${session.object.sessionId} (isNewSession: ${isNewSession}, isolationEnabled: ${isolationEnabled}, workingDirectory: ${workingDirectory}, worktreePath: ${worktreeProperties?.worktreePath})`); + this.logService.info(`Using Copilot CLI session: ${session.object.sessionId} (isNewSession: ${isNewSession}, isolationEnabled: ${isIsolationEnabled(workspaceInfo)}, workingDirectory: ${workingDirectory}, worktreePath: ${worktreeProperties?.worktreePath})`); if (isNewSession) { this.untitledSessionIdMapping.set(id, session.object.sessionId); if (worktreeProperties) { void this.copilotCLIWorktreeManagerService.setWorktreeProperties(session.object.sessionId, worktreeProperties); } } - if (session.object.options.workingDirectory && !session.object.options.isolationEnabled) { - void this.workspaceFolderService.trackSessionWorkspaceFolder(session.object.sessionId, session.object.options.workingDirectory.fsPath); + const sessionWorkingDirectory = getWorkingDirectory(session.object.workspace); + if (sessionWorkingDirectory && !isIsolationEnabled(session.object.workspace)) { + void this.workspaceFolderService.trackSessionWorkspaceFolder(session.object.sessionId, sessionWorkingDirectory.fsPath); } disposables.add(session.object.attachStream(stream)); disposables.add(session.object.attachPermissionHandler(async (permissionRequest: PermissionRequest, toolCall: ToolCall | undefined, token: vscode.CancellationToken) => requestPermission(this.instantiationService, permissionRequest, toolCall, workingDirectory, this.toolsService, request.toolInvocationToken, token))); @@ -1261,7 +1264,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { // Check for uncommitted changes const worktreeProperties = await this.copilotCLIWorktreeManagerService.getWorktreeProperties(session.sessionId); - const repositoryPath = worktreeProperties?.repositoryPath ? Uri.file(worktreeProperties.repositoryPath) : session.options.workingDirectory; + const repositoryPath = worktreeProperties?.repositoryPath ? Uri.file(worktreeProperties.repositoryPath) : getWorkingDirectory(session.workspace); const repository = repositoryPath ? await this.gitService.getRepository(repositoryPath) : undefined; const hasChanges = (repository?.changes?.indexChanges && repository.changes.indexChanges.length > 0); @@ -1280,15 +1283,11 @@ export class CopilotCLIChatSessionParticipant extends Disposable { toolInvocationToken: vscode.ChatParticipantToolToken, token: vscode.CancellationToken ): Promise<{ - isolationEnabled: boolean; - workingDirectory: Uri | undefined; - worktreeProperties: ChatSessionWorktreeProperties | undefined; + workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean; }> { - let workingDirectory: Uri | undefined; - let worktreeProperties: ChatSessionWorktreeProperties | undefined; - + let folderInfo: FolderRepositoryInfo; if (chatSessionContext) { const existingSessionId = this.untitledSessionIdMapping.get(SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource)); const id = existingSessionId ?? SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource); @@ -1298,39 +1297,22 @@ export class CopilotCLIChatSessionParticipant extends Disposable { // Use FolderRepositoryManager to initialize folder/repository with worktree creation const branch = _sessionBranch.get(id); const isolation = (_sessionIsolation.get(id) as IsolationMode | undefined) ?? undefined; - const folderInfo = await this.folderRepositoryManager.initializeFolderRepository(id, { stream, toolInvocationToken, branch: branch ?? undefined, isolation }, token); - - if (folderInfo.trusted === false || folderInfo.cancelled) { - return { isolationEnabled: false, workingDirectory: undefined, worktreeProperties: undefined, cancelled: true, trusted: folderInfo.trusted !== false }; - } - - workingDirectory = folderInfo.worktree ?? folderInfo.folder; - worktreeProperties = folderInfo.worktreeProperties; + folderInfo = await this.folderRepositoryManager.initializeFolderRepository(id, { stream, toolInvocationToken, branch: branch ?? undefined, isolation }, token); } else { // Existing session - use getFolderRepository for resolution with trust check - const folderInfo = await this.folderRepositoryManager.getFolderRepository(id, { promptForTrust: true, stream }, token); - - if (folderInfo.trusted === false) { - return { isolationEnabled: false, workingDirectory: undefined, worktreeProperties: undefined, cancelled: true, trusted: false }; - } - - workingDirectory = folderInfo.worktree ?? folderInfo.folder; - worktreeProperties = folderInfo.worktree ? await this.copilotCLIWorktreeManagerService.getWorktreeProperties(id) : undefined; + folderInfo = await this.folderRepositoryManager.getFolderRepository(id, { promptForTrust: true, stream }, token); } } else { // No chat session context (e.g., delegation) - initialize with active repository - const folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: undefined }, token); - - if (folderInfo.trusted === false || folderInfo.cancelled) { - return { isolationEnabled: false, workingDirectory: undefined, worktreeProperties: undefined, cancelled: true, trusted: folderInfo.trusted !== false }; - } - - workingDirectory = folderInfo.worktree ?? folderInfo.folder; - worktreeProperties = folderInfo.worktreeProperties; + folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: undefined }, token); } - const isolationEnabled = !!worktreeProperties; - return { isolationEnabled, workingDirectory, worktreeProperties, cancelled: false, trusted: true }; + if (folderInfo.trusted === false || folderInfo.cancelled) { + return { workspaceInfo: emptyWorkspaceInfo(), cancelled: true, trusted: folderInfo.trusted !== false }; + } + + const workspaceInfo = Object.assign({}, folderInfo); + return { workspaceInfo, cancelled: false, trusted: true }; } private async handleDelegationFromAnotherChat( @@ -1355,7 +1337,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { return summary ? `${userPrompt}\n${summary}` : userPrompt; })(); - const [{ isolationEnabled, workingDirectory, worktreeProperties, cancelled }, model, agent] = await Promise.all([ + const [{ workspaceInfo, cancelled }, model, agent] = await Promise.all([ this.getOrInitializeWorkingDirectory(undefined, stream, request.toolInvocationToken, token), this.getModelId(request, token), // prefer model in request, as we're delegating from another session here. this.getAgent(undefined, undefined, token) @@ -1365,10 +1347,11 @@ export class CopilotCLIChatSessionParticipant extends Disposable { stream.markdown(l10n.t('Background Agent delegation cancelled.')); return {}; } + const workingDirectory = getWorkingDirectory(workspaceInfo); + const worktreeProperties = workspaceInfo.worktreeProperties; + const { prompt, attachments, references } = await this.promptResolver.resolvePrompt(request, await requestPromptPromise, (otherReferences || []).concat([]), workspaceInfo, token); - const { prompt, attachments, references } = await this.promptResolver.resolvePrompt(request, await requestPromptPromise, (otherReferences || []).concat([]), isolationEnabled, workingDirectory, token); - - const session = await this.sessionService.createSession({ workingDirectory, isolationEnabled, agent, model }, token); + const session = await this.sessionService.createSession({ workspaceInfo, agent, model }, token); void this.copilotCLIAgents.trackSessionAgent(session.object.sessionId, agent?.name); if (summary) { const summaryRef = await this.chatDelegationSummaryService.trackSummaryUsage(session.object.sessionId, summary); @@ -1380,8 +1363,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable { if (worktreeProperties) { void this.copilotCLIWorktreeManagerService.setWorktreeProperties(session.object.sessionId, worktreeProperties); } - if (session.object.options.workingDirectory && !session.object.options.isolationEnabled) { - void this.workspaceFolderService.trackSessionWorkspaceFolder(session.object.sessionId, session.object.options.workingDirectory.fsPath); + if (workingDirectory && !isIsolationEnabled(workspaceInfo)) { + void this.workspaceFolderService.trackSessionWorkspaceFolder(session.object.sessionId, workingDirectory.fsPath); } try { diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index 981670848c0..f68b9181249 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -28,6 +28,7 @@ import { URI } from '../../../../util/vs/base/common/uri'; import { IInstantiationService, ServicesAccessor } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../vscodeTypes'; import { IChatDelegationSummaryService } from '../../copilotcli/common/delegationSummaryService'; +import { getWorkingDirectory, IWorkspaceInfo } from '../../common/workspaceInfo'; import { type CopilotCLIModelInfo, type ICopilotCLIModels, type ICopilotCLISDK } from '../../copilotcli/node/copilotCli'; import { CopilotCLIPromptResolver } from '../../copilotcli/node/copilotcliPromptResolver'; import { CopilotCLISession, CopilotCLISessionInput } from '../../copilotcli/node/copilotcliSession'; @@ -245,7 +246,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { const accessor = services.createTestingAccessor(); disposables.add(accessor); promptResolver = new class extends mock() { - override resolvePrompt = vi.fn(async (request: vscode.ChatRequest, prompt: string | undefined) => { + override resolvePrompt = vi.fn(async (request: vscode.ChatRequest, prompt: string | undefined, _additionalReferences: vscode.ChatPromptReference[], _workspaceInfo: IWorkspaceInfo, _token: vscode.CancellationToken) => { return { prompt: prompt ?? request.prompt, attachments: [], references: [] }; }); }(); @@ -397,12 +398,12 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { await participant.createHandler()(request, context, stream, token); expect(cliSessions.length).toBe(1); - expect(cliSessions[0].options.isolationEnabled).toBe(true); - expect(cliSessions[0].options.workingDirectory?.fsPath).toBe(`${sep}worktree`); + expect(cliSessions[0].workspace.worktreeProperties).toBeDefined(); + expect(getWorkingDirectory(cliSessions[0].workspace)?.fsPath).toBe(`${sep}worktree`); expect(mcpHandler.loadMcpConfig).toHaveBeenCalled(); // Prompt resolver should receive the effective workingDirectory. expect(promptResolver.resolvePrompt).toHaveBeenCalled(); - expect((promptResolver.resolvePrompt as unknown as ReturnType).mock.calls[0][4]?.fsPath).toBe(`${sep}worktree`); + expect(getWorkingDirectory((promptResolver.resolvePrompt as unknown as ReturnType).mock.calls[0][3])?.fsPath).toBe(`${sep}worktree`); }); it('falls back to workspace workingDirectory when isolation is enabled but worktree creation fails', async () => { @@ -417,12 +418,12 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { await participant.createHandler()(request, context, stream, token); expect(cliSessions.length).toBe(1); - expect(cliSessions[0].options.isolationEnabled).toBe(false); - expect(cliSessions[0].options.workingDirectory?.fsPath).toBe(`${sep}workspace`); + expect(cliSessions[0].workspace.worktreeProperties).toBeUndefined(); + expect(getWorkingDirectory(cliSessions[0].workspace)?.fsPath).toBe(`${sep}workspace`); expect(mcpHandler.loadMcpConfig).toHaveBeenCalled(); // Prompt resolver should receive the effective workingDirectory. expect(promptResolver.resolvePrompt).toHaveBeenCalled(); - expect((promptResolver.resolvePrompt as unknown as ReturnType).mock.calls[0][4]?.fsPath).toBe(`${sep}workspace`); + expect(getWorkingDirectory((promptResolver.resolvePrompt as unknown as ReturnType).mock.calls[0][3])?.fsPath).toBe(`${sep}workspace`); }); it('reuses existing session (non-untitled) and does not create new one', async () => { diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/parseAttachments.spec.ts b/extensions/copilot/src/extension/prompts/node/agent/test/parseAttachments.spec.ts index 220644227d0..4680b3ab87e 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/parseAttachments.spec.ts +++ b/extensions/copilot/src/extension/prompts/node/agent/test/parseAttachments.spec.ts @@ -25,6 +25,7 @@ import { ChatReferenceDiagnostic } from '../../../../../vscodeTypes'; import { extractChatPromptReferences } from '../../../../chatSessions/copilotcli/common/copilotCLIPrompt'; import { CopilotCLIImageSupport } from '../../../../chatSessions/copilotcli/node/copilotCLIImageSupport'; import { CopilotCLIPromptResolver } from '../../../../chatSessions/copilotcli/node/copilotcliPromptResolver'; +import { emptyWorkspaceInfo, IWorkspaceInfo } from '../../../../chatSessions/common/workspaceInfo'; import { createExtensionUnitTestingServices } from '../../../../test/node/services'; import { TestChatRequest } from '../../../../test/node/testHelpers'; @@ -36,8 +37,7 @@ suite('CopilotCLI Generate & parse prompts', () => { let fileSystem: MockFileSystemService; let workspaceService: TestWorkspaceService; let resolver: CopilotCLIPromptResolver; - const workingDirectory = workspaceType === 'emptyWorkspace' ? undefined : (workspaceType === 'workspace' ? URI.file('/workspace') : URI.file('/worktree')); - const isolationEnabled = workspaceType === 'worktree' ? true : false; + const workspaceInfo = createWorkspaceInfo(workspaceType); beforeEach(() => { const services = createExtensionUnitTestingServices(disposables); const accessor = disposables.add(services.createTestingAccessor()); @@ -60,7 +60,7 @@ suite('CopilotCLI Generate & parse prompts', () => { }); test('just the prompt without anything else', async () => { const req = new TestChatRequest('hello world'); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -70,7 +70,7 @@ suite('CopilotCLI Generate & parse prompts', () => { test('returns original prompt unchanged for slash command', async () => { const req = new TestChatRequest('/help something'); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -80,7 +80,7 @@ suite('CopilotCLI Generate & parse prompts', () => { test('returns overridden prompt instead of using the request prompt', async () => { const req = new TestChatRequest('/help something'); - const resolved = await resolver.resolvePrompt(req, 'What is 1+2', [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, 'What is 1+2', [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -121,7 +121,7 @@ suite('CopilotCLI Generate & parse prompts', () => { value: pyUri } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -148,7 +148,7 @@ suite('CopilotCLI Generate & parse prompts', () => { value: folderUri } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -174,7 +174,7 @@ suite('CopilotCLI Generate & parse prompts', () => { ]]) } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -222,7 +222,7 @@ suite('CopilotCLI Generate & parse prompts', () => { } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -281,7 +281,7 @@ suite('CopilotCLI Generate & parse prompts', () => { } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -329,7 +329,7 @@ suite('CopilotCLI Generate & parse prompts', () => { value: new Location(pyUri, new Range(3, 0, 3, 15)) } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -357,7 +357,7 @@ suite('CopilotCLI Generate & parse prompts', () => { } ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -378,7 +378,7 @@ suite('CopilotCLI Generate & parse prompts', () => { untitledTsFile ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -405,7 +405,7 @@ suite('CopilotCLI Generate & parse prompts', () => { regularFileRef ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -425,7 +425,7 @@ suite('CopilotCLI Generate & parse prompts', () => { promptFile ]); - const resolved = await resolver.resolvePrompt(req, undefined, [], isolationEnabled, workingDirectory, CancellationToken.None); + const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, CancellationToken.None); const result = extractChatPromptReferences(resolved.prompt); expect(resolved.prompt).toMatchSnapshot(); @@ -461,6 +461,33 @@ suite('CopilotCLI Generate & parse prompts', () => { }); }); +function createWorkspaceInfo(workspaceType: 'emptyWorkspace' | 'workspace' | 'worktree'): IWorkspaceInfo { + if (workspaceType === 'workspace') { + return { + ...emptyWorkspaceInfo(), + folder: URI.file('/workspace'), + }; + } + + if (workspaceType === 'worktree') { + return { + ...emptyWorkspaceInfo(), + folder: URI.file('/workspace'), + worktree: URI.file('/worktree'), + worktreeProperties: { + version: 2, + baseCommit: 'HEAD', + branchName: 'worktree-branch', + repositoryPath: '/workspace', + worktreePath: '/worktree', + baseBranchName: 'main', + }, + }; + } + + return emptyWorkspaceInfo(); +} + /** * As we want test to run on all platforms, we need to fix file paths in attachments * to use forward slashes for comparison. diff --git a/extensions/copilot/test/e2e/cli.stest.ts b/extensions/copilot/test/e2e/cli.stest.ts index 3ceae20ad7e..2033d2f63b8 100644 --- a/extensions/copilot/test/e2e/cli.stest.ts +++ b/extensions/copilot/test/e2e/cli.stest.ts @@ -14,6 +14,7 @@ import { OpenAIAdapterFactoryForSTests } from '../../src/extension/agents/node/a import { ILanguageModelServer, ILanguageModelServerConfig, LanguageModelServer } from '../../src/extension/agents/node/langModelServer'; import { ICustomSessionTitleService } from '../../src/extension/chatSessions/copilotcli/common/customSessionTitleService'; import { ChatDelegationSummaryService, IChatDelegationSummaryService } from '../../src/extension/chatSessions/copilotcli/common/delegationSummaryService'; +import { emptyWorkspaceInfo, IWorkspaceInfo } from '../../src/extension/chatSessions/common/workspaceInfo'; import { CopilotCLIAgents, CopilotCLIModels, CopilotCLISDK, CopilotCLISessionOptions, ICopilotCLIAgents, ICopilotCLIModels, ICopilotCLISDK } from '../../src/extension/chatSessions/copilotcli/node/copilotCli'; import { CopilotCLIImageSupport, ICopilotCLIImageSupport } from '../../src/extension/chatSessions/copilotcli/node/copilotCLIImageSupport'; import { CopilotCLIPromptResolver } from '../../src/extension/chatSessions/copilotcli/node/copilotcliPromptResolver'; @@ -76,6 +77,18 @@ function restoreEnvVariablesAfterTests() { } } +function sessionOptionsFor(workingDirectory: Uri | undefined) { + return { + workingDirectory, + workspaceInfo: { + folder: workingDirectory, + repository: undefined, + worktree: undefined, + worktreeProperties: undefined, + } satisfies IWorkspaceInfo + }; +} + async function registerChatServices(testingServiceCollection: TestingServiceCollection) { const ITestSessionOptionsProvider = createServiceIdentifier('ITestSessionOptionsProvider'); class TestSessionOptionsProvider { @@ -116,7 +129,7 @@ async function registerChatServices(testingServiceCollection: TestingServiceColl } class TestCopilotCLISessionOptions extends CopilotCLISessionOptions { - constructor(options: { model?: string; isolationEnabled?: boolean; workingDirectory?: Uri; mcpServers?: SessionOptions['mcpServers'] }, logger: ILogService, private readonly testOptions: Pick) { + constructor(options: { model?: string; workingDirectory?: Uri; workspaceInfo: IWorkspaceInfo; mcpServers?: SessionOptions['mcpServers'] }, logger: ILogService, private readonly testOptions: Pick) { super(options, logger); } override toSessionOptions() { @@ -205,7 +218,7 @@ async function registerChatServices(testingServiceCollection: TestingServiceColl override async monitorSessionFiles() { // Override to do nothing in tests } - protected override async createSessionsOptions(options: { model?: string; isolationEnabled?: boolean; workingDirectory?: Uri; mcpServers?: SessionOptions['mcpServers'] }): Promise { + protected override async createSessionsOptions(options: { model?: string; workingDirectory?: Uri; workspaceInfo: IWorkspaceInfo; mcpServers?: SessionOptions['mcpServers'] }): Promise { const testOptionsProvider = this.instantiationService.invokeFunction((accessor) => accessor.get(ITestSessionOptionsProvider)); const overrideOptions = await testOptionsProvider.getOptions(); const sessionOptions = new TestCopilotCLISessionOptions(options, this.logService, overrideOptions); @@ -422,7 +435,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { testRunner(async ({ sessionService, init, authInfo }, scenariosPath, toolInvocations, stream, disposables) => { const workingDirectory = URI.file(path.join(scenariosPath, 'wkspc1')); await init(workingDirectory); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -448,7 +461,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { let sessionId = ''; // Start session. { - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); sessionId = session.object.sessionId; await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'What is 1+8?' }, [], undefined, authInfo, CancellationToken.None); @@ -460,7 +473,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { const session = await new Promise>((resolve, reject) => { const interval = disposables.add(new IntervalTimer()); interval.cancelAndSet(async () => { - const session = await sessionService.getSession(sessionId, { readonly: false, workingDirectory }, CancellationToken.None); + const session = await sessionService.getSession(sessionId, { readonly: false, ...sessionOptionsFor(workingDirectory) }, CancellationToken.None); if (session) { interval.dispose(); resolve(session); @@ -486,7 +499,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { await init(workingDirectory); const file = URI.joinPath(workingDirectory, 'sample.js'); const prompt = `Explain the contents of the file '${path.basename(file.fsPath)}'. There is no need to check for contents in the directory. This file exists on disc.`; - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -504,7 +517,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { const externalFile = path.join(scenariosPath, 'wkspc2', 'foobar.js'); const prompt = `Explain the contents of the file '${externalFile}'. This file exists on disc but not in the current working directory. There's no need to search the directory, just read this file and explain its contents.`; - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); let permissionRequested = false; @@ -540,7 +553,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -562,7 +575,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -602,7 +615,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -622,7 +635,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -643,7 +656,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -670,7 +683,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { ); let contents = await fs.readFile(file, 'utf-8'); assert.ok(!contents.trim().endsWith('}'), '} is missing'); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -698,7 +711,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { [createDiagnosticReference(tsFile, [tsDiag]), createDiagnosticReference(pyFile, [pyDiag1, pyDiag2])], promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -723,7 +736,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { [], promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); disposables.add(session.object.attachPermissionHandler(async (permission: PermissionRequest) => { @@ -754,7 +767,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -782,7 +795,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -811,7 +824,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -840,7 +853,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => { promptResolver ); - const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None); + const session = await sessionService.createSession(sessionOptionsFor(workingDirectory), CancellationToken.None); disposables.add(session); disposables.add(session.object.attachStream(stream)); @@ -899,5 +912,5 @@ function createDiagnosticReference(file: string, diag: Diagnostic[]): ChatPrompt function resolvePromptWithFileReferences(prompt: string, filesOrReferences: (string | ChatPromptReference)[], promptResolver: CopilotCLIPromptResolver): Promise<{ prompt: string; attachments: any[] }> { - return promptResolver.resolvePrompt(createWithRequestWithFileReference(prompt, filesOrReferences), undefined, [], false, undefined, CancellationToken.None); + return promptResolver.resolvePrompt(createWithRequestWithFileReference(prompt, filesOrReferences), undefined, [], emptyWorkspaceInfo(), CancellationToken.None); }