Skip to content
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4449,6 +4449,15 @@
"experimental"
]
},
"github.copilot.chat.cli.planMode.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.cli.planMode.enabled%",
"tags": [
"advanced",
"experimental"
]
},
"github.copilot.chat.cli.mcp.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 @@ -400,6 +400,7 @@
"github.copilot.config.claudeAgent.enabled": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic's Claude Agent SDK directly in the editor. Uses your existing Copilot subscription.",
"github.copilot.config.claudeAgent.allowDangerouslySkipPermissions": "Allow bypass permissions mode. Recommended only for sandboxes with no internet access.",
"github.copilot.config.cli.customAgents.enabled": "Enable Custom Agents for Background Agents.",
"github.copilot.config.cli.planMode.enabled": "Enable Plan Mode for Background Agents.",
"github.copilot.config.cli.mcp.enabled": "Enable Model Context Protocol (MCP) server for Background Agents.",
"github.copilot.config.cli.branchSupport.enabled": "Enable branch support for Background Agents.",
"github.copilot.config.cli.isolationOption.enabled": "Enable the isolation mode option for Background Agents. When enabled, users can choose between Worktree and Workspace modes.",
Expand Down
9 changes: 7 additions & 2 deletions src/extension/agents/copilotcli/node/copilotcliSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const copilotCLICommands: readonly CopilotCLICommand[] = ['compact'] as c
* Either a free-form prompt **or** a known command.
*/
export type CopilotCLISessionInput =
| { readonly prompt: string }
| { readonly prompt: string; plan?: boolean }
| { readonly command: CopilotCLICommand };

type PermissionHandler = (
Expand Down Expand Up @@ -404,13 +404,13 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
})));

this._logRequest(prompt, modelId || '', attachments, logStartTime);

if (!token.isCancellationRequested) {
if ('command' in input) {
switch (input.command) {
case 'compact': {
this._stream?.progress(l10n.t('Compacting conversation...'));
await this._sdkSession.initializeAndValidateTools();
this._sdkSession.currentMode = 'interactive';
const result = await this._sdkSession.compactHistory();
if (result.success) {
this._stream?.markdown(l10n.t('Compacted conversation.'));
Expand All @@ -421,6 +421,11 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
}
}
} else {
if (input.plan) {
this._sdkSession.currentMode = 'plan';
} else {
this._sdkSession.currentMode = 'interactive';
}
await this._sdkSession.send({ prompt: input.prompt, attachments, abortController });
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
try {
const telemetryService = new internal.NoopTelemetryService() as unknown as TelemetryService;
return new internal.LocalSessionManager({ telemetryService, flushDebounceMs: undefined, settings: undefined, version: undefined });
}
catch (error) {
} catch (error) {
this.logService.error(`Failed to initialize Copilot CLI Session Manager: ${error}`);
throw error;
}
Expand Down
7 changes: 7 additions & 0 deletions src/extension/chatSessions/vscode-node/chatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEnvService, INativeEnvService } from '../../../platform/env/common/envService';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { IGitService } from '../../../platform/git/common/gitService';
Expand Down Expand Up @@ -43,6 +44,7 @@ import { ChatSessionWorkspaceFolderService } from './chatSessionWorkspaceFolderS
import { ChatSessionWorktreeService } from './chatSessionWorktreeServiceImpl';
import { ClaudeChatSessionContentProvider, ClaudeSessionUri } from './claudeChatSessionContentProvider';
import { CopilotCLIChatSessionContentProvider, CopilotCLIChatSessionItemProvider, CopilotCLIChatSessionParticipant, registerCLIChatCommands } from './copilotCLIChatSessionsContribution';
import { PlanAgentProvider } from './copilotCLIPlanAgentProvider';
import { CopilotCLITerminalIntegration, ICopilotCLITerminalIntegration } from './copilotCLITerminalIntegration';
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
import { ClaudeFolderRepositoryManager, CopilotCLIFolderRepositoryManager } from './folderRepositoryManagerImpl';
Expand Down Expand Up @@ -153,11 +155,16 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib
const nativeEnvService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(INativeEnvService));
const fileSystemService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IFileSystemService));
const copilotModels = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(ICopilotCLIModels));
const configurationService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IConfigurationService));

this._register(copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(ICopilotCLISessionTracker)));
this._register(copilotcliAgentInstaService.createInstance(CopilotCLIContrib));

copilotModels.registerLanguageModelChatProvider(vscode.lm);
if (configurationService.getConfig(ConfigKey.Advanced.CLIPlanModeEnabled)) {
const planProvider = this._register(copilotcliAgentInstaService.createInstance(PlanAgentProvider));
this._register(vscode.chat.registerCustomAgentProvider(planProvider));
}

const copilotcliParticipant = vscode.chat.createChatParticipant(this.copilotcliSessionType, copilotcliChatSessionParticipant.createHandler());
this._register(vscode.chat.registerChatSessionContentProvider(this.copilotcliSessionType, copilotcliChatSessionContentProvider, copilotcliParticipant));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { isUntitledSessionId } from '../common/utils';
import { convertReferenceToVariable } from './copilotCLIPromptReferences';
import { ICopilotCLITerminalIntegration, TerminalOpenLocation } from './copilotCLITerminalIntegration';
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
import { isCopilotCLIPlanAgent } from './copilotCLIPlanAgentProvider';

const AGENTS_OPTION_ID = 'agent';
const REPOSITORY_OPTION_ID = 'repository';
Expand Down Expand Up @@ -999,8 +1000,9 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
await this.commitWorktreeChangesIfNeeded(session.object, token);
} 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);
await session.object.handleRequest(request, { prompt }, attachments, modelId, authInfo, token);
await session.object.handleRequest(request, { prompt, plan }, attachments, modelId, authInfo, token);
await this.commitWorktreeChangesIfNeeded(session.object, token);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Uri, type CancellationToken, type ChatCustomAgentProvider, type ChatRequestModeInstructions, type ChatResource } from 'vscode';
import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { ILogService } from '../../../platform/log/common/logService';
import { Emitter } from '../../../util/vs/base/common/event';
import { Disposable } from '../../../util/vs/base/common/lifecycle';

const planPrompt = `Plan model is configured and defined within Github Copilot CLI`;

export function isCopilotCLIPlanAgent(mode: ChatRequestModeInstructions) {
return mode.name.toLowerCase() === 'plan' && mode.content.trim().includes(planPrompt.trim());
}

export class PlanAgentProvider extends Disposable implements ChatCustomAgentProvider {
private static readonly CACHE_DIR = 'github.copilotcli';
private static readonly AGENT_FILENAME = `Plan${AGENT_FILE_EXTENSION}`;

private readonly _onDidChangeCustomAgents = this._register(new Emitter<void>());
readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event;

constructor(
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
@IFileSystemService private readonly fileSystemService: IFileSystemService,
@ILogService private readonly logService: ILogService,
) {
super();
}

async provideCustomAgents(
_context: unknown,
_token: CancellationToken
): Promise<ChatResource[]> {
// Generate .agent.md content
const content = `---
name: Plan
description: Github Copilot CLI Plan agent
target: github-copilot
---
${planPrompt}
For more details on Plan mode, see https://github.blog/changelog/2026-01-21-github-copilot-cli-plan-before-you-build-steer-as-you-go/#plan-mode`;

// Write to cache file and return URI
const fileUri = await this.writeCacheFile(content);
return [{ uri: fileUri }];
}

private async writeCacheFile(content: string): Promise<Uri> {
const cacheDir = Uri.joinPath(
this.extensionContext.globalStorageUri,
PlanAgentProvider.CACHE_DIR
);

// Ensure cache directory exists
try {
await this.fileSystemService.stat(cacheDir);
} catch {
await this.fileSystemService.createDirectory(cacheDir);
}

const fileUri = Uri.joinPath(cacheDir, PlanAgentProvider.AGENT_FILENAME);
await this.fileSystemService.writeFile(fileUri, new TextEncoder().encode(content));
this.logService.trace(`[PlanAgentProvider] Wrote agent file: ${fileUri.toString()}`);
return fileUri;
}
}
1 change: 1 addition & 0 deletions src/platform/configuration/common/configurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ export namespace ConfigKey {
export const UseResponsesApiTruncation = defineAndMigrateSetting<boolean | undefined>('chat.advanced.useResponsesApiTruncation', 'chat.useResponsesApiTruncation', false);
export const OmitBaseAgentInstructions = defineAndMigrateSetting<boolean>('chat.advanced.omitBaseAgentInstructions', 'chat.omitBaseAgentInstructions', false);
export const CLICustomAgentsEnabled = defineAndMigrateSetting<boolean | undefined>('chat.advanced.cli.customAgents.enabled', 'chat.cli.customAgents.enabled', true);
export const CLIPlanModeEnabled = defineAndMigrateSetting<boolean | undefined>('chat.advanced.cli.planMode.enabled', 'chat.cli.planMode.enabled', false);
export const CLIMCPServerEnabled = defineAndMigrateSetting<boolean | undefined>('chat.advanced.cli.mcp.enabled', 'chat.cli.mcp.enabled', false);
export const CLIBranchSupport = defineSetting<boolean>('chat.cli.branchSupport.enabled', ConfigType.Simple, false);
export const CLIIsolationOption = defineSetting<boolean>('chat.cli.isolationOption.enabled', ConfigType.Simple, false);
Expand Down