Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,11 @@
"title": "Open Memory Files",
"category": "Claude Agent"
},
{
"command": "copilot.claude.terminal",
"title": "%github.copilot.command.claude.terminal%",
"category": "Claude Agent"
},
{
"command": "github.copilot.cli.sessions.delete",
"title": "%github.copilot.command.deleteAgentSession%",
Expand Down Expand Up @@ -5346,6 +5351,10 @@
{
"name": "memory",
"description": "Open memory files (CLAUDE.md) for editing"
},
{
"name": "terminal",
"description": "Launch Claude Code CLI using your GitHub Copilot subscription"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@
"github.copilot.config.tools.defaultToolsGrouped": "Group default tools in prompts.",
"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.command.claude.terminal": "New CLI Session",
"github.copilot.config.cli.customAgents.enabled": "Enable Custom Agents for Background Agents.",
"github.copilot.config.cli.mcp.enabled": "Enable Model Context Protocol (MCP) server for Background Agents.",
"github.copilot.config.backgroundAgent.enabled": "Enable the Background Agent. When disabled, the Background Agent will not be available in 'Continue In' context menus.",
Expand Down
8 changes: 5 additions & 3 deletions src/extension/agents/claude/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,10 @@ Slash commands often need to present choices or gather input from users. When do
The `show*` APIs are sufficient for most slash command use cases and result in cleaner, more maintainable code. Only use `create*` APIs when you need advanced features like dynamic item updates, multi-step wizards, or custom event handling.

**Current Slash Commands:**
- `/hooks` - Display information about registered hooks (from `hooksCommand.ts`)
- `/memory` - Memory management commands (from `memoryCommand.ts`)
- `/hooks` - Configure Claude Agent hooks for tool execution and events (from `hooksCommand.ts`)
- `/memory` - Open memory files (CLAUDE.md) for editing (from `memoryCommand.ts`)
- `/agents` - Create and manage specialized Claude agents (from `agentsCommand.ts`)
- `/terminal` - Create a terminal with Claude CLI configured to use Copilot Chat endpoints (from `terminalCommand.ts`)

### Tool Permission Handlers

Expand All @@ -260,7 +262,7 @@ Tool permission handlers control what actions Claude can take without user confi
- **Common handlers** (`common/toolPermissionHandlers/`):
- `bashToolHandler.ts` - Controls bash/shell command execution
- `exitPlanModeHandler.ts` - Manages plan mode transitions

- **Node handlers** (`node/toolPermissionHandlers/`):
- `editToolHandler.ts` - Handles file edit operations (Edit, Write, MultiEdit)

Expand Down
18 changes: 15 additions & 3 deletions src/extension/agents/claude/node/claudeLanguageModelServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,23 @@ export class ClaudeLanguageModelServer extends Disposable {
}

/**
* Verify nonce
* Verify nonce from x-api-key or Authorization header
*/
private async isAuthTokenValid(req: http.IncomingMessage): Promise<boolean> {
const authHeader = req.headers['x-api-key'];
return authHeader === this.config.nonce;
// Check x-api-key header (used by SDK)
const apiKeyHeader = req.headers['x-api-key'];
if (apiKeyHeader === this.config.nonce) {
return true;
}

// Check Authorization header with Bearer prefix (used by CLI with ANTHROPIC_AUTH_TOKEN)
const authHeader = req.headers['authorization'];
if (typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
const token = authHeader.slice(7); // Remove "Bearer " prefix
return token === this.config.nonce;
}

return false;
}

private async readRequestBody(req: http.IncomingMessage): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import './agentsCommand';
import './hooksCommand';
import './memoryCommand';
import './terminalCommand';

// Future commands can be added here:
// import './settingsCommand';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { execFile } from 'child_process';
import { promisify } from 'util';
import * as vscode from 'vscode';
import { ILogService } from '../../../../../platform/log/common/logService';
import { ITerminalService } from '../../../../../platform/terminal/common/terminalService';
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
import { ClaudeLanguageModelServer } from '../../node/claudeLanguageModelServer';
import { IClaudeSlashCommandHandler, registerClaudeSlashCommand } from './claudeSlashCommandRegistry';

const execFileAsync = promisify(execFile);

/**
* Slash command handler for creating a terminal session with Claude CLI configured
* to use Copilot Chat's endpoints.
*
* This command starts a ClaudeLanguageModelServer instance (if not already running)
* and creates a new terminal with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY environment
* variables set to proxy requests through Copilot Chat's chat endpoints.
*
* ## Usage
* 1. In a Claude Agent chat session, type `/terminal`
* 2. A new terminal will be created with the environment variables configured
* 3. Run `claude` in the terminal to start Claude Code
* 4. Claude Code will use Copilot Chat's endpoints for all LLM requests
*
* ## Requirements
* - Claude CLI (`claude`) must be installed and available in PATH
* - The terminal inherits the environment with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY set
* - The language model server runs on localhost with a random available port
*/
export class TerminalSlashCommand implements IClaudeSlashCommandHandler {
readonly commandName = 'terminal';
readonly description = vscode.l10n.t('Launch Claude Code CLI using your GitHub Copilot subscription');
readonly commandId = 'copilot.claude.terminal';

private _langModelServer: ClaudeLanguageModelServer | undefined;

constructor(
@ILogService private readonly logService: ILogService,
@ITerminalService private readonly terminalService: ITerminalService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }

async handle(
_args: string,
stream: vscode.ChatResponseStream | undefined,
_token: CancellationToken
): Promise<vscode.ChatResult> {
stream?.markdown(vscode.l10n.t('Creating Claude Code CLI instance...'));

try {
// Check which CLI is available
const cliCommand = await this._getClaudeCliCommand();
if (!cliCommand) {
const installUrl = 'https://code.claude.com';
const downloadLabel = vscode.l10n.t('Download Claude Code CLI');
if (stream) {
stream.markdown(vscode.l10n.t('Claude Code CLI is not installed. Download Claude Code CLI to get started.'));
stream.button({ command: 'vscode.open', arguments: [vscode.Uri.parse(installUrl)], title: downloadLabel });
} else {
vscode.window.showErrorMessage(
vscode.l10n.t('Claude Code CLI is not installed.'),
downloadLabel
).then(selection => {
if (selection === downloadLabel) {
vscode.env.openExternal(vscode.Uri.parse(installUrl));
}
});
}
return {};
}

// Get or create the language model server
const server = await this._getLanguageModelServer();
const config = server.getConfig();

// Create terminal with environment variables configured
const terminal = this.terminalService.createTerminal({
name: 'Claude',
message: '\n\x1b[1;36m▸ ' + vscode.l10n.t('This instance of Claude Code CLI is configured to use your GitHub Copilot subscription.') + '\x1b[0m\n',
env: {
ANTHROPIC_BASE_URL: `http://localhost:${config.port}`,
ANTHROPIC_AUTH_TOKEN: config.nonce
}
});

// Show the terminal
terminal.show();

// Send the appropriate command to the terminal
terminal.sendText(cliCommand);
Comment on lines +84 to +97
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The terminal created by this command is not tracked or disposed. While VS Code manages terminal lifecycle, the terminal may outlive the language model server, potentially leading to connection errors when the user tries to use the CLI in the terminal after the extension is deactivated or the server is stopped. Consider tracking terminal instances and notifying users if the server stops, or ensuring the server remains active as long as associated terminals exist.

Copilot uses AI. Check for mistakes.

this.logService.info(`[TerminalSlashCommand] Created terminal with Claude CLI configured on port ${config.port}, command: ${cliCommand}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logService.error('[TerminalSlashCommand] Error creating terminal:', error);
if (stream) {
stream.markdown(vscode.l10n.t('Error creating terminal: {0}', errorMessage));
} else {
vscode.window.showErrorMessage(vscode.l10n.t('Error creating terminal: {0}', errorMessage));
}
}

return {};
}

/**
* Check which Claude CLI command is available.
* Returns 'claude' if available, 'agency claude' if agency is available, or undefined if neither.
*/
private async _getClaudeCliCommand(): Promise<string | undefined> {
const whichCommand = process.platform === 'win32' ? 'where' : 'which';

// Check if 'claude' is available
if (await this._isCommandAvailable(whichCommand, 'claude')) {
return 'claude';
}

// Check if 'agency' is available (fallback)
if (await this._isCommandAvailable(whichCommand, 'agency')) {
return 'agency claude';
}

return undefined;
}

/**
* Check if a command is available in PATH
*/
private async _isCommandAvailable(whichCommand: string, command: string): Promise<boolean> {
try {
await execFileAsync(whichCommand, [command]);
return true;
} catch {
return false;
}
}

private async _getLanguageModelServer(): Promise<ClaudeLanguageModelServer> {
if (!this._langModelServer) {
this._langModelServer = this.instantiationService.createInstance(ClaudeLanguageModelServer);
await this._langModelServer.start();
}

return this._langModelServer;
}
Comment on lines +145 to +152
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TerminalSlashCommand creates a ClaudeLanguageModelServer instance that extends Disposable but never disposes of it. The server starts an HTTP server that should be properly disposed when no longer needed. Consider making TerminalSlashCommand extend Disposable and register the server for disposal, or dispose of it when appropriate (e.g., when the extension is deactivated).

Copilot uses AI. Check for mistakes.
}

// Self-register the terminal command
registerClaudeSlashCommand(TerminalSlashCommand);
Loading
Loading