Support chat participants in agent mode (#256113)

* Allow participants in any mode
Fix #255921

* Get rid of unneeded context keys

* Preserve modes for default agents

* Fix tests

* Fix test
This commit is contained in:
Rob Lourens 2025-07-15 15:37:20 -07:00 committed by GitHub
parent 9abb71c178
commit d8af289890
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 19 additions and 19 deletions

View File

@ -75,6 +75,9 @@
"name": "hello",
"description": "Hello"
}
],
"modes": [
"agent", "ask", "edit"
]
},
{

View File

@ -208,8 +208,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
metadata: revive(metadata),
slashCommands: [],
disambiguation: [],
locations: [ChatAgentLocation.Panel], // TODO all dynamic participants are panel only?
modes: [ChatModeKind.Ask]
locations: [ChatAgentLocation.Panel],
modes: [ChatModeKind.Ask, ChatModeKind.Agent, ChatModeKind.Edit],
},
impl);
} else {

View File

@ -155,10 +155,7 @@ abstract class OpenChatGlobalAction extends Action2 {
}
const switchToModeInput = opts?.mode ?? this.mode;
let switchToMode = switchToModeInput && (chatModeService.findModeById(switchToModeInput) ?? chatModeService.findModeByName(switchToModeInput));
if (!switchToMode) {
switchToMode = opts?.query?.startsWith('@') ? ChatMode.Ask : undefined;
}
const switchToMode = switchToModeInput && (chatModeService.findModeById(switchToModeInput) ?? chatModeService.findModeByName(switchToModeInput));
if (switchToMode) {
await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);
}

View File

@ -71,7 +71,7 @@ export function registerNewChatActions() {
title: localize2('chat.newEdits.label', "New Chat"),
category: CHAT_CATEGORY,
icon: Codicon.plus,
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
precondition: ContextKeyExpr.and(ChatContextKeys.enabled),
f1: true,
menu: [{
id: MenuId.ChatContext,
@ -142,7 +142,7 @@ export function registerNewChatActions() {
title: localize2('chat.undoEdit.label', "Undo Last Request"),
category: CHAT_CATEGORY,
icon: Codicon.discard,
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanUndo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanUndo, ChatContextKeys.enabled),
f1: true,
menu: [{
id: MenuId.ViewTitle,
@ -166,7 +166,7 @@ export function registerNewChatActions() {
title: localize2('chat.redoEdit.label', "Redo Checkpoint Restore"),
category: CHAT_CATEGORY,
icon: Codicon.redo,
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled),
f1: true,
menu: [
{
@ -191,7 +191,7 @@ export function registerNewChatActions() {
id: 'workbench.action.chat.redoEdit2',
title: localize2('chat.redoEdit.label2', "Redo Checkpoint Restore"),
category: CHAT_CATEGORY,
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled),
f1: true,
menu: [{
id: MenuId.ChatMessageRestoreCheckpoint,

View File

@ -247,6 +247,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
continue;
}
if (providerDescriptor.isDefault && !providerDescriptor.modes?.length) {
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register default participant without modes.`);
continue;
}
if (providerDescriptor.locations && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) {
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`);
continue;
@ -291,7 +296,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
locations: isNonEmptyArray(providerDescriptor.locations) ?
providerDescriptor.locations.map(ChatAgentLocation.fromRaw) :
[ChatAgentLocation.Panel],
modes: providerDescriptor.modes ?? [ChatModeKind.Ask],
modes: providerDescriptor.isDefault ? providerDescriptor.modes! : [ChatModeKind.Agent, ChatModeKind.Ask, ChatModeKind.Edit],
slashCommands: providerDescriptor.commands ?? [],
disambiguation: coalesce(participantsDisambiguation.flat()),
} satisfies IChatAgentData));

View File

@ -59,6 +59,7 @@ export interface IChatAgentData {
metadata: IChatAgentMetadata;
slashCommands: IChatAgentCommand[];
locations: ChatAgentLocation[];
/** This is only relevant for isDefault agents. Others should have all modes available. */
modes: ChatModeKind[];
disambiguation: { category: string; description: string; examples: string[] }[];
}
@ -232,7 +233,6 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
private readonly _hasDefaultAgent: IContextKey<boolean>;
private readonly _extensionAgentRegistered: IContextKey<boolean>;
private readonly _defaultAgentRegistered: IContextKey<boolean>;
private readonly _editingAgentRegistered: IContextKey<boolean>;
private _hasToolsAgent = false;
private _chatParticipantDetectionProviders = new Map<number, IChatParticipantDetectionProvider>();
@ -245,7 +245,6 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
this._hasDefaultAgent = ChatContextKeys.enabled.bindTo(this.contextKeyService);
this._extensionAgentRegistered = ChatContextKeys.extensionParticipantRegistered.bindTo(this.contextKeyService);
this._defaultAgentRegistered = ChatContextKeys.panelParticipantRegistered.bindTo(this.contextKeyService);
this._editingAgentRegistered = ChatContextKeys.editingParticipantRegistered.bindTo(this.contextKeyService);
this._register(contextKeyService.onDidChangeContext((e) => {
if (e.affectsSome(this._agentsContextKeys)) {
this._updateContextKeys();
@ -295,7 +294,6 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
}
private _updateContextKeys(): void {
let editingAgentRegistered = false;
let extensionAgentRegistered = false;
let defaultAgentRegistered = false;
let toolsAgentRegistered = false;
@ -304,16 +302,14 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
if (!agent.isCore) {
extensionAgentRegistered = true;
}
if (agent.modes.includes(ChatModeKind.Agent)) {
if (agent.id === 'chat.setup' || agent.id === 'github.copilot.editsAgent') {
// TODO@roblourens firing the event below probably isn't necessary but leave it alone for now
toolsAgentRegistered = true;
} else if (agent.modes.includes(ChatModeKind.Edit)) {
editingAgentRegistered = true;
} else {
defaultAgentRegistered = true;
}
}
}
this._editingAgentRegistered.set(editingAgentRegistered);
this._defaultAgentRegistered.set(defaultAgentRegistered);
this._extensionAgentRegistered.set(extensionAgentRegistered);
if (toolsAgentRegistered !== this._hasToolsAgent) {

View File

@ -42,7 +42,6 @@ export namespace ChatContextKeys {
export const extensionParticipantRegistered = new RawContextKey<boolean>('chatPanelExtensionParticipantRegistered', false, { type: 'boolean', description: localize('chatPanelExtensionParticipantRegistered', "True when a default chat participant is registered for the panel from an extension.") });
export const panelParticipantRegistered = new RawContextKey<boolean>('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") });
export const editingParticipantRegistered = new RawContextKey<boolean>('chatEditingParticipantRegistered', false, { type: 'boolean', description: localize('chatEditingParticipantRegistered', "True when a default chat participant is registered for editing.") });
export const chatEditingCanUndo = new RawContextKey<boolean>('chatEditingCanUndo', false, { type: 'boolean', description: localize('chatEditingCanUndo', "True when it is possible to undo an interaction in the editing panel.") });
export const chatEditingCanRedo = new RawContextKey<boolean>('chatEditingCanRedo', false, { type: 'boolean', description: localize('chatEditingCanRedo', "True when it is possible to redo an interaction in the editing panel.") });
export const extensionInvalid = new RawContextKey<boolean>('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") });