Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6404,7 +6404,7 @@
"node-gyp": "npm:node-gyp@10.3.1",
"zod": "3.25.76"
},
"vscodeCommit": "eb014b61a9ac4d91acc39984167e2ca84c03b758",
"vscodeCommit": "afba0a4a1fc1e34dae9073d6787b6b541bda23eb",
"__metadata": {
"id": "7ec7d6e6-b89e-4cc5-a59b-d6c4d238246f",
"publisherId": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ import { COPILOT_CLI_REASONING_EFFORT_PROPERTY, ICopilotCLIAgents, ICopilotCLIMo
import { ICopilotCLISession } from '../copilotcli/node/copilotcliSession';
import { ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService';
import { buildMcpServerMappings, McpServerMappings } from '../copilotcli/node/mcpHandler';
import { BRANCH_OPTION_ID, ISOLATION_OPTION_ID, REPOSITORY_OPTION_ID } from './sessionOptionGroupBuilder';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';

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

export interface SessionInitOptions {
isolation?: IsolationMode;
branch?: string;
folder?: vscode.Uri;
newBranch?: Promise<string | undefined>;
stream: vscode.ChatResponseStream;
}

export interface ICopilotCLIChatSessionInitializer {
readonly _serviceBrand: undefined;

Expand All @@ -41,9 +48,8 @@ export interface ICopilotCLIChatSessionInitializer {
*/
getOrCreateSession(
request: vscode.ChatRequest,
chatSessionContext: vscode.ChatSessionContext,
stream: vscode.ChatResponseStream,
options: { branchName: Promise<string | undefined> },
chatResource: vscode.Uri,
options: SessionInitOptions,
disposables: DisposableStore,
token: vscode.CancellationToken
): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }>;
Expand All @@ -53,10 +59,8 @@ export interface ICopilotCLIChatSessionInitializer {
* Used for both normal requests and delegation flows.
*/
initializeWorkingDirectory(
chatSessionContext: vscode.ChatSessionContext | undefined,
isolation: IsolationMode | undefined,
branchName: Promise<string | undefined> | undefined,
stream: vscode.ChatResponseStream,
chatResource: vscode.Uri | undefined,
options: SessionInitOptions,
toolInvocationToken: vscode.ChatParticipantToolToken,
token: vscode.CancellationToken
): Promise<{ workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean }>;
Expand Down Expand Up @@ -94,18 +98,17 @@ export class CopilotCLIChatSessionInitializer implements ICopilotCLIChatSessionI

async getOrCreateSession(
request: vscode.ChatRequest,
chatSessionContext: vscode.ChatSessionContext,
stream: vscode.ChatResponseStream,
options: { branchName: Promise<string | undefined> },
chatResource: vscode.Uri,
options: SessionInitOptions,
disposables: DisposableStore,
token: vscode.CancellationToken
): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }> {
const { resource } = chatSessionContext.chatSessionItem;
const sessionId = SessionIdForCLI.parse(resource);
const sessionId = SessionIdForCLI.parse(chatResource);
const isNewSession = this.sessionService.isNewSessionId(sessionId);
const { stream } = options;

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

async initializeWorkingDirectory(
chatSessionContext: vscode.ChatSessionContext | undefined,
isolation: IsolationMode | undefined,
branchName: Promise<string | undefined> | undefined,
stream: vscode.ChatResponseStream,
chatResource: vscode.Uri | undefined,
options: SessionInitOptions,
toolInvocationToken: vscode.ChatParticipantToolToken,
token: vscode.CancellationToken
): Promise<{ workspaceInfo: IWorkspaceInfo; cancelled: boolean; trusted: boolean }> {
let folderInfo: FolderRepositoryInfo;
let folder: undefined | vscode.Uri = undefined;
const { stream } = options;
let folder: undefined | vscode.Uri = options?.folder;
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
if (workspaceFolders.length === 1) {
if (workspaceFolders.length === 1 && !folder) {
folder = workspaceFolders[0];
}
if (chatSessionContext) {
const sessionId = SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource);
if (chatResource) {
const sessionId = SessionIdForCLI.parse(chatResource);
const isNewSession = this.sessionService.isNewSessionId(sessionId);

if (isNewSession) {
let isolation = IsolationMode.Workspace;
let branch: string | undefined = undefined;
for (const opt of (chatSessionContext.initialSessionOptions || [])) {
const value = typeof opt.value === 'string' ? opt.value : opt.value.id;
if (opt.optionId === REPOSITORY_OPTION_ID && value) {
folder = vscode.Uri.file(value);
} else if (opt.optionId === BRANCH_OPTION_ID && value) {
branch = value;
} else if (opt.optionId === ISOLATION_OPTION_ID && value) {
isolation = value as IsolationMode;
}
}
const isolation = options?.isolation ?? IsolationMode.Workspace;
const branch = options?.branch;

// Use FolderRepositoryManager to initialize folder/repository with worktree creation
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(sessionId, { stream, toolInvocationToken, branch, isolation, folder, newBranch: branchName }, token);
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(sessionId, { stream, toolInvocationToken, branch, isolation, folder, newBranch: options?.newBranch }, token);
} else {
// Existing session - use getFolderRepository for resolution with trust check
folderInfo = await this.folderRepositoryManager.getFolderRepository(sessionId, { promptForTrust: true, stream }, token);
}
} else {
// No chat session context (e.g., delegation) - initialize with active repository
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation, folder }, token);
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: options?.isolation, folder }, token);
}

if (folderInfo.trusted === false || folderInfo.cancelled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ import { builtinSlashSCommands, CopilotCLICommand, copilotCLICommands, ICopilotC
import { ICopilotCLISessionItem, ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService';
import { buildMcpServerMappings } from '../copilotcli/node/mcpHandler';
import { ICopilotCLISessionTracker } from '../copilotcli/vscode-node/copilotCLISessionTracker';
import { ICopilotCLIChatSessionInitializer } from './copilotCLIChatSessionInitializer';
import { ICopilotCLIChatSessionInitializer, SessionInitOptions } from './copilotCLIChatSessionInitializer';
import { convertReferenceToVariable } from './copilotCLIPromptReferences';
import { ICopilotCLITerminalIntegration, TerminalOpenLocation } from './copilotCLITerminalIntegration';
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
import { IPullRequestDetectionService } from './pullRequestDetectionService';
import { ISessionOptionGroupBuilder, OPEN_REPOSITORY_COMMAND_ID, toRepositoryOptionItem, toWorkspaceFolderOptionItem } from './sessionOptionGroupBuilder';
import { getSelectedSessionOptions, ISessionOptionGroupBuilder, OPEN_REPOSITORY_COMMAND_ID, toRepositoryOptionItem, toWorkspaceFolderOptionItem } from './sessionOptionGroupBuilder';
import { ISessionRequestLifecycle } from './sessionRequestLifecycle';
import { UNTRUSTED_FOLDER_MESSAGE } from './folderRepositoryManagerImpl';

/**
* ODO:
Expand Down Expand Up @@ -267,19 +268,22 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements

const newInputStates: WeakRef<vscode.ChatSessionInputState>[] = [];
controller.getChatSessionInputState = async (sessionResource, context, token) => {
const groups = sessionResource ? await this._optionGroupBuilder.buildExistingSessionInputStateGroups(sessionResource, token) : await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.previousInputState);
const state = controller.createChatSessionInputState(groups);
if (!sessionResource) {
const isExistingSession = sessionResource && !this.sessionService.isNewSessionId(SessionIdForCLI.parse(sessionResource));
if (isExistingSession) {
const groups = await this._optionGroupBuilder.buildExistingSessionInputStateGroups(sessionResource, token);
return controller.createChatSessionInputState(groups);
} else {
const groups = await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.previousInputState);
const state = controller.createChatSessionInputState(groups);
// Only wire dynamic updates for new sessions (existing sessions are fully locked).
// Note: don't use the getChatSessionInputState token here — it's a one-shot token
// that may be disposed by the time the user interacts with the dropdowns.
newInputStates.push(new WeakRef(state));

state.onDidChange(() => {
void this._optionGroupBuilder.handleInputStateChange(state);
});
return state;
}
return state;
};

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

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

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

const folderRepo = await this.folderRepositoryManager.getFolderRepository(copilotcliSessionId, undefined, token);
const [history, title] = await Promise.all([
const [history, title, optionGroups] = await Promise.all([
this.getSessionHistory(copilotcliSessionId, folderRepo, token),
this.customSessionTitleService.getCustomSessionTitle(copilotcliSessionId),
this._optionGroupBuilder.buildExistingSessionInputStateGroups(resource, token),
]);

const options: Record<string, string | vscode.ChatSessionProviderOptionItem> = {};
for (const group of optionGroups) {
if (group.selected) {
options[group.id] = { ...group.selected, locked: true };
}
}

return {
title,
history,
activeResponseCallback: undefined,
options,
requestHandler: undefined,
};
}
Expand All @@ -536,9 +548,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
}
}

public async updateInputStateAfterFolderSelection(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): Promise<void> {
return this._optionGroupBuilder.updateInputStateAfterFolderSelection(inputState, folderUri);
}
}

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

const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { branchName: branchNamePromise }, disposables, token);
const selectedOptions = getSelectedSessionOptions(chatSessionContext.inputState);
const sessionResult = await this.getOrCreateSession(request, chatSessionContext.chatSessionItem.resource, { ...selectedOptions, newBranch: branchNamePromise, stream }, disposables, token);
({ session } = sessionResult);
const { model } = sessionResult;
if (!session || token.isCancellationRequested) {
Expand Down Expand Up @@ -759,8 +769,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
}
}

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

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

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

// Command handler receives `{ inputState, sessionResource }` context args (new API)
disposableStore.add(vscode.commands.registerCommand(OPEN_REPOSITORY_COMMAND_ID, async (contextArg?: { inputState: vscode.ChatSessionInputState; sessionResource: vscode.Uri | undefined } | vscode.Uri) => {
// Support both new API shape and legacy Uri shape for backward compat
const inputState = contextArg && !isUri(contextArg) ? contextArg.inputState : undefined;

disposableStore.add(vscode.commands.registerCommand(OPEN_REPOSITORY_COMMAND_ID, async ({ inputState }: { inputState: vscode.ChatSessionInputState; sessionResource: vscode.Uri | undefined }) => {
let selectedFolderUri: Uri | undefined = undefined;
const mruItems = await copilotCLIFolderMruService.getRecentlyUsedFolders(CancellationToken.None);

Expand Down Expand Up @@ -1202,11 +1209,15 @@ export function registerCLIChatCommands(
return;
}

// // We need to check trust now, as we need to determine whether this is a Git repo or not.
// // Using the relevant services to check if its a git repo result in checking trust as well, might as well check now instead of complicating code later to handle both trusted and untrusted cases.
// if (!(await vscode.workspace.isResourceTrusted(selectedFolderUri))) {
// return;
// }
// First check if user trusts the folder.
const trusted = await vscode.workspace.requestResourceTrust({
uri: selectedFolderUri,
message: UNTRUSTED_FOLDER_MESSAGE
});
if (!trusted) {
return;
}


// Update inputState groups with newly selected folder and reload branches
if (inputState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
resource: request.sessionResource,
},
isUntitled: false,
initialSessionOptions: undefined
initialSessionOptions: undefined,
inputState: undefined as unknown as vscode.ChatSessionInputState
};
context = {
chatSessionContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionS
/**
* Message shown when user needs to trust a folder to continue.
*/
const UNTRUSTED_FOLDER_MESSAGE = l10n.t('The selected folder is not trusted. Please trust the folder to continue with the {0}.', 'Copilot CLI');
export const UNTRUSTED_FOLDER_MESSAGE = l10n.t('The selected folder is not trusted. Please trust the folder to continue with the {0}.', 'Copilot CLI');

// #region FolderRepositoryManager (abstract base)

Expand Down
Loading
Loading