Add new CLI session commands and enhance terminal integration (#3596)

This commit is contained in:
Peng Lyu
2026-02-09 16:03:46 -08:00
committed by GitHub
parent d99df825ea
commit fb9fd9db40
4 changed files with 45 additions and 17 deletions
+12
View File
@@ -2292,6 +2292,18 @@
"icon": "$(terminal)",
"category": "Copilot CLI"
},
{
"command": "github.copilot.cli.newSession",
"title": "%github.copilot.command.cli.newSession%",
"icon": "$(terminal)",
"category": "Chat"
},
{
"command": "github.copilot.cli.newSessionToSide",
"title": "%github.copilot.command.cli.newSessionToSide%",
"icon": "$(terminal)",
"category": "Chat"
},
{
"command": "github.copilot.chat.replay",
"title": "Start Chat Replay",
+2
View File
@@ -410,6 +410,8 @@
"github.copilot.command.cli.sessions.openRepository": "Open Repository",
"github.copilot.command.cli.sessions.openWorktreeInNewWindow": "Open Worktree in New Window",
"github.copilot.command.cli.sessions.openWorktreeInTerminal": "Open Worktree in Integrated Terminal",
"github.copilot.command.cli.newSession": "New CLI Session",
"github.copilot.command.cli.newSessionToSide": "New CLI Session to the Side",
"github.copilot.command.chat.copilotCLI.addFileReference": "Add File Reference to Prompt",
"github.copilot.command.chat.copilotCLI.acceptDiff": "Accept Changes",
"github.copilot.command.chat.copilotCLI.rejectDiff": "Reject Changes",
@@ -39,7 +39,7 @@ import { ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../c
import { FolderRepositoryMRUEntry, IFolderRepositoryManager } from '../common/folderRepositoryManager';
import { isUntitledSessionId } from '../common/utils';
import { convertReferenceToVariable } from './copilotCLIPromptReferences';
import { ICopilotCLITerminalIntegration } from './copilotCLITerminalIntegration';
import { ICopilotCLITerminalIntegration, TerminalOpenLocation } from './copilotCLITerminalIntegration';
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
const AGENTS_OPTION_ID = 'agent';
@@ -220,10 +220,10 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc
} satisfies vscode.ChatSessionItem;
}
public async createCopilotCLITerminal(): Promise<void> {
public async createCopilotCLITerminal(location: TerminalOpenLocation = 'editor', name?: string): Promise<void> {
// TODO@rebornix should be set by CLI
const terminalName = process.env.COPILOTCLI_TERMINAL_TITLE || l10n.t('Background Agent');
await this.terminalIntegration.openTerminal(terminalName);
const terminalName = name || process.env.COPILOTCLI_TERMINAL_TITLE || l10n.t('Background Agent');
await this.terminalIntegration.openTerminal(terminalName, [], undefined, location);
}
public async resumeCopilotCLISessionInTerminal(sessionItem: vscode.ChatSessionItem): Promise<void> {
@@ -1320,6 +1320,12 @@ export function registerCLIChatCommands(copilotcliSessionItemProvider: CopilotCL
await copilotcliSessionItemProvider.resumeCopilotCLISessionInTerminal(sessionItem);
}
}));
disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.newSession', async () => {
await copilotcliSessionItemProvider.createCopilotCLITerminal('editor', l10n.t('Copilot CLI'));
}));
disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.newSessionToSide', async () => {
await copilotcliSessionItemProvider.createCopilotCLITerminal('editorBeside', l10n.t('Copilot CLI'));
}));
disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.openWorktreeInNewWindow', async (sessionItem?: vscode.ChatSessionItem) => {
if (!sessionItem?.resource) {
return;
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { promises as fs } from 'fs';
import { Terminal, TerminalOptions, TerminalProfile, ThemeIcon, ViewColumn, window, workspace } from 'vscode';
import { Terminal, TerminalLocation, TerminalOptions, TerminalProfile, ThemeIcon, ViewColumn, window, workspace } from 'vscode';
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { IEnvService } from '../../../platform/env/common/envService';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
@@ -24,9 +24,11 @@ const COPILOT_CLI_SHIM_JS = 'copilotCLIShim.js';
const COPILOT_CLI_COMMAND = 'copilot';
const COPILOT_ICON = new ThemeIcon('copilot');
export type TerminalOpenLocation = 'panel' | 'editor' | 'editorBeside';
export interface ICopilotCLITerminalIntegration extends Disposable {
readonly _serviceBrand: undefined;
openTerminal(name: string, cliArgs?: string[], cwd?: string): Promise<void>;
openTerminal(name: string, cliArgs?: string[], cwd?: string, location?: TerminalOpenLocation): Promise<void>;
}
type IShellInfo = {
@@ -96,7 +98,7 @@ ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(storageLocation, COPIL
if (!shellInfo) {
return;
}
this.sendTerminalOpenTelemetry('new', shellInfo.shell, 'newFromTerminalProfile');
this.sendTerminalOpenTelemetry('new', shellInfo.shell, 'newFromTerminalProfile', 'panel');
return new TerminalProfile({
name: 'GitHub Copilot CLI',
shellPath: shellInfo.shellPath,
@@ -108,7 +110,7 @@ ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(storageLocation, COPIL
}
public async openTerminal(name: string, cliArgs: string[] = [], cwd?: string) {
public async openTerminal(name: string, cliArgs: string[] = [], cwd?: string, location: TerminalOpenLocation = 'editor') {
// Capture session type before mutating cliArgs.
// If cliArgs are provided (e.g. --resume), we are resuming a session; otherwise it's a new session.
const sessionType = cliArgs.length > 0 ? 'resume' : 'new';
@@ -122,7 +124,7 @@ ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(storageLocation, COPIL
this.initialization
]);
const options = await getCommonTerminalOptions(name, this._authenticationService);
const options = await getCommonTerminalOptions(name, this._authenticationService, location);
options.cwd = cwd;
if (shellPathAndArgs) {
options.iconPath = shellPathAndArgs.iconPath ?? options.iconPath;
@@ -134,7 +136,7 @@ ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(storageLocation, COPIL
this._register(terminal);
const command = this.buildCommandForPythonTerminal(shellPathAndArgs?.copilotCommand, cliArgs, shellPathAndArgs);
await this.sendCommandToTerminal(terminal, command, true, shellPathAndArgs);
this.sendTerminalOpenTelemetry(sessionType, shellPathAndArgs.shell, 'pythonTerminal');
this.sendTerminalOpenTelemetry(sessionType, shellPathAndArgs.shell, 'pythonTerminal', location);
return;
}
}
@@ -144,7 +146,7 @@ ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(storageLocation, COPIL
cliArgs.shift(); // Remove --clear as we can't run it without a shell integration
const command = this.buildCommandForTerminal(terminal, COPILOT_CLI_COMMAND, cliArgs);
await this.sendCommandToTerminal(terminal, command, false, shellPathAndArgs);
this.sendTerminalOpenTelemetry(sessionType, 'unknown', 'fallbackTerminal');
this.sendTerminalOpenTelemetry(sessionType, 'unknown', 'fallbackTerminal', location);
return;
}
@@ -155,24 +157,26 @@ ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(storageLocation, COPIL
options.shellArgs = shellPathAndArgs.shellArgs;
const terminal = this._register(this.terminalService.createTerminal(options));
terminal.show();
this.sendTerminalOpenTelemetry(sessionType, shellPathAndArgs.shell, 'shellArgsTerminal');
this.sendTerminalOpenTelemetry(sessionType, shellPathAndArgs.shell, 'shellArgsTerminal', location);
}
}
private sendTerminalOpenTelemetry(sessionType: string, shell: string, terminalCreationMethod: string): void {
private sendTerminalOpenTelemetry(sessionType: string, shell: string, terminalCreationMethod: string, location: TerminalOpenLocation): void {
/* __GDPR__
"copilotcli.terminal.open" : {
"owner": "DonJayamanne",
"comment": "Event sent when a Copilot CLI terminal is opened.",
"sessionType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the terminal is for a new session or resuming an existing one." },
"shell" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The shell type used for the terminal." },
"terminalCreationMethod" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How the terminal was created." }
"terminalCreationMethod" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How the terminal was created." },
"location" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Where the terminal was opened - panel, editor area (active), or editor area (beside)." }
}
*/
this.telemetryService.sendMSFTTelemetryEvent('copilotcli.terminal.open', {
sessionType,
shell,
terminalCreationMethod
terminalCreationMethod,
location
});
}
@@ -329,13 +333,17 @@ function quoteArgsForShell(shellScript: string, args: string[]): string {
return args.length ? `${escapeArg(shellScript)} ${escapedArgs.join(' ')}` : escapeArg(shellScript);
}
async function getCommonTerminalOptions(name: string, authenticationService: IAuthenticationService): Promise<TerminalOptions> {
async function getCommonTerminalOptions(name: string, authenticationService: IAuthenticationService, location: TerminalOpenLocation = 'editor'): Promise<TerminalOptions> {
const options: TerminalOptions = {
name,
iconPath: new ThemeIcon('terminal'),
location: { viewColumn: ViewColumn.Active },
hideFromUser: false
};
if (location === 'panel') {
options.location = TerminalLocation.Panel;
} else {
options.location = { viewColumn: location === 'editorBeside' ? ViewColumn.Beside : ViewColumn.Active };
}
const session = await authenticationService.getGitHubSession('any', { silent: true });
if (session) {
options.env = {