Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/vscode-node/slashCommands/terminalCommand.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { execFile } from 'child_process';
7
import { promisify } from 'util';
8
import * as vscode from 'vscode';
9
import { ILogService } from '../../../../../platform/log/common/logService';
10
import { CapturingToken } from '../../../../../platform/requestLogger/common/capturingToken';
11
import { ITerminalService } from '../../../../../platform/terminal/common/terminalService';
12
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
13
import { generateUuid } from '../../../../../util/vs/base/common/uuid';
14
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
15
import { ClaudeLanguageModelServer } from '../../node/claudeLanguageModelServer';
16
import { IClaudeSessionStateService } from '../../common/claudeSessionStateService';
17
import { IClaudeSlashCommandHandler, registerClaudeSlashCommand } from './claudeSlashCommandRegistry';
18
19
const execFileAsync = promisify(execFile);
20
21
/**
22
* Slash command handler for creating a terminal session with Claude CLI configured
23
* to use Copilot Chat's endpoints.
24
*
25
* This command starts a ClaudeLanguageModelServer instance (if not already running)
26
* and creates a new terminal with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY environment
27
* variables set to proxy requests through Copilot Chat's chat endpoints.
28
*
29
* ## Usage
30
* 1. In a Claude Agent chat session, type `/terminal`
31
* 2. A new terminal will be created with the environment variables configured
32
* 3. Run `claude` in the terminal to start Claude Code
33
* 4. Claude Code will use Copilot Chat's endpoints for all LLM requests
34
*
35
* ## Requirements
36
* - Claude CLI (`claude`) must be installed and available in PATH
37
* - The terminal inherits the environment with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY set
38
* - The language model server runs on localhost with a random available port
39
*/
40
export class TerminalSlashCommand implements IClaudeSlashCommandHandler {
41
readonly commandName = 'terminal';
42
readonly description = vscode.l10n.t('Launch Claude Code CLI using your GitHub Copilot subscription');
43
readonly commandId = 'copilot.claude.terminal';
44
45
private _langModelServer: ClaudeLanguageModelServer | undefined;
46
47
constructor(
48
@ILogService private readonly logService: ILogService,
49
@ITerminalService private readonly terminalService: ITerminalService,
50
@IClaudeSessionStateService private readonly sessionStateService: IClaudeSessionStateService,
51
@IInstantiationService private readonly instantiationService: IInstantiationService,
52
) { }
53
54
async handle(
55
_args: string,
56
stream: vscode.ChatResponseStream | undefined,
57
_token: CancellationToken
58
): Promise<vscode.ChatResult> {
59
stream?.markdown(vscode.l10n.t('Creating Claude CLI instance...'));
60
61
try {
62
// Check which CLI is available
63
const cliCommand = await this._getClaudeCliCommand();
64
if (!cliCommand) {
65
const installUrl = 'https://code.claude.com';
66
const downloadLabel = vscode.l10n.t('Download Claude CLI');
67
if (stream) {
68
stream.markdown(vscode.l10n.t('Claude CLI is not installed. Download Claude CLI to get started.'));
69
stream.button({ command: 'vscode.open', arguments: [vscode.Uri.parse(installUrl)], title: downloadLabel });
70
} else {
71
vscode.window.showErrorMessage(
72
vscode.l10n.t('Claude CLI is not installed.'),
73
downloadLabel
74
).then(selection => {
75
if (selection === downloadLabel) {
76
vscode.env.openExternal(vscode.Uri.parse(installUrl));
77
}
78
});
79
}
80
return {};
81
}
82
83
// Get or create the language model server
84
const server = await this._getLanguageModelServer();
85
const config = server.getConfig();
86
87
// Generate a unique session ID for this terminal session
88
const sessionId = generateUuid();
89
90
// Create terminal with environment variables configured
91
const terminal = this.terminalService.createTerminal({
92
name: 'Claude',
93
message: formatMessageForTerminal(vscode.l10n.t('This instance of Claude CLI is configured to use your GitHub Copilot subscription.'), { loudFormatting: true }),
94
env: {
95
ANTHROPIC_BASE_URL: `http://localhost:${config.port}`,
96
ANTHROPIC_AUTH_TOKEN: `${config.nonce}.${sessionId}`,
97
// Hide account info banner in CLI since it's redundant with the message above
98
CLAUDE_CODE_HIDE_ACCOUNT_INFO: '1',
99
}
100
});
101
102
// Show the terminal
103
terminal.show();
104
105
// Send the appropriate command to the terminal with the session ID
106
terminal.sendText(`${cliCommand} --session-id ${sessionId}`);
107
108
// Set capturing token only after terminal is successfully created to avoid leaking stale session state
109
this.sessionStateService.setCapturingTokenForSession(
110
sessionId,
111
new CapturingToken(`Claude CLI (${sessionId})`, 'claude')
112
);
113
114
this.logService.info(`[TerminalSlashCommand] Created terminal with Claude CLI configured on port ${config.port}, command: ${cliCommand}, sessionId: ${sessionId}`);
115
} catch (error) {
116
const errorMessage = error instanceof Error ? error.message : String(error);
117
this.logService.error('[TerminalSlashCommand] Error creating terminal:', error);
118
if (stream) {
119
stream.markdown(vscode.l10n.t('Error creating terminal: {0}', errorMessage));
120
} else {
121
vscode.window.showErrorMessage(vscode.l10n.t('Error creating terminal: {0}', errorMessage));
122
}
123
}
124
125
return {};
126
}
127
128
/**
129
* Check which Claude CLI command is available.
130
* Returns 'claude' if available, 'agency claude' if agency is available, or undefined if neither.
131
* TODO: support some way to specify custom path to CLI in case it's not in PATH
132
*/
133
private async _getClaudeCliCommand(): Promise<string | undefined> {
134
const whichCommand = process.platform === 'win32' ? 'where' : 'which';
135
136
// Check if 'claude' is available
137
if (await this._isCommandAvailable(whichCommand, 'claude')) {
138
return 'claude';
139
}
140
141
// Check if 'agency' is available (fallback)
142
if (await this._isCommandAvailable(whichCommand, 'agency')) {
143
return 'agency claude';
144
}
145
146
return undefined;
147
}
148
149
/**
150
* Check if a command is available in PATH
151
*/
152
private async _isCommandAvailable(whichCommand: string, command: string): Promise<boolean> {
153
try {
154
await execFileAsync(whichCommand, [command]);
155
return true;
156
} catch {
157
return false;
158
}
159
}
160
161
private async _getLanguageModelServer(): Promise<ClaudeLanguageModelServer> {
162
if (!this._langModelServer) {
163
this._langModelServer = this.instantiationService.createInstance(ClaudeLanguageModelServer);
164
await this._langModelServer.start();
165
}
166
167
return this._langModelServer;
168
}
169
}
170
171
// Taken from
172
// https://github.com/microsoft/vscode/blob/30cd06b93d47b98d2cfa7c32be721d3c20aa0761/src/vs/platform/terminal/common/terminalStrings.ts#L18-L34
173
174
export interface ITerminalFormatMessageOptions {
175
/**
176
* Whether to exclude the new line at the start of the message. Defaults to false.
177
*/
178
excludeLeadingNewLine?: boolean;
179
/**
180
* Whether to use "loud" formatting, this is for more important messages where the it's
181
* desirable to visually break the buffer up. Defaults to false.
182
*/
183
loudFormatting?: boolean;
184
}
185
186
/**
187
* Formats a message from the product to be written to the terminal.
188
*/
189
export function formatMessageForTerminal(message: string, options: ITerminalFormatMessageOptions = {}): string {
190
let result = '';
191
if (!options.excludeLeadingNewLine) {
192
result += '\r\n';
193
}
194
result += '\x1b[0m\x1b[7m * ';
195
if (options.loudFormatting) {
196
result += '\x1b[0;104m';
197
} else {
198
result += '\x1b[0m';
199
}
200
result += ` ${message} \x1b[0m\n\r`;
201
return result;
202
}
203
204
registerClaudeSlashCommand(TerminalSlashCommand);
205
206