Skip to content

Commit aae9b00

Browse files
authored
Allow to start chat session from the command line (#252588)
1 parent 18cd9f8 commit aae9b00

File tree

8 files changed

+177
-32
lines changed

8 files changed

+177
-32
lines changed

src/vs/code/electron-main/main.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -491,22 +491,27 @@ class CodeMain {
491491
// Parse arguments
492492
const args = this.validatePaths(parseMainProcessArgv(process.argv));
493493

494-
// If we are started with --wait create a random temporary file
495-
// and pass it over to the starting instance. We can use this file
496-
// to wait for it to be deleted to monitor that the edited file
497-
// is closed and then exit the waiting process.
498-
//
499-
// Note: we are not doing this if the wait marker has been already
500-
// added as argument. This can happen if VS Code was started from CLI.
501-
502494
if (args.wait && !args.waitMarkerFilePath) {
495+
// If we are started with --wait create a random temporary file
496+
// and pass it over to the starting instance. We can use this file
497+
// to wait for it to be deleted to monitor that the edited file
498+
// is closed and then exit the waiting process.
499+
//
500+
// Note: we are not doing this if the wait marker has been already
501+
// added as argument. This can happen if VS Code was started from CLI.
503502
const waitMarkerFilePath = createWaitMarkerFileSync(args.verbose);
504503
if (waitMarkerFilePath) {
505504
addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath);
506505
args.waitMarkerFilePath = waitMarkerFilePath;
507506
}
508507
}
509508

509+
if (args.chat) {
510+
// If we are started with chat subcommand, the current working
511+
// directory is always the path to open
512+
args._ = [cwd()];
513+
}
514+
510515
return args;
511516
}
512517

src/vs/code/node/cli.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { whenDeleted, writeFileSync } from '../../base/node/pfs.js';
1515
import { findFreePort } from '../../base/node/ports.js';
1616
import { watchFileContents } from '../../platform/files/node/watcher/nodejs/nodejsWatcherLib.js';
1717
import { NativeParsedArgs } from '../../platform/environment/common/argv.js';
18-
import { buildHelpMessage, buildVersionMessage, NATIVE_CLI_COMMANDS, OPTIONS } from '../../platform/environment/node/argv.js';
18+
import { buildHelpMessage, buildStdinMessage, buildVersionMessage, NATIVE_CLI_COMMANDS, OPTIONS } from '../../platform/environment/node/argv.js';
1919
import { addArg, parseCLIProcessArgv } from '../../platform/environment/node/argvHelper.js';
2020
import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from '../../platform/environment/node/stdin.js';
2121
import { createWaitMarkerFileSync } from '../../platform/environment/node/wait.js';
@@ -88,12 +88,18 @@ export async function main(argv: string[]): Promise<any> {
8888
}
8989
}
9090

91-
// Help
91+
// Help (general)
9292
if (args.help) {
9393
const executable = `${product.applicationName}${isWindows ? '.exe' : ''}`;
9494
console.log(buildHelpMessage(product.nameLong, executable, product.version, OPTIONS));
9595
}
9696

97+
// Help (chat)
98+
else if (args.chat?.help) {
99+
const executable = `${product.applicationName}${isWindows ? '.exe' : ''}`;
100+
console.log(buildHelpMessage(product.nameLong, executable, product.version, OPTIONS.chat.options, { isChat: true }));
101+
}
102+
97103
// Version Info
98104
else if (args.version) {
99105
console.log(buildVersionMessage(product.version, product.commit));
@@ -236,7 +242,7 @@ export async function main(argv: string[]): Promise<any> {
236242
});
237243
}
238244

239-
const hasReadStdinArg = args._.some(arg => arg === '-');
245+
const hasReadStdinArg = args._.some(arg => arg === '-') || args.chat?._.some(arg => arg === '-');
240246
if (hasReadStdinArg) {
241247
// remove the "-" argument when we read from stdin
242248
args._ = args._.filter(a => a !== '-');
@@ -275,9 +281,15 @@ export async function main(argv: string[]): Promise<any> {
275281
processCallbacks.push(() => readFromStdinDone.p);
276282
}
277283

278-
// Make sure to open tmp file as editor but ignore it in the "recently open" list
279-
addArg(argv, stdinFilePath);
280-
addArg(argv, '--skip-add-to-recently-opened');
284+
if (args.chat) {
285+
// Make sure to add tmp file as context to chat
286+
addArg(argv, '--add-file', stdinFilePath);
287+
} else {
288+
// Make sure to open tmp file as editor but ignore
289+
// it in the "recently open" list
290+
addArg(argv, stdinFilePath);
291+
addArg(argv, '--skip-add-to-recently-opened');
292+
}
281293

282294
console.log(`Reading from stdin via: ${stdinFilePath}`);
283295
} catch (e) {
@@ -290,11 +302,7 @@ export async function main(argv: string[]): Promise<any> {
290302
// if we detect that data flows into via stdin after a certain timeout.
291303
processCallbacks.push(_ => stdinDataListener(1000).then(dataReceived => {
292304
if (dataReceived) {
293-
if (isWindows) {
294-
console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`);
295-
} else {
296-
console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`);
297-
}
305+
console.log(buildStdinMessage(product.applicationName, !!args.chat));
298306
}
299307
}));
300308
}

src/vs/platform/environment/common/argv.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface INativeCliOptions {
1313
* A list of command line arguments we support natively.
1414
*/
1515
export interface NativeParsedArgs {
16+
1617
// subcommands
1718
tunnel?: INativeCliOptions & {
1819
user: {
@@ -23,6 +24,14 @@ export interface NativeParsedArgs {
2324
};
2425
};
2526
'serve-web'?: INativeCliOptions;
27+
chat?: {
28+
_: string[];
29+
'add-file'?: string[];
30+
mode?: string;
31+
help?: boolean;
32+
};
33+
34+
// arguments
2635
_: string[];
2736
'folder-uri'?: string[]; // undefined or array of 1 or more
2837
'file-uri'?: string[]; // undefined or array of 1 or more

src/vs/platform/environment/node/argv.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const NATIVE_CLI_COMMANDS = ['tunnel', 'serve-web'] as const;
5050
export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
5151
'tunnel': {
5252
type: 'subcommand',
53-
description: 'Make the current machine accessible from vscode.dev or other machines through a secure tunnel',
53+
description: 'Make the current machine accessible from vscode.dev or other machines through a secure tunnel.',
5454
options: {
5555
'cli-data-dir': { type: 'string', args: 'dir', description: localize('cliDataDir', "Directory where CLI metadata should be stored.") },
5656
'disable-telemetry': { type: 'boolean' },
@@ -78,6 +78,16 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
7878
'telemetry-level': { type: 'string' },
7979
}
8080
},
81+
'chat': {
82+
type: 'subcommand',
83+
description: 'Pass in a prompt to run in a chat session in the current working directory.',
84+
options: {
85+
'_': { type: 'string[]', description: localize('prompt', "The prompt to use as chat.") },
86+
'mode': { type: 'string', cat: 'o', alias: 'm', args: 'agent|ask|edit', description: localize('chatMode', "The mode to use for the chat session. Defaults to 'agent'.") },
87+
'add-file': { type: 'string[]', cat: 'o', alias: 'a', description: localize('addFile', "Add files as context to the chat session.") },
88+
'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") }
89+
}
90+
},
8191

8292
'diff': { type: 'boolean', cat: 'o', alias: 'd', args: ['file', 'file'], description: localize('diff', "Compare two files with each other.") },
8393
'merge': { type: 'boolean', cat: 'o', alias: 'm', args: ['path1', 'path2', 'base', 'result'], description: localize('merge', "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.") },
@@ -428,20 +438,16 @@ function wrapText(text: string, columns: number): string[] {
428438
return lines;
429439
}
430440

431-
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, capabilities?: { noPipe?: boolean; noInputFiles: boolean }): string {
441+
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, capabilities?: { noPipe?: boolean; noInputFiles?: boolean; isChat?: boolean }): string {
432442
const columns = (process.stdout).isTTY && (process.stdout).columns || 80;
433-
const inputFiles = capabilities?.noInputFiles !== true ? `[${localize('paths', 'paths')}...]` : '';
443+
const inputFiles = capabilities?.noInputFiles ? '' : capabilities?.isChat ? ` [${localize('cliPrompt', 'prompt')}]` : ` [${localize('paths', 'paths')}...]`;
434444

435445
const help = [`${productName} ${version}`];
436446
help.push('');
437447
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}]${inputFiles}`);
438448
help.push('');
439449
if (capabilities?.noPipe !== true) {
440-
if (isWindows) {
441-
help.push(localize('stdinWindows', "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", executableName));
442-
} else {
443-
help.push(localize('stdinUnix', "To read from stdin, append '-' (e.g. 'ps aux | grep code | {0} -')", executableName));
444-
}
450+
help.push(buildStdinMessage(executableName, capabilities?.isChat));
445451
help.push('');
446452
}
447453
const optionsByCategory: { [P in keyof typeof helpCategories]?: OptionDescriptions<any> } = {};
@@ -481,6 +487,25 @@ export function buildHelpMessage(productName: string, executableName: string, ve
481487
return help.join('\n');
482488
}
483489

490+
export function buildStdinMessage(executableName: string, isChat?: boolean): string {
491+
let example: string;
492+
if (isWindows) {
493+
if (isChat) {
494+
example = `echo Hello World | ${executableName} chat <prompt> -`;
495+
} else {
496+
example = `echo Hello World | ${executableName} -`;
497+
}
498+
} else {
499+
if (isChat) {
500+
example = `ps aux | grep code | ${executableName} chat <prompt> -`;
501+
} else {
502+
example = `ps aux | grep code | ${executableName} -`;
503+
}
504+
}
505+
506+
return localize('stdinUsage', "To read from stdin, append '-' (e.g. '{0}')", example);
507+
}
508+
484509
export function buildVersionMessage(version: string | undefined, commit: string | undefined): string {
485510
return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`;
486511
}

src/vs/platform/windows/electron-main/windowsMainService.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
284284
// Bring window to front
285285
window.focus();
286286

287-
// Handle --wait
287+
// Handle `<app> --wait`
288288
this.handleWaitMarkerFile(openConfig, [window]);
289+
290+
// Handle `<app> chat`
291+
this.handleChatRequest(openConfig, [window]);
289292
}
290293

291294
async open(openConfig: IOpenConfiguration): Promise<ICodeWindow[]> {
@@ -443,9 +446,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
443446
this.workspacesHistoryMainService.addRecentlyOpened(recents);
444447
}
445448

446-
// Handle --wait
449+
// Handle `<app> --wait`
447450
this.handleWaitMarkerFile(openConfig, usedWindows);
448451

452+
// Handle `<app> chat`
453+
this.handleChatRequest(openConfig, usedWindows);
454+
449455
return usedWindows;
450456
}
451457

@@ -468,6 +474,27 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
468474
}
469475
}
470476

477+
private handleChatRequest(openConfig: IOpenConfiguration, usedWindows: ICodeWindow[]): void {
478+
if (openConfig.context !== OpenContext.CLI || !openConfig.cli.chat || usedWindows.length === 0) {
479+
return;
480+
}
481+
482+
let windowHandlingChatRequest: ICodeWindow | undefined;
483+
if (usedWindows.length === 1) {
484+
windowHandlingChatRequest = usedWindows[0];
485+
} else {
486+
const chatRequestFolder = openConfig.cli._[0]; // chat request gets cwd() as folder to open
487+
if (chatRequestFolder) {
488+
windowHandlingChatRequest = findWindowOnWorkspaceOrFolder(usedWindows, URI.file(chatRequestFolder));
489+
}
490+
}
491+
492+
if (windowHandlingChatRequest) {
493+
windowHandlingChatRequest.sendWhenReady('vscode:handleChatRequest', CancellationToken.None, openConfig.cli.chat);
494+
windowHandlingChatRequest.focus();
495+
}
496+
}
497+
471498
private async doOpen(
472499
openConfig: IOpenConfiguration,
473500
workspacesToOpen: IWorkspacePathToOpen[],

src/vs/server/node/remoteExtensionHostAgentCli.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ class CliMain extends Disposable {
6363
}
6464

6565
private registerListeners(): void {
66-
// Dispose on exit
67-
process.once('exit', () => this.dispose());
66+
process.once('exit', () => this.dispose()); // Dispose on exit
6867
}
6968

7069
async run(): Promise<void> {
@@ -190,13 +189,13 @@ export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string, op
190189
console.log(buildHelpMessage(product.nameLong, executable, product.version, optionDescriptions, { noInputFiles: true, noPipe: true }));
191190
return;
192191
}
192+
193193
// Version Info
194194
if (args.version) {
195195
console.log(buildVersionMessage(product.version, product.commit));
196196
return;
197197
}
198198

199-
200199
const cliMain = new CliMain(args, REMOTE_DATA_FOLDER);
201200
try {
202201
await cliMain.run();

src/vs/workbench/contrib/chat/browser/actions/chatActions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export interface IChatViewOpenOptions {
9595
* Whether a screenshot of the focused window should be taken and attached
9696
*/
9797
attachScreenshot?: boolean;
98+
/**
99+
* A list of file URIs to attach to the chat as context.
100+
*/
101+
attachFiles?: URI[];
98102
/**
99103
* The mode to open the chat in.
100104
*/
@@ -166,6 +170,11 @@ abstract class OpenChatGlobalAction extends Action2 {
166170
chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));
167171
}
168172
}
173+
if (opts?.attachFiles) {
174+
for (const file of opts.attachFiles) {
175+
chatWidget.attachmentModel.addFile(file);
176+
}
177+
}
169178
if (opts?.query) {
170179
if (opts.isPartialQuery) {
171180
chatWidget.setInput(opts.query);

src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { localize } from '../../../../nls.js';
67
import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForVoiceChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from './actions/voiceChatActions.js';
78
import { registerAction2 } from '../../../../platform/actions/common/actions.js';
89
import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
@@ -11,6 +12,17 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
1112
import { ILanguageModelToolsService } from '../common/languageModelToolsService.js';
1213
import { FetchWebPageTool, FetchWebPageToolData } from './tools/fetchPageTool.js';
1314
import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js';
15+
import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js';
16+
import { ICommandService } from '../../../../platform/commands/common/commands.js';
17+
import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js';
18+
import { ChatModeKind, validateChatMode } from '../common/constants.js';
19+
import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/globals.js';
20+
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
21+
import { URI } from '../../../../base/common/uri.js';
22+
import { resolve } from '../../../../base/common/path.js';
23+
import { showChatView } from '../browser/chat.js';
24+
import { IViewsService } from '../../../services/views/common/viewsService.js';
25+
import { ILogService } from '../../../../platform/log/common/log.js';
1426

1527
class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution {
1628

@@ -28,6 +40,56 @@ class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchCon
2840
}
2941
}
3042

43+
class ChatCommandLineHandler extends Disposable {
44+
45+
static readonly ID = 'workbench.contrib.chatCommandLineHandler';
46+
47+
constructor(
48+
@INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
49+
@ICommandService private readonly commandService: ICommandService,
50+
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
51+
@IViewsService private readonly viewsService: IViewsService,
52+
@ILogService private readonly logService: ILogService
53+
) {
54+
super();
55+
56+
this.registerListeners();
57+
}
58+
59+
private registerListeners() {
60+
ipcRenderer.on('vscode:handleChatRequest', (_, args: typeof this.environmentService.args.chat) => {
61+
this.logService.trace('vscode:handleChatRequest', args);
62+
63+
this.prompt(args);
64+
});
65+
}
66+
67+
private async prompt(args: typeof this.environmentService.args.chat): Promise<void> {
68+
if (!Array.isArray(args?._)) {
69+
return;
70+
}
71+
72+
const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({
73+
message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.")
74+
});
75+
76+
if (!trusted) {
77+
return;
78+
}
79+
80+
const opts: IChatViewOpenOptions = {
81+
query: args._.length > 0 ? args._.join(' ') : '',
82+
mode: validateChatMode(args.mode) ?? ChatModeKind.Agent,
83+
attachFiles: args['add-file']?.map(file => URI.file(resolve(file))), // use `resolve` to deal with relative paths properly
84+
};
85+
86+
const chatWidget = await showChatView(this.viewsService);
87+
await chatWidget?.waitForReady();
88+
await this.commandService.executeCommand(ACTION_ID_NEW_CHAT);
89+
await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, opts);
90+
}
91+
}
92+
3193
registerAction2(StartVoiceChatAction);
3294
registerAction2(InstallSpeechProviderForVoiceChatAction);
3395

@@ -47,3 +109,4 @@ registerChatDeveloperActions();
47109

48110
registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored);
49111
registerWorkbenchContribution2(NativeBuiltinToolsContribution.ID, NativeBuiltinToolsContribution, WorkbenchPhase.AfterRestored);
112+
registerWorkbenchContribution2(ChatCommandLineHandler.ID, ChatCommandLineHandler, WorkbenchPhase.BlockRestore);

0 commit comments

Comments
 (0)