Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2851,6 +2851,12 @@
"enablement": "github.copilot.chat.copilotCLI.hasSession",
"category": "Copilot CLI"
},
{
"command": "github.copilot.chat.copilotCLI.addSelection",
"title": "%github.copilot.command.chat.copilotCLI.addSelection%",
"enablement": "github.copilot.chat.copilotCLI.hasSession",
"category": "Copilot CLI"
},
{
"command": "github.copilot.chat.copilotCLI.acceptDiff",
"title": "%github.copilot.command.chat.copilotCLI.acceptDiff%",
Expand Down Expand Up @@ -4605,6 +4611,11 @@
"command": "github.copilot.chat.copilotCLI.addFileReference",
"group": "copilot",
"when": "github.copilot.chat.copilotCLI.hasSession && !inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'"
},
{
"command": "github.copilot.chat.copilotCLI.addSelection",
"group": "copilot",
"when": "github.copilot.chat.copilotCLI.hasSession && editorHasSelection && !inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'"
}
],
"editor/context/chat": [
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@
"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 to Copilot CLI",
"github.copilot.command.chat.copilotCLI.addSelection": "Add Selection to Copilot CLI",
"github.copilot.command.chat.copilotCLI.acceptDiff": "Accept Changes",
"github.copilot.command.chat.copilotCLI.rejectDiff": "Reject Changes",
"github.copilot.command.openCopilotAgentSessionsInBrowser": "Open in Browser",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,21 @@
*--------------------------------------------------------------------------------------------*/

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

export interface FileReferenceInfo {
filePath: string;
fileUrl: string;
selection: {
start: { line: number; character: number };
end: { line: number; character: number };
} | null;
selectedText: string | null;
}
import { sendEditorContextToSession, sendUriToSession } from './sendContext';

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, sessionTracker: ICopilotCLISessionTracker): vscode.Disposable {
return vscode.commands.registerCommand(ADD_FILE_REFERENCE_COMMAND, async (uri?: vscode.Uri) => {
logger.debug('Add file reference command executed');

const sessionId = await pickSession(logger, httpServer, sessionTracker);
if (!sessionId) {
return;
}

// 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(`Sending file reference from explorer to session ${sessionId}: ${uri.fsPath}`);
httpServer.sendNotification(
sessionId,
ADD_FILE_REFERENCE_NOTIFICATION,
fileReferenceInfo as unknown as Record<string, unknown>,
);
return;
await sendUriToSession(logger, httpServer, sessionTracker, uri);
} else {
await sendEditorContextToSession(logger, httpServer, sessionTracker);
}

// 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(`Sending file reference to session ${sessionId}: ${selectionInfo.filePath}`);
httpServer.sendNotification(sessionId, ADD_FILE_REFERENCE_NOTIFICATION, fileReferenceInfo as unknown as Record<string, unknown>);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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 { ILogger } from '../../../../../platform/log/common/logService';
import { ICopilotCLISessionTracker } from '../copilotCLISessionTracker';
import { InProcHttpServer } from '../inProcHttpServer';
import { sendEditorContextToSession } from './sendContext';

export const ADD_SELECTION_COMMAND = 'github.copilot.chat.copilotCLI.addSelection';

export function registerAddSelectionCommand(logger: ILogger, httpServer: InProcHttpServer, sessionTracker: ICopilotCLISessionTracker): vscode.Disposable {
return vscode.commands.registerCommand(ADD_SELECTION_COMMAND, async () => {
logger.debug('Add selection command executed');
await sendEditorContextToSession(logger, httpServer, sessionTracker);
});
}
15 changes: 3 additions & 12 deletions src/extension/agents/copilotcli/vscode-node/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,6 @@
* 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';
export { registerAddFileReferenceCommand } from './addFileReference';
export { registerAddSelectionCommand } from './addSelection';
export { registerDiffCommands } from './diffCommands';
116 changes: 116 additions & 0 deletions src/extension/agents/copilotcli/vscode-node/commands/sendContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as l10n from '@vscode/l10n';
import * as vscode from 'vscode';
import { ILogger } from '../../../../../platform/log/common/logService';
import { Schemas } from '../../../../../util/vs/base/common/network';
import { ICopilotCLISessionTracker } from '../copilotCLISessionTracker';
import { InProcHttpServer } from '../inProcHttpServer';
import { getSelectionInfo } from '../tools';
import { pickSession } from './pickSession';

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_NOTIFICATION = 'add_file_reference';

/**
* URI schemes that represent real file-system files and can be sent to CLI sessions.
*/
const ALLOWED_SCHEMES = new Set([Schemas.file]);

/**
* Validates URI scheme and shows warning if not allowed.
* Returns true if allowed, false otherwise.
*/
function validateScheme(logger: ILogger, uri: vscode.Uri): boolean {
if (ALLOWED_SCHEMES.has(uri.scheme)) {
return true;
}
logger.debug(`Unsupported URI scheme: ${uri.scheme}`);
vscode.window.showWarningMessage(l10n.t('Cannot send virtual files to Copilot CLI.'));
return false;
}

/**
* Picks a session (if needed) and sends a file reference notification.
*/
export async function sendToSession(
logger: ILogger,
httpServer: InProcHttpServer,
sessionTracker: ICopilotCLISessionTracker,
fileReferenceInfo: FileReferenceInfo,
): Promise<void> {
const sessionId = await pickSession(logger, httpServer, sessionTracker);
if (!sessionId) {
return;
}

logger.info(`Sending context to session ${sessionId}: ${fileReferenceInfo.filePath}`);
httpServer.sendNotification(sessionId, ADD_FILE_REFERENCE_NOTIFICATION, fileReferenceInfo as unknown as Record<string, unknown>);
}

/**
* Sends a file reference (from explorer URI) to a CLI session.
*/
export async function sendUriToSession(
logger: ILogger,
httpServer: InProcHttpServer,
sessionTracker: ICopilotCLISessionTracker,
uri: vscode.Uri,
): Promise<void> {
if (!validateScheme(logger, uri)) {
return;
}

await sendToSession(logger, httpServer, sessionTracker, {
filePath: uri.fsPath,
fileUrl: uri.toString(),
selection: null,
selectedText: null,
});
}

/**
* Sends editor context (file + optional selection) to a CLI session.
*/
export async function sendEditorContextToSession(
logger: ILogger,
httpServer: InProcHttpServer,
sessionTracker: ICopilotCLISessionTracker,
): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
logger.debug('No active editor');
vscode.window.showWarningMessage(l10n.t('No active editor. Open a file to add a reference.'));
return;
}

if (!validateScheme(logger, editor.document.uri)) {
return;
}

const selectionInfo = getSelectionInfo(editor);

await sendToSession(logger, httpServer, sessionTracker, {
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,
});
}
3 changes: 2 additions & 1 deletion src/extension/agents/copilotcli/vscode-node/contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import { ILogger, ILogService } from '../../../../platform/log/common/logService';
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
import { ServiceCollection } from '../../../../util/vs/platform/instantiation/common/serviceCollection';
import { registerAddFileReferenceCommand, registerDiffCommands } from './commands';
import { registerAddFileReferenceCommand, registerAddSelectionCommand, registerDiffCommands } from './commands';
import { registerCommandContext } from './commands/context';
import { CopilotCLISessionTracker, ICopilotCLISessionTracker } from './copilotCLISessionTracker';
import { DiffStateManager } from './diffState';
Expand Down Expand Up @@ -42,6 +42,7 @@ export class CopilotCLIContrib extends Disposable {

// Register commands
this._register(registerAddFileReferenceCommand(logger, httpServer, this.sessionTracker));
this._register(registerAddSelectionCommand(logger, httpServer, this.sessionTracker));
for (const d of registerDiffCommands(logger, diffState)) {
this._register(d);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { TestLogService } from '../../../../../platform/testing/common/testLogService';
import type { InProcHttpServer } from '../inProcHttpServer';
import { MockHttpServer, MockSessionTracker, createMockEditor } from './testHelpers';
import { MockHttpServer, MockSessionTracker, createMockEditor, createMockEditorWithScheme } from './testHelpers';

const { mockRegisterCommand, mockActiveTextEditor, mockShowQuickPick } = vi.hoisted(() => ({
mockRegisterCommand: vi.fn(),
Expand All @@ -26,7 +26,8 @@ vi.mock('vscode', () => ({
}));

import * as vscode from 'vscode';
import { ADD_FILE_REFERENCE_COMMAND, ADD_FILE_REFERENCE_NOTIFICATION, registerAddFileReferenceCommand } from '../commands/addFileReference';
import { ADD_FILE_REFERENCE_COMMAND, registerAddFileReferenceCommand } from '../commands/addFileReference';
import { ADD_FILE_REFERENCE_NOTIFICATION } from '../commands/sendContext';

describe('addFileReference command', () => {
const logger = new TestLogService();
Expand Down Expand Up @@ -60,6 +61,7 @@ describe('addFileReference command', () => {

const uri = {
fsPath: '/test/explorer-file.ts',
scheme: 'file',
toString: () => 'file:///test/explorer-file.ts',
};

Expand Down Expand Up @@ -150,6 +152,7 @@ describe('addFileReference command', () => {

const explorerUri = {
fsPath: '/test/explorer-file.ts',
scheme: 'file',
toString: () => 'file:///test/explorer-file.ts',
};
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(explorerUri);
Expand All @@ -167,6 +170,7 @@ describe('addFileReference command', () => {

it('should show warning when no sessions are connected', async () => {
httpServer.setConnectedSessionIds([]);
mockActiveTextEditor.value = createMockEditor('/test/file.ts', 'content', 0, 0, 0, 0);

registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();
Expand All @@ -185,6 +189,7 @@ describe('addFileReference command', () => {

const uri = {
fsPath: '/test/file.ts',
scheme: 'file',
toString: () => 'file:///test/file.ts',
};
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);
Expand All @@ -202,6 +207,7 @@ describe('addFileReference command', () => {
sessionTracker.setSessionName('session-1', 'My CLI');
sessionTracker.setSessionName('session-2', 'session-2');
mockShowQuickPick.mockResolvedValue({ sessionId: 'session-1', label: 'My CLI' });
mockActiveTextEditor.value = createMockEditor('/test/file.ts', 'content', 0, 0, 0, 0);

registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();
Expand All @@ -222,4 +228,50 @@ describe('addFileReference command', () => {

expect(httpServer.sendNotification).not.toHaveBeenCalled();
});

describe('URI scheme validation', () => {
it('should reject output scheme from editor with warning', async () => {
mockActiveTextEditor.value = createMockEditorWithScheme('/Output', 'content', 0, 0, 0, 7, 'output');

registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();

expect(httpServer.sendNotification).not.toHaveBeenCalled();
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(
'Cannot send virtual files to Copilot CLI.',
);
});

it('should reject virtual scheme from explorer URI with warning', async () => {
registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());

const uri = {
fsPath: '/block',
scheme: 'vscode-chat-code-block',
toString: () => 'vscode-chat-code-block:///block',
};
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);

expect(httpServer.sendNotification).not.toHaveBeenCalled();
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(
'Cannot send virtual files to Copilot CLI.',
);
});

it('should reject vscode-remote scheme from explorer URI with warning', async () => {
registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());

const uri = {
fsPath: '/remote/file.ts',
scheme: 'vscode-remote',
toString: () => 'vscode-remote:///remote/file.ts',
};
await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);

expect(httpServer.sendNotification).not.toHaveBeenCalled();
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(
'Cannot send virtual files to Copilot CLI.',
);
});
});
});
Loading