Skip to content

Commit cf75ff5

Browse files
authored
'Remote coding agent' entrypoint in chat widget (#252363)
* initial frame * simple system working * wired up history * restore css * restore * comment * Add and implement ChatSummarizer from proposed. defaultChatParticipant.d.ts (microsoft/vscode-copilot#18919) * remove demo extension * tidy
1 parent 27267ee commit cf75ff5

File tree

13 files changed

+311
-1
lines changed

13 files changed

+311
-1
lines changed

src/vs/platform/extensions/common/extensionsApiProposals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ const _allApiProposals = {
305305
quickPickSortByLabel: {
306306
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts',
307307
},
308+
remoteCodingAgents: {
309+
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.remoteCodingAgents.d.ts',
310+
},
308311
resolvers: {
309312
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
310313
},

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
187187
provideChatTitle: (history, token) => {
188188
return this._proxy.$provideChatTitle(handle, history, token);
189189
},
190+
provideChatSummary: (history, token) => {
191+
return this._proxy.$provideChatSummary(handle, history, token);
192+
},
190193
};
191194

192195
let disposable: IDisposable;

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,7 @@ export interface ExtHostChatAgentsShape2 {
13641364
$acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void;
13651365
$invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]>;
13661366
$provideChatTitle(handle: number, context: IChatAgentHistoryEntryDto[], token: CancellationToken): Promise<string | undefined>;
1367+
$provideChatSummary(handle: number, context: IChatAgentHistoryEntryDto[], token: CancellationToken): Promise<string | undefined>;
13671368
$releaseSession(sessionId: string): void;
13681369
$detectChatParticipant(handle: number, request: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise<IChatParticipantDetectionResult | null | undefined>;
13691370
$provideRelatedFiles(handle: number, request: Dto<IChatRequestDraft>, token: CancellationToken): Promise<Dto<IChatRelatedFile>[] | undefined>;

src/vs/workbench/api/common/extHostChatAgents2.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,16 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
757757
const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context });
758758
return await agent.provideTitle({ history }, token);
759759
}
760+
761+
async $provideChatSummary(handle: number, context: IChatAgentHistoryEntryDto[], token: CancellationToken): Promise<string | undefined> {
762+
const agent = this._agents.get(handle);
763+
if (!agent) {
764+
return;
765+
}
766+
767+
const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context });
768+
return await agent.provideSummary({ history }, token);
769+
}
760770
}
761771

762772
class ExtHostParticipantDetector {
@@ -786,6 +796,7 @@ class ExtHostChatAgent {
786796
private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] };
787797
private _additionalWelcomeMessage?: string | vscode.MarkdownString | undefined;
788798
private _titleProvider?: vscode.ChatTitleProvider | undefined;
799+
private _summarizer?: vscode.ChatSummarizer | undefined;
789800
private _requester: vscode.ChatRequesterInformation | undefined;
790801
private _pauseStateEmitter = new Emitter<vscode.ChatParticipantPauseStateEvent>();
791802

@@ -841,6 +852,14 @@ class ExtHostChatAgent {
841852
return await this._titleProvider.provideChatTitle(context, token) ?? undefined;
842853
}
843854

855+
async provideSummary(context: vscode.ChatContext, token: CancellationToken): Promise<string | undefined> {
856+
if (!this._summarizer) {
857+
return;
858+
}
859+
860+
return await this._summarizer.provideChatSummary(context, token) ?? undefined;
861+
}
862+
844863
get apiAgent(): vscode.ChatParticipant {
845864
let disposed = false;
846865
let updateScheduled = false;
@@ -974,6 +993,14 @@ class ExtHostChatAgent {
974993
checkProposedApiEnabled(that.extension, 'defaultChatParticipant');
975994
return that._titleProvider;
976995
},
996+
set summarizer(v) {
997+
checkProposedApiEnabled(that.extension, 'defaultChatParticipant');
998+
that._summarizer = v;
999+
},
1000+
get summarizer() {
1001+
checkProposedApiEnabled(that.extension, 'defaultChatParticipant');
1002+
return that._summarizer;
1003+
},
9771004
get onDidChangePauseState() {
9781005
checkProposedApiEnabled(that.extension, 'chatParticipantAdditions');
9791006
return that._pauseStateEmitter.event;

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

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { basename } from '../../../../../base/common/resources.js';
7+
import { CancellationToken } from '../../../../../base/common/cancellation.js';
78
import { Codicon } from '../../../../../base/common/codicons.js';
89
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
910
import { ThemeIcon } from '../../../../../base/common/themables.js';
@@ -13,11 +14,13 @@ import { localize, localize2 } from '../../../../../nls.js';
1314
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
1415
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
1516
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
16-
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
17+
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
1718
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
1819
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1920
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
21+
import { IChatAgentService, IChatAgentHistoryEntry } from '../../common/chatAgents.js';
2022
import { ChatContextKeys } from '../../common/chatContextKeys.js';
23+
import { toChatHistoryContent } from '../../common/chatModel.js';
2124
import { ChatMode2, IChatMode, validateChatMode2 } from '../../common/chatModes.js';
2225
import { chatVariableLeader } from '../../common/chatParserTypes.js';
2326
import { IChatService } from '../../common/chatService.js';
@@ -28,6 +31,7 @@ import { IChatWidget, IChatWidgetService } from '../chat.js';
2831
import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js';
2932
import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js';
3033
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
34+
import { IRemoteCodingAgentsService } from '../../../remoteCodingAgents/common/remoteCodingAgentsService.js';
3135

3236
export interface IVoiceChatExecuteActionContext {
3337
readonly disableTimeout?: boolean;
@@ -454,6 +458,100 @@ class SubmitWithoutDispatchingAction extends Action2 {
454458
}
455459
}
456460

461+
export class CreateRemoteAgentJobAction extends Action2 {
462+
static readonly ID = 'workbench.action.chat.createRemoteAgentJob';
463+
464+
constructor() {
465+
const precondition = ContextKeyExpr.and(
466+
ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),
467+
whenNotInProgressOrPaused,
468+
ChatContextKeys.remoteJobCreating.negate(),
469+
);
470+
471+
super({
472+
id: CreateRemoteAgentJobAction.ID,
473+
title: localize2('actions.chat.createRemoteJob', "Create Remote Job"),
474+
icon: Codicon.cloudUpload,
475+
precondition,
476+
toggled: {
477+
condition: ChatContextKeys.remoteJobCreating,
478+
icon: Codicon.sync,
479+
tooltip: localize('remoteJobCreating', "Remote job is being created"),
480+
},
481+
menu: {
482+
id: MenuId.ChatExecute,
483+
group: 'navigation',
484+
order: 0,
485+
when: ChatContextKeys.hasRemoteCodingAgent
486+
}
487+
});
488+
}
489+
490+
async run(accessor: ServicesAccessor, ...args: any[]) {
491+
const contextKeyService = accessor.get(IContextKeyService);
492+
const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService);
493+
494+
try {
495+
remoteJobCreatingKey.set(true);
496+
497+
const remoteCodingAgent = accessor.get(IRemoteCodingAgentsService);
498+
const commandService = accessor.get(ICommandService);
499+
const widgetService = accessor.get(IChatWidgetService);
500+
const chatAgentService = accessor.get(IChatAgentService);
501+
502+
const widget = widgetService.lastFocusedWidget;
503+
if (!widget) {
504+
return;
505+
}
506+
const session = widget.viewModel?.sessionId;
507+
if (!session) {
508+
return;
509+
}
510+
511+
const userPrompt = widget.getInput();
512+
widget.setInput();
513+
514+
const chatModel = widget.viewModel?.model;
515+
const chatRequests = chatModel.getRequests();
516+
const agents = remoteCodingAgent.getRegisteredAgents();
517+
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel);
518+
519+
const agent = agents[0]; // TODO: We just pick the first one for testing
520+
if (!agent) {
521+
return;
522+
}
523+
524+
let summary: string | undefined;
525+
if (defaultAgent && chatRequests.length > 0) {
526+
const historyEntries: IChatAgentHistoryEntry[] = chatRequests
527+
.filter(req => req.response) // Only include completed requests
528+
.map(req => ({
529+
request: {
530+
sessionId: session,
531+
requestId: req.id,
532+
agentId: req.response?.agent?.id ?? '',
533+
message: req.message.text,
534+
command: req.response?.slashCommand?.name,
535+
variables: req.variableData,
536+
location: ChatAgentLocation.Panel,
537+
editedFileEvents: req.editedFileEvents,
538+
},
539+
response: toChatHistoryContent(req.response!.response.value),
540+
result: req.response?.result ?? {}
541+
}));
542+
543+
summary = await chatAgentService.getChatSummary(defaultAgent.id, historyEntries, CancellationToken.None);
544+
}
545+
await commandService.executeCommand(agent.command, {
546+
userPrompt,
547+
summary: summary || `Chat session with ${chatRequests.length} messages`
548+
});
549+
} finally {
550+
remoteJobCreatingKey.set(false);
551+
}
552+
}
553+
}
554+
457555
export class ChatSubmitWithCodebaseAction extends Action2 {
458556
static readonly ID = 'workbench.action.chat.submitWithCodebase';
459557

@@ -642,6 +740,7 @@ export function registerChatExecuteActions() {
642740
registerAction2(CancelAction);
643741
registerAction2(SendToNewChatAction);
644742
registerAction2(ChatSubmitWithCodebaseAction);
743+
registerAction2(CreateRemoteAgentJobAction);
645744
registerAction2(ToggleChatModeAction);
646745
registerAction2(ToggleRequestPausedAction);
647746
registerAction2(SwitchToNextModelAction);

src/vs/workbench/contrib/chat/common/chatAgents.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface IChatAgentImplementation {
7373
setRequestPaused?(requestId: string, isPaused: boolean): void;
7474
provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]>;
7575
provideChatTitle?: (history: IChatAgentHistoryEntry[], token: CancellationToken) => Promise<string | undefined>;
76+
provideChatSummary?: (history: IChatAgentHistoryEntry[], token: CancellationToken) => Promise<string | undefined>;
7677
}
7778

7879
export interface IChatParticipantDetectionResult {
@@ -195,6 +196,7 @@ export interface IChatAgentService {
195196
setRequestPaused(agent: string, requestId: string, isPaused: boolean): void;
196197
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]>;
197198
getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined>;
199+
getChatSummary(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined>;
198200
getAgent(id: string, includeDisabled?: boolean): IChatAgentData | undefined;
199201
getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined;
200202
getAgents(): IChatAgentData[];
@@ -502,6 +504,15 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
502504
return data.impl.provideChatTitle(history, token);
503505
}
504506

507+
async getChatSummary(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> {
508+
const data = this._agents.get(id);
509+
if (!data?.impl?.provideChatSummary) {
510+
return undefined;
511+
}
512+
513+
return data.impl.provideChatSummary(history, token);
514+
}
515+
505516
registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider) {
506517
this._chatParticipantDetectionProviders.set(handle, provider);
507518
return toDisposable(() => {

src/vs/workbench/contrib/chat/common/chatContextKeys.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export namespace ChatContextKeys {
5353

5454
export const languageModelsAreUserSelectable = new RawContextKey<boolean>('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") });
5555

56+
export const remoteJobCreating = new RawContextKey<boolean>('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") });
57+
export const hasRemoteCodingAgent = new RawContextKey<boolean>('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available"));
58+
5659
export const Setup = {
5760
hidden: new RawContextKey<boolean>('chatSetupHidden', false, true), // True when chat setup is explicitly hidden.
5861
installed: new RawContextKey<boolean>('chatSetupInstalled', false, true), // True when the chat extension is installed and enabled.

src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ suite('VoiceChat', () => {
8181
getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]> { throw new Error('Method not implemented.'); }
8282
agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); }
8383
getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }
84+
getChatSummary(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }
8485
hasToolsAgent: boolean = false;
8586
hasChatParticipantDetectionProviders(): boolean {
8687
throw new Error('Method not implemented.');
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 { Disposable } from '../../../../base/common/lifecycle.js';
7+
import { localize } from '../../../../nls.js';
8+
import { MenuRegistry } from '../../../../platform/actions/common/actions.js';
9+
import { ILogService } from '../../../../platform/log/common/log.js';
10+
import { Registry } from '../../../../platform/registry/common/platform.js';
11+
import { IWorkbenchContribution, Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js';
12+
import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
13+
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
14+
import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
15+
import { IRemoteCodingAgent, IRemoteCodingAgentsService } from '../common/remoteCodingAgentsService.js';
16+
17+
interface IRemoteCodingAgentExtensionPoint {
18+
id: string;
19+
command: string;
20+
displayName: string;
21+
description?: string;
22+
when?: string;
23+
}
24+
25+
const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IRemoteCodingAgentExtensionPoint[]>({
26+
extensionPoint: 'remoteCodingAgents',
27+
jsonSchema: {
28+
description: localize('remoteCodingAgentsExtPoint', 'Contributes remote coding agent integrations to the chat widget.'),
29+
type: 'array',
30+
items: {
31+
type: 'object',
32+
properties: {
33+
id: {
34+
description: localize('remoteCodingAgentsExtPoint.id', 'A unique identifier for this item.'),
35+
type: 'string',
36+
},
37+
command: {
38+
description: localize('remoteCodingAgentsExtPoint.command', 'Identifier of the command to execute. The command must be declared in the "commands" section.'),
39+
type: 'string'
40+
},
41+
displayName: {
42+
description: localize('remoteCodingAgentsExtPoint.displayName', 'A user-friendly name for this item which is used for display in menus.'),
43+
type: 'string'
44+
},
45+
description: {
46+
description: localize('remoteCodingAgentsExtPoint.description', 'Description of the remote agent for use in menus and tooltips.'),
47+
type: 'string'
48+
},
49+
when: {
50+
description: localize('remoteCodingAgentsExtPoint.when', 'Condition which must be true to show this item.'),
51+
type: 'string'
52+
},
53+
},
54+
required: ['command', 'displayName'],
55+
}
56+
}
57+
});
58+
59+
export class RemoteCodingAgentsContribution extends Disposable implements IWorkbenchContribution {
60+
constructor(
61+
@ILogService private readonly logService: ILogService,
62+
@IRemoteCodingAgentsService private readonly remoteCodingAgentsService: IRemoteCodingAgentsService
63+
) {
64+
super();
65+
extensionPoint.setHandler(extensions => {
66+
for (const ext of extensions) {
67+
if (!isProposedApiEnabled(ext.description, 'remoteCodingAgents')) {
68+
continue;
69+
}
70+
if (!Array.isArray(ext.value)) {
71+
continue;
72+
}
73+
for (const contribution of ext.value) {
74+
const command = MenuRegistry.getCommand(contribution.command);
75+
if (!command) {
76+
continue;
77+
}
78+
79+
// TODO: Handle 'when' clause
80+
81+
const agent: IRemoteCodingAgent = {
82+
id: contribution.id,
83+
command: contribution.command,
84+
displayName: contribution.displayName,
85+
description: contribution.description
86+
};
87+
this.logService.info(`Registering remote coding agent: ${agent.displayName} (${agent.command})`);
88+
this.remoteCodingAgentsService.registerAgent(agent);
89+
}
90+
}
91+
});
92+
}
93+
}
94+
95+
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
96+
workbenchRegistry.registerWorkbenchContribution(RemoteCodingAgentsContribution, LifecyclePhase.Restored);

0 commit comments

Comments
 (0)