Skip to content
Draft
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
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4573,6 +4573,14 @@
"advanced"
]
},
"github.copilot.chat.cli.thinkingEffort.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "%github.copilot.config.cli.thinkingEffort.enabled%",
"tags": [
"advanced"
]
},
"github.copilot.chat.cli.sessionControllerForSessionsApp.enabled": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@
"github.copilot.config.cli.isolationOption.enabled": "Enable the isolation mode option for Copilot CLI. When enabled, users can choose between Worktree and Workspace modes.",
"github.copilot.config.cli.autoCommit.enabled": "Enable automatic commit for Copilot CLI. When enabled, changes made by Copilot CLI will be automatically committed to the repository at the end of each turn.",
"github.copilot.config.cli.sessionController.enabled": "Enable the new session controller API for Copilot CLI. Requires VS Code reload.",
"github.copilot.config.cli.thinkingEffort.enabled": "Enable thinking effort for Language Models in Copilot CLI.",
"github.copilot.config.cli.sessionControllerForSessionsApp.enabled": "Enable the new session controller API for Sessions App. Requires VS Code reload.",
"github.copilot.config.cli.terminalLinks.enabled": "Enable advanced clickable file links in Copilot CLI terminals. Resolves relative paths against session state directories. Requires VS Code reload.",
"github.copilot.config.backgroundAgent.enabled": "Enable the Copilot CLI. When disabled, the Copilot CLI will not be available in 'Continue In' context menus.",
Expand Down
45 changes: 43 additions & 2 deletions src/extension/chatSessions/copilotcli/node/copilotCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import type { SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
import * as l10n from '@vscode/l10n';
import { promises as fs } from 'fs';
import * as path from 'path';
import type * as vscode from 'vscode';
Expand All @@ -26,6 +27,7 @@ import { getCopilotLogger } from './logger';
import { ensureNodePtyShim } from './nodePtyShim';
import { ensureRipgrepShim } from './ripgrepShim';

export const COPILOT_CLI_REASONING_EFFORT_PROPERTY = 'reasoningEffort';
const COPILOT_CLI_MODEL_MEMENTO_KEY = 'github.copilot.cli.sessionModel';
const COPILOT_CLI_REQUEST_MAP_KEY = 'github.copilot.cli.requestMap';
// Store last used Agent for a Session.
Expand All @@ -44,6 +46,9 @@ export interface CopilotCLIModelInfo {
readonly maxOutputTokens?: number;
readonly maxContextWindowTokens: number;
readonly supportsVision?: boolean;
readonly supportsReasoningEffort?: boolean;
readonly defaultReasoningEffort?: string;
readonly supportedReasoningEfforts?: string[];
}

export interface ICopilotCLIModels {
Expand Down Expand Up @@ -124,6 +129,9 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
maxOutputTokens: model.capabilities.limits.max_output_tokens,
maxContextWindowTokens: model.capabilities.limits.max_context_window_tokens,
supportsVision: model.capabilities.supports.vision,
supportsReasoningEffort: model.capabilities.supports.reasoningEffort,
defaultReasoningEffort: model.defaultReasoningEffort,
supportedReasoningEfforts: model.supportedReasoningEfforts
} satisfies CopilotCLIModelInfo));
} catch (ex) {
this.logService.error(`[CopilotCLISession] Failed to fetch models`, ex);
Expand Down Expand Up @@ -152,7 +160,7 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {

private async _provideLanguageModelChatInfo(): Promise<vscode.LanguageModelChatInformation[]> {
const models = await this.getModels();
return models.map((model, index) => {
const modelsInfo = models.map((model, index) => {
const multiplier = model.multiplier === undefined ? undefined : `${model.multiplier}x`;
return {
id: model.id,
Expand All @@ -164,14 +172,16 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
multiplier,
multiplierNumeric: model.multiplier,
isUserSelectable: true,
configurationSchema: buildConfigurationSchema(model),
capabilities: {
imageInput: model.supportsVision,
toolCalling: true
toolCalling: true,
},
targetChatSessionType: 'copilotcli',
isDefault: index === 0 // SDK guarantees the first item is the default model
};
});
return modelsInfo;
}
}

Expand All @@ -181,6 +191,37 @@ export interface CLIAgentInfo {
/** File URI for prompt-file agents, synthetic `copilotcli:` URI for SDK-only agents. */
readonly sourceUri: URI;
}
function buildConfigurationSchema(modelInfo: CopilotCLIModelInfo): vscode.LanguageModelConfigurationSchema | undefined {
const effortLevels = modelInfo.supportedReasoningEfforts ?? [];
if (effortLevels.length === 0) {
return;
}

const defaultEffort = modelInfo.defaultReasoningEffort;

return {
properties: {
[COPILOT_CLI_REASONING_EFFORT_PROPERTY]: {
type: 'string',
title: l10n.t('Thinking Effort'),
enum: effortLevels,
enumItemLabels: effortLevels.map(level => level.charAt(0).toUpperCase() + level.slice(1)),
enumDescriptions: effortLevels.map(level => {
switch (level) {
case 'none': return l10n.t('No reasoning applied');
case 'low': return l10n.t('Faster responses with less reasoning');
case 'medium': return l10n.t('Balanced reasoning and speed');
case 'high': return l10n.t('Greater reasoning depth but slower');
case 'xhigh': return l10n.t('Maximum reasoning depth but slower');
default: return level;
}
}),
default: defaultEffort,
group: 'navigation',
}
}
};
}

export interface ICopilotCLIAgents {
readonly _serviceBrand: undefined;
Expand Down
31 changes: 18 additions & 13 deletions src/extension/chatSessions/copilotcli/node/copilotcliSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export interface ICopilotCLISession extends IDisposable {
request: { id: string; toolInvocationToken: ChatParticipantToolToken; sessionResource?: vscode.Uri },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
model: { model: string; reasoningEffort?: string } | undefined,
authInfo: NonNullable<SessionOptions['authInfo']>,
token: vscode.CancellationToken
): Promise<void>;
Expand Down Expand Up @@ -213,7 +213,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
request: { id: string; toolInvocationToken: ChatParticipantToolToken; sessionResource?: vscode.Uri },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
model: { model: string; reasoningEffort?: string } | undefined,
authInfo: NonNullable<SessionOptions['authInfo']>,
token: vscode.CancellationToken
): Promise<void> {
Expand All @@ -229,12 +229,12 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
const previousRequestSnapshot = this.previousRequest;

const handled = this._requestLogger.captureInvocation(capturingToken, async () => {
await this.updateModel(modelId, authInfo, token);
await this.updateModel(model?.model, model?.reasoningEffort, authInfo, token);

if (isAlreadyBusyWithAnotherRequest) {
return this._handleRequestSteering(input, attachments, modelId, previousRequestSnapshot, token);
return this._handleRequestSteering(input, attachments, model, previousRequestSnapshot, token);
} else {
return this._handleRequestImpl(request, input, attachments, modelId, token);
return this._handleRequestImpl(request, input, attachments, model, token);
}
});

Expand All @@ -261,7 +261,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
private async _handleRequestSteering(
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
model: { model: string; reasoningEffort?: string } | undefined,
previousRequestPromise: Promise<unknown>,
token: vscode.CancellationToken,
): Promise<void> {
Expand All @@ -281,9 +281,9 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
// previous request to finish, so this promise settles only once all
// in-flight work is done.
await Promise.all([previousRequestPromise, this.sendRequestInternal(input, attachments, true, logStartTime)]);
this._logConversation(prompt, '', modelId || '', attachments, logStartTime, 'Completed');
this._logConversation(prompt, '', model?.model || '', attachments, logStartTime, 'Completed');
} catch (error) {
this._logConversation(prompt, '', modelId || '', attachments, logStartTime, 'Failed', error instanceof Error ? error.message : String(error));
this._logConversation(prompt, '', model?.model || '', attachments, logStartTime, 'Failed', error instanceof Error ? error.message : String(error));
throw error;
} finally {
disposables.dispose();
Expand All @@ -294,9 +294,10 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
model: { model: string; reasoningEffort?: string } | undefined,
token: vscode.CancellationToken
): Promise<void> {
const modelId = model?.model;
return this._otelService.startActiveSpan(
'invoke_agent copilotcli',
{
Expand Down Expand Up @@ -766,7 +767,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
}
}

private async updateModel(modelId: string | undefined, authInfo: NonNullable<SessionOptions['authInfo']>, token: CancellationToken): Promise<void> {
private async updateModel(modelId: string | undefined, reasoningEffort: string | undefined, authInfo: NonNullable<SessionOptions['authInfo']>, token: CancellationToken): Promise<void> {
// Where possible try to avoid an extra call to getSelectedModel by using cached value.
let currentModel: string | undefined = undefined;
if (modelId) {
Expand All @@ -782,9 +783,13 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
if (authInfo) {
this._sdkSession.setAuthInfo(authInfo);
}
if (modelId && modelId !== currentModel) {
this._lastUsedModel = modelId;
await raceCancellation(this._sdkSession.setSelectedModel(modelId), token);
if (modelId) {
if (modelId !== currentModel) {
this._lastUsedModel = modelId;
await raceCancellation(this._sdkSession.setSelectedModel(modelId, reasoningEffort), token);
} else if (reasoningEffort && this._sdkSession.getReasoningEffort() !== reasoningEffort) {
await raceCancellation(this._sdkSession.setSelectedModel(modelId, reasoningEffort), token);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface ICopilotCLISessionItem {
export type ExtendedChatRequest = ChatRequest & { prompt: string };
export type ISessionOptions = {
model?: string;
reasoningEffort?: string;
workspace: IWorkspaceInfo;
agent?: SweCustomAgent;
debugTargetSessionIds?: readonly string[];
Expand Down Expand Up @@ -526,34 +527,33 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
}
}

public async createSession({ model, workspace, agent, sessionId, debugTargetSessionIds, mcpServerMappings }: ICreateSessionOptions, token: CancellationToken): Promise<RefCountedSession> {
public async createSession(options: ICreateSessionOptions, token: CancellationToken): Promise<RefCountedSession> {
const { mcpConfig: mcpServers, disposable: mcpGateway } = await this.mcpHandler.loadMcpConfig();
try {
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
const { agentName, sessionOptions } = await this.createSessionsOptions({ model, workspace, mcpServers, agent, copilotUrl, sessionId, debugTargetSessionIds, mcpServerMappings });
const sessionOptions = await this.createSessionsOptions({ ...options, mcpServers });
const sessionManager = await raceCancellationError(this.getSessionManager(), token);
const sdkSession = await sessionManager.createSession({ ...sessionOptions, sessionId });
const sdkSession = await sessionManager.createSession({ ...sessionOptions, sessionId: options.sessionId });
this._newSessionIds.delete(sdkSession.sessionId);
// After the first session creation, the SDK's OTel TracerProvider is
// initialized. Install the bridge processor so SDK-native spans flow
// to the debug panel.
this._installBridgeIfNeeded();

if (copilotUrl) {
if (sessionOptions.copilotUrl) {
sdkSession.setAuthInfo({
type: 'hmac',
hmac: 'empty',
host: 'https://github.com',
copilotUser: {
endpoints: {
api: copilotUrl
api: sessionOptions.copilotUrl
}
}
});
}
this.logService.trace(`[CopilotCLISession] Created new CopilotCLI session ${sdkSession.sessionId}.`);

const session = this.createCopilotSession(sdkSession, workspace, agentName, sessionManager);
const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager);
session.object.add(mcpGateway);
return session;
}
Expand Down Expand Up @@ -638,7 +638,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
return false;
}

protected async createSessionsOptions(options: { model?: string; workspace: IWorkspaceInfo; mcpServers?: SessionOptions['mcpServers']; agent: SweCustomAgent | undefined; copilotUrl?: string; sessionId?: string; debugTargetSessionIds?: readonly string[]; mcpServerMappings?: McpServerMappings }): Promise<{ readonly sessionOptions: Readonly<SessionOptions>; readonly agentName: string | undefined }> {
protected async createSessionsOptions(options: ICreateSessionOptions & { mcpServers?: SessionOptions['mcpServers'] }): Promise<Readonly<SessionOptions>> {
const [agentInfos, skillLocations] = await Promise.all([
this.agents.getAgents(),
this.copilotCLISkills.getSkillsLocations(),
Expand Down Expand Up @@ -680,34 +680,35 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
allOptions.customAgents = customAgents;
}
allOptions.enableStreaming = true;
if (options.copilotUrl) {
allOptions.copilotUrl = options.copilotUrl;
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
if (copilotUrl) {
allOptions.copilotUrl = copilotUrl;
}
if (systemMessage) {
allOptions.systemMessage = systemMessage;
}
allOptions.sessionCapabilities = new Set(['plan-mode', 'memory', 'cli-documentation', 'ask-user', 'interactive-mode', 'system-notifications']);
if (options.reasoningEffort) {
allOptions.reasoningEffort = options.reasoningEffort;
}

return {
sessionOptions: allOptions as Readonly<SessionOptions>,
agentName: options.agent?.name,
};
return allOptions as Readonly<SessionOptions>;
}

public async getSession({ sessionId, model, workspace, agent, debugTargetSessionIds, mcpServerMappings }: IGetSessionOptions, token: CancellationToken): Promise<RefCountedSession | undefined> {
public async getSession(options: IGetSessionOptions, token: CancellationToken): Promise<RefCountedSession | undefined> {
// https://github.com/microsoft/vscode/issues/276573
const lock = this.sessionMutexForGetSession.get(sessionId) ?? new Mutex();
this.sessionMutexForGetSession.set(sessionId, lock);
const lock = this.sessionMutexForGetSession.get(options.sessionId) ?? new Mutex();
this.sessionMutexForGetSession.set(options.sessionId, lock);
const lockDisposable = await lock.acquire(token);
try {
{
const session = this._sessionWrappers.get(sessionId);
const session = this._sessionWrappers.get(options.sessionId);
if (session) {
this.logService.trace(`[CopilotCLISession] Reusing CopilotCLI session ${sessionId}.`);
this._partialSessionHistories.delete(sessionId);
this.logService.trace(`[CopilotCLISession] Reusing CopilotCLI session ${options.sessionId}.`);
this._partialSessionHistories.delete(options.sessionId);
session.acquire();
if (agent) {
await session.object.sdkSession.selectCustomAgent(agent.name);
if (options.agent) {
await session.object.sdkSession.selectCustomAgent(options.agent.name);
} else {
session.object.sdkSession.clearCustomAgent();
}
Expand All @@ -720,16 +721,15 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
this.mcpHandler.loadMcpConfig(),
]);
try {
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
const { agentName, sessionOptions } = await this.createSessionsOptions({ model, agent, workspace: workspace, mcpServers, copilotUrl, sessionId, debugTargetSessionIds, mcpServerMappings });
const sessionOptions = await this.createSessionsOptions({ ...options, mcpServers });

const sdkSession = await sessionManager.getSession({ ...sessionOptions, sessionId }, true);
const sdkSession = await sessionManager.getSession({ ...sessionOptions, sessionId: options.sessionId }, true);
if (!sdkSession) {
this.logService.error(`[CopilotCLISession] CopilotCLI failed to get session ${sessionId}.`);
this.logService.error(`[CopilotCLISession] CopilotCLI failed to get session ${options.sessionId}.`);
return undefined;
}

const session = this.createCopilotSession(sdkSession, workspace, agentName, sessionManager);
const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager);
session.object.add(mcpGateway);
return session;
}
Expand Down Expand Up @@ -875,12 +875,11 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
const newSessionId = generateUuid();
this._sessionsBeingCreatedViaFork.add(newSessionId);
try {
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
const [sessionManager, title, { history, events: originalSessionEvents }, { sessionOptions }] = await Promise.all([
const [sessionManager, title, { history, events: originalSessionEvents }, sessionOptions] = await Promise.all([
raceCancellationError(this.getSessionManager(), token),
this.getSessionTitle(sessionId, token),
requestId ? this.getChatHistoryImpl({ sessionId, workspace }, token) : Promise.resolve({ history: [], events: [] }),
this.createSessionsOptions({ workspace, mcpServers: undefined, copilotUrl, agent: undefined, sessionId: newSessionId }),
this.createSessionsOptions({ workspace, mcpServers: undefined, agent: undefined, sessionId: newSessionId }),
copySessionFilesForForking(sessionId, newSessionId, workspace, this._chatSessionMetadataStore, token),
]);

Expand Down
Loading
Loading