Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
addcade
Initial code migration
alexweininger Feb 6, 2026
1ecf96a
Migrate unit tests
alexweininger Feb 6, 2026
aeeccad
Fix command titles
alexweininger Feb 6, 2026
5aa29d4
Use DI patterns
alexweininger Feb 6, 2026
64bb061
add tests
alexweininger Feb 6, 2026
f4e236a
Merge branch 'main' into alex/migrate-cli
alexweininger Feb 6, 2026
1f23b5e
Fix createReadonlyUri to use fileUri.path for correct URI generation
alexweininger Feb 6, 2026
c02595d
Fix makeTextResult to ensure fallback string conversion for non-strin…
alexweininger Feb 6, 2026
da31d33
Add debounce cleanup for diagnostics and selection change notifications
alexweininger Feb 6, 2026
68f07ee
Increase JSON body size limit to 10MB for MCP requests
alexweininger Feb 6, 2026
15321bc
Normalize diagnostic severity to lowercase in getDiagnostics tool
alexweininger Feb 6, 2026
14aa084
Refactor logging level in DiffStateManager to use trace for detailed …
alexweininger Feb 6, 2026
97a2897
Handle errors when updating hasActiveDiff context in DiffStateManager
alexweininger Feb 6, 2026
e97cdd1
Refactor showNotification tool: reorder imports and remove unnecessar…
alexweininger Feb 6, 2026
37a9264
Refactor closeDiff tests: replace createMockServer with MockMcpServer…
alexweininger Feb 6, 2026
f8494cf
Remove showNotification tool and its associated tests
alexweininger Feb 6, 2026
0696cbe
Add mock implementations for Disposable and Uri in test files
alexweininger Feb 6, 2026
36ef8b2
Fix unit tests!
alexweininger Feb 6, 2026
a5526da
Merge branch 'main' into alex/migrate-cli
DonJayamanne Feb 7, 2026
30b9a96
feat: Add CLI integration configuration and update command titles in …
DonJayamanne Feb 8, 2026
85b6127
refactor: Improve debounce handling for diagnostics and selection cha…
DonJayamanne Feb 8, 2026
9d813e8
Merge branch 'main' into alex/migrate-cli
DonJayamanne Feb 8, 2026
542b581
refactor: Enhance initialization logic and configuration handling in …
DonJayamanne Feb 8, 2026
48d500a
Revert change
DonJayamanne Feb 8, 2026
15f35a5
fix: Disable CLI integration by default in configuration
DonJayamanne Feb 9, 2026
996cb00
Merge branch 'main' into alex/migrate-cli
DonJayamanne Feb 9, 2026
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
768 changes: 704 additions & 64 deletions package-lock.json

Large diffs are not rendered by default.

64 changes: 63 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,23 @@
"icon": "$(git-stash-pop)",
"category": "GitHub Copilot"
},
{
"command": "github.copilot.chat.copilotCLI.addFileReference",
"title": "%github.copilot.command.chat.copilotCLI.addFileReference%",
"category": "Copilot CLI"
},
{
"command": "github.copilot.chat.copilotCLI.acceptDiff",
"title": "%github.copilot.command.chat.copilotCLI.acceptDiff%",
"icon": "$(check)",
"category": "Copilot CLI"
},
{
"command": "github.copilot.chat.copilotCLI.rejectDiff",
"title": "%github.copilot.command.chat.copilotCLI.rejectDiff%",
"icon": "$(close)",
"category": "Copilot CLI"
},
{
"command": "github.copilot.chat.checkoutPullRequestReroute",
"title": "%github.copilot.command.checkoutPullRequestReroute.title%",
Expand Down Expand Up @@ -4380,6 +4397,15 @@
"experimental"
]
},
"github.copilot.chat.cli.integration.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.cli.integration.enabled%",
"tags": [
"advanced",
"experimental"
]
},
"github.copilot.chat.cli.mcp.enabled": {
"type": "boolean",
"default": false,
Expand Down Expand Up @@ -4488,20 +4514,47 @@
"command": "github.copilot.chat.showAsChatSession",
"group": "navigation@9",
"when": "resourceFilename === 'benchRun.chatReplay.json' || resourceFilename === 'chat-export-logs.json'"
},
{
"command": "github.copilot.chat.copilotCLI.acceptDiff",
"group": "navigation@1",
"when": "github.copilot.chat.copilotCLI.hasActiveDiff"
},
{
"command": "github.copilot.chat.copilotCLI.rejectDiff",
"group": "navigation@2",
"when": "github.copilot.chat.copilotCLI.hasActiveDiff"
}
],
"editor/title/context": [
{
"command": "github.copilot.chat.copilotCLI.addFileReference",
"group": "copilot",
"when": "config.github.copilot.chat.cli.integration.enabled && !inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'"
}
],
"explorer/context": [
{
"command": "github.copilot.chat.showAsChatSession",
"when": "resourceFilename === 'benchRun.chatReplay.json' || resourceFilename === 'chat-export-logs.json'",
"group": "2_copilot@1"
},
{
"command": "github.copilot.chat.copilotCLI.addFileReference",
"group": "copilot",
"when": "config.github.copilot.chat.cli.integration.enabled && !explorerResourceIsFolder"
}
],
"editor/context": [
{
"command": "github.copilot.chat.explain",
"when": "!github.copilot.interactiveSession.disabled",
"group": "1_chat@4"
},
{
"command": "github.copilot.chat.copilotCLI.addFileReference",
"group": "copilot",
"when": "!inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'"
}
],
"editor/context/chat": [
Expand Down Expand Up @@ -5316,6 +5369,12 @@
}
},
"keybindings": [
{
"command": "github.copilot.chat.copilotCLI.addFileReference",
"key": "ctrl+shift+.",
"mac": "cmd+shift+.",
"when": "editorTextFocus"
},
{
"command": "github.copilot.chat.rerunWithCopilotDebug",
"key": "ctrl+alt+.",
Expand Down Expand Up @@ -5723,6 +5782,7 @@
"@types/google-protobuf": "^3.15.12",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it": "^14.0.0",
"@types/express": "^5.0.6",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.10",
"@types/node": "^22.16.3",
Expand Down Expand Up @@ -5832,7 +5892,9 @@
"minimatch": "^10.0.3",
"undici": "^7.18.2",
"vscode-tas-client": "^0.1.84",
"web-tree-sitter": "^0.23.0"
"web-tree-sitter": "^0.23.0",
"express": "^5.2.1",
"@modelcontextprotocol/sdk": "^1.25.2"
},
"overrides": {
"@aminya/node-gyp-build": "npm:node-gyp-build@4.8.1",
Expand Down
4 changes: 4 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@
"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.chat.copilotCLI.addFileReference": "Add File Reference to Prompt",
"github.copilot.command.chat.copilotCLI.acceptDiff": "Accept Changes",
"github.copilot.command.chat.copilotCLI.rejectDiff": "Reject Changes",
"github.copilot.config.cli.integration.enabled": "Enable GitHub Copilot CLI integration in VS Code",
"github.copilot.command.openCopilotAgentSessionsInBrowser": "Open in Browser",
"github.copilot.command.closeChatSessionPullRequest.title": "Close Pull Request",
"github.copilot.command.installPRExtension.title": "Install GitHub Pull Request Extension",
Expand Down
14 changes: 14 additions & 0 deletions src/extension/agents/copilotcli/vscode-node/cliHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { homedir } from 'os';
import { join } from 'path';

const APP_DIRECTORY = '.copilot';

export function getCopilotCliStateDir(): string {
const xdgHome = process.env.XDG_STATE_HOME;
return xdgHome ? join(xdgHome, APP_DIRECTORY) : join(homedir(), APP_DIRECTORY);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as l10n from '@vscode/l10n';
import { ILogger } from '../../../../../platform/log/common/logService';
import { InProcHttpServer } from '../inProcHttpServer';
import { getSelectionInfo } from '../tools';

export interface FileReferenceInfo {
filePath: string;
fileUrl: string;
selection: {
start: { line: number; character: number };
end: { line: number; character: number };
} | null;
selectedText: string | null;
}

export const ADD_FILE_REFERENCE_COMMAND = 'github.copilot.chat.copilotCLI.addFileReference';
export const ADD_FILE_REFERENCE_NOTIFICATION = 'add_file_reference';

export function registerAddFileReferenceCommand(logger: ILogger, httpServer: InProcHttpServer): vscode.Disposable {
return vscode.commands.registerCommand(ADD_FILE_REFERENCE_COMMAND, (uri?: vscode.Uri) => {
logger.debug('Add file reference command executed');

// If URI is provided (from explorer context menu), use it directly
if (uri) {
const fileReferenceInfo: FileReferenceInfo = {
filePath: uri.fsPath,
fileUrl: uri.toString(),
selection: null,
selectedText: null,
};

logger.info(`Broadcasting file reference from explorer: ${uri.fsPath}`);
httpServer.broadcastNotification(
ADD_FILE_REFERENCE_NOTIFICATION,
fileReferenceInfo as unknown as Record<string, unknown>,
);
return;
}

// Otherwise, use the active editor
const editor = vscode.window.activeTextEditor;
if (!editor) {
logger.debug('No active editor for file reference');
vscode.window.showWarningMessage(l10n.t('No active editor. Open a file to add a reference.'));
return;
}

const selectionInfo = getSelectionInfo(editor);

const fileReferenceInfo: FileReferenceInfo = {
filePath: selectionInfo.filePath,
fileUrl: selectionInfo.fileUrl,
selection: selectionInfo.selection.isEmpty
? null
: {
start: selectionInfo.selection.start,
end: selectionInfo.selection.end,
},
selectedText: selectionInfo.selection.isEmpty ? null : selectionInfo.text,
};

logger.info(`Broadcasting file reference: ${selectionInfo.filePath}`);
httpServer.broadcastNotification(ADD_FILE_REFERENCE_NOTIFICATION, fileReferenceInfo as unknown as Record<string, unknown>);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { DiffStateManager } from '../diffState';
import { ILogger } from '../../../../../platform/log/common/logService';

export const ACCEPT_DIFF_COMMAND = 'github.copilot.chat.copilotCLI.acceptDiff';
export const REJECT_DIFF_COMMAND = 'github.copilot.chat.copilotCLI.rejectDiff';

export function registerDiffCommands(logger: ILogger, diffState: DiffStateManager): vscode.Disposable[] {
const disposables: vscode.Disposable[] = [];

disposables.push(
vscode.commands.registerCommand(ACCEPT_DIFF_COMMAND, () => {
logger.info('[DIFF] ===== ACCEPT COMMAND =====');
const diff = diffState.getForCurrentTab();
if (!diff) {
logger.info('[DIFF] No active diff found for accept');
return;
}

logger.info(`[DIFF] Accepting diff: ${diff.tabName}, diffId=${diff.diffId}`);
diff.cleanup();
diff.resolve({ status: 'SAVED', trigger: 'accepted_via_button' });
logger.info('[DIFF] Accept command done');
})
);

disposables.push(
vscode.commands.registerCommand(REJECT_DIFF_COMMAND, () => {
logger.info('[DIFF] ===== REJECT COMMAND =====');
const diff = diffState.getForCurrentTab();
if (!diff) {
logger.info('[DIFF] No active diff found for reject');
return;
}
logger.info(`[DIFF] Rejecting diff: ${diff.tabName}, diffId=${diff.diffId}`);
diff.cleanup();
diff.resolve({ status: 'REJECTED', trigger: 'rejected_via_button' });
logger.info('[DIFF] Reject command done');
})
);

return disposables;
}
17 changes: 17 additions & 0 deletions src/extension/agents/copilotcli/vscode-node/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export {
registerAddFileReferenceCommand,
ADD_FILE_REFERENCE_COMMAND,
ADD_FILE_REFERENCE_NOTIFICATION,
FileReferenceInfo,
} from './addFileReference';

export {
registerDiffCommands,
ACCEPT_DIFF_COMMAND,
REJECT_DIFF_COMMAND,
} from './diffCommands';
90 changes: 89 additions & 1 deletion src/extension/agents/copilotcli/vscode-node/contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,104 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
import { ILogger, ILogService } from '../../../../platform/log/common/logService';
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { IExtensionContribution } from '../../../common/contributions';
import { registerAddFileReferenceCommand, registerDiffCommands } from './commands';
import { DiffStateManager } from './diffState';
import { InProcHttpServer } from './inProcHttpServer';
import { cleanupStaleLockFiles, createLockFile } from './lockFile';
import { ReadonlyContentProvider } from './readonlyContentProvider';
import { registerTools, SelectionState } from './tools';
import { registerDiagnosticsChangedNotification, registerSelectionChangedNotification } from './tools/push';

export class CopilotCLIContrib extends Disposable implements IExtensionContribution {
readonly id = 'copilotCLI';

private initialized: boolean = false;
constructor(
@IInstantiationService _instantiationService: IInstantiationService,
@ILogService private readonly logService: ILogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();

this.configurationService.onDidChangeConfiguration(() => this.initialize());
this.initialize();
}

private initialize() {
if (this.initialized) {
return;
}
if (!this.configurationService.getConfig(ConfigKey.Advanced.CLIIntegrationEnabled)) {
return;
}

this.initialized = true;
const logger = this.logService.createSubLogger('CopilotCLI');

// Create shared instances
const diffState = new DiffStateManager(logger);
const httpServer = new InProcHttpServer(logger);
const selectionState = new SelectionState();
const contentProvider = new ReadonlyContentProvider();

// Register commands
this._register(registerAddFileReferenceCommand(logger, httpServer));
for (const d of registerDiffCommands(logger, diffState)) {
this._register(d);
}
for (const d of diffState.setupContextTracking()) {
this._register(d);
}
this._register(contentProvider.register());

// Clean up any stale lockfiles from previous sessions
const cleanedCount = cleanupStaleLockFiles(logger);
if (cleanedCount > 0) {
logger.info(`Cleaned up ${cleanedCount} stale lock file(s).`);
}

// Start the MCP server
this._startMcpServer(logger, httpServer, diffState, selectionState, contentProvider);
}
private async _startMcpServer(logger: ILogger, httpServer: InProcHttpServer, diffState: DiffStateManager, selectionState: SelectionState, contentProvider: ReadonlyContentProvider): Promise<void> {
try {
const { disposable, serverUri, headers } = await httpServer.start({
id: 'vscode-copilot-cli',
serverLabel: 'VS Code Copilot CLI',
serverVersion: '0.0.1',
registerTools: server => {
registerTools(server, logger, diffState, selectionState, contentProvider);
},
registerPushNotifications: () => {
for (const d of registerSelectionChangedNotification(logger, httpServer, selectionState)) {
this._register(d);
}
for (const d of registerDiagnosticsChangedNotification(logger, httpServer)) {
this._register(d);
}
},
});

const lockFile = await createLockFile(serverUri, headers, logger);
logger.info(`MCP server started. Lock file: ${lockFile.path}`);
logger.info(`Server URI: ${serverUri.toString()}`);

// Update lock file when workspace folders change
this._register(vscode.workspace.onDidChangeWorkspaceFolders(() => {
lockFile.update();
logger.info('Workspace folders changed, lock file updated.');
}));

this._register(disposable);
this._register({ dispose: () => lockFile.remove() });
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
logger.error(`Failed to start MCP server: ${errMsg}`);
}
}
}
Loading