Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/vscode-node/slashCommands/terminalCommand.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { execFile } from 'child_process';6import { promisify } from 'util';7import * as vscode from 'vscode';8import { ILogService } from '../../../../../platform/log/common/logService';9import { CapturingToken } from '../../../../../platform/requestLogger/common/capturingToken';10import { ITerminalService } from '../../../../../platform/terminal/common/terminalService';11import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';12import { generateUuid } from '../../../../../util/vs/base/common/uuid';13import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';14import { ClaudeLanguageModelServer } from '../../node/claudeLanguageModelServer';15import { IClaudeSessionStateService } from '../../common/claudeSessionStateService';16import { IClaudeSlashCommandHandler, registerClaudeSlashCommand } from './claudeSlashCommandRegistry';1718const execFileAsync = promisify(execFile);1920/**21* Slash command handler for creating a terminal session with Claude CLI configured22* to use Copilot Chat's endpoints.23*24* This command starts a ClaudeLanguageModelServer instance (if not already running)25* and creates a new terminal with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY environment26* variables set to proxy requests through Copilot Chat's chat endpoints.27*28* ## Usage29* 1. In a Claude Agent chat session, type `/terminal`30* 2. A new terminal will be created with the environment variables configured31* 3. Run `claude` in the terminal to start Claude Code32* 4. Claude Code will use Copilot Chat's endpoints for all LLM requests33*34* ## Requirements35* - Claude CLI (`claude`) must be installed and available in PATH36* - The terminal inherits the environment with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY set37* - The language model server runs on localhost with a random available port38*/39export class TerminalSlashCommand implements IClaudeSlashCommandHandler {40readonly commandName = 'terminal';41readonly description = vscode.l10n.t('Launch Claude Code CLI using your GitHub Copilot subscription');42readonly commandId = 'copilot.claude.terminal';4344private _langModelServer: ClaudeLanguageModelServer | undefined;4546constructor(47@ILogService private readonly logService: ILogService,48@ITerminalService private readonly terminalService: ITerminalService,49@IClaudeSessionStateService private readonly sessionStateService: IClaudeSessionStateService,50@IInstantiationService private readonly instantiationService: IInstantiationService,51) { }5253async handle(54_args: string,55stream: vscode.ChatResponseStream | undefined,56_token: CancellationToken57): Promise<vscode.ChatResult> {58stream?.markdown(vscode.l10n.t('Creating Claude CLI instance...'));5960try {61// Check which CLI is available62const cliCommand = await this._getClaudeCliCommand();63if (!cliCommand) {64const installUrl = 'https://code.claude.com';65const downloadLabel = vscode.l10n.t('Download Claude CLI');66if (stream) {67stream.markdown(vscode.l10n.t('Claude CLI is not installed. Download Claude CLI to get started.'));68stream.button({ command: 'vscode.open', arguments: [vscode.Uri.parse(installUrl)], title: downloadLabel });69} else {70vscode.window.showErrorMessage(71vscode.l10n.t('Claude CLI is not installed.'),72downloadLabel73).then(selection => {74if (selection === downloadLabel) {75vscode.env.openExternal(vscode.Uri.parse(installUrl));76}77});78}79return {};80}8182// Get or create the language model server83const server = await this._getLanguageModelServer();84const config = server.getConfig();8586// Generate a unique session ID for this terminal session87const sessionId = generateUuid();8889// Create terminal with environment variables configured90const terminal = this.terminalService.createTerminal({91name: 'Claude',92message: formatMessageForTerminal(vscode.l10n.t('This instance of Claude CLI is configured to use your GitHub Copilot subscription.'), { loudFormatting: true }),93env: {94ANTHROPIC_BASE_URL: `http://localhost:${config.port}`,95ANTHROPIC_AUTH_TOKEN: `${config.nonce}.${sessionId}`,96// Hide account info banner in CLI since it's redundant with the message above97CLAUDE_CODE_HIDE_ACCOUNT_INFO: '1',98}99});100101// Show the terminal102terminal.show();103104// Send the appropriate command to the terminal with the session ID105terminal.sendText(`${cliCommand} --session-id ${sessionId}`);106107// Set capturing token only after terminal is successfully created to avoid leaking stale session state108this.sessionStateService.setCapturingTokenForSession(109sessionId,110new CapturingToken(`Claude CLI (${sessionId})`, 'claude')111);112113this.logService.info(`[TerminalSlashCommand] Created terminal with Claude CLI configured on port ${config.port}, command: ${cliCommand}, sessionId: ${sessionId}`);114} catch (error) {115const errorMessage = error instanceof Error ? error.message : String(error);116this.logService.error('[TerminalSlashCommand] Error creating terminal:', error);117if (stream) {118stream.markdown(vscode.l10n.t('Error creating terminal: {0}', errorMessage));119} else {120vscode.window.showErrorMessage(vscode.l10n.t('Error creating terminal: {0}', errorMessage));121}122}123124return {};125}126127/**128* Check which Claude CLI command is available.129* Returns 'claude' if available, 'agency claude' if agency is available, or undefined if neither.130* TODO: support some way to specify custom path to CLI in case it's not in PATH131*/132private async _getClaudeCliCommand(): Promise<string | undefined> {133const whichCommand = process.platform === 'win32' ? 'where' : 'which';134135// Check if 'claude' is available136if (await this._isCommandAvailable(whichCommand, 'claude')) {137return 'claude';138}139140// Check if 'agency' is available (fallback)141if (await this._isCommandAvailable(whichCommand, 'agency')) {142return 'agency claude';143}144145return undefined;146}147148/**149* Check if a command is available in PATH150*/151private async _isCommandAvailable(whichCommand: string, command: string): Promise<boolean> {152try {153await execFileAsync(whichCommand, [command]);154return true;155} catch {156return false;157}158}159160private async _getLanguageModelServer(): Promise<ClaudeLanguageModelServer> {161if (!this._langModelServer) {162this._langModelServer = this.instantiationService.createInstance(ClaudeLanguageModelServer);163await this._langModelServer.start();164}165166return this._langModelServer;167}168}169170// Taken from171// https://github.com/microsoft/vscode/blob/30cd06b93d47b98d2cfa7c32be721d3c20aa0761/src/vs/platform/terminal/common/terminalStrings.ts#L18-L34172173export interface ITerminalFormatMessageOptions {174/**175* Whether to exclude the new line at the start of the message. Defaults to false.176*/177excludeLeadingNewLine?: boolean;178/**179* Whether to use "loud" formatting, this is for more important messages where the it's180* desirable to visually break the buffer up. Defaults to false.181*/182loudFormatting?: boolean;183}184185/**186* Formats a message from the product to be written to the terminal.187*/188export function formatMessageForTerminal(message: string, options: ITerminalFormatMessageOptions = {}): string {189let result = '';190if (!options.excludeLeadingNewLine) {191result += '\r\n';192}193result += '\x1b[0m\x1b[7m * ';194if (options.loudFormatting) {195result += '\x1b[0;104m';196} else {197result += '\x1b[0m';198}199result += ` ${message} \x1b[0m\n\r`;200return result;201}202203registerClaudeSlashCommand(TerminalSlashCommand);204205206