diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 9e9ea69fed2..18f54a0db69 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -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", diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 7c510b494ef..b75afd28cba 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -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", diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 244e9575b1f..d3165427635 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -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 { + public async createCopilotCLITerminal(location: TerminalOpenLocation = 'editor', name?: string): Promise { // 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 { @@ -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; diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLITerminalIntegration.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLITerminalIntegration.ts index 9339011707a..6e15bf32f85 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLITerminalIntegration.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLITerminalIntegration.ts @@ -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; + openTerminal(name: string, cliArgs?: string[], cwd?: string, location?: TerminalOpenLocation): Promise; } 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 { +async function getCommonTerminalOptions(name: string, authenticationService: IAuthenticationService, location: TerminalOpenLocation = 'editor'): Promise { 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 = {