Skip to content

Commit

Permalink
Support agent hover and proper rendering in /help (#212311)
Browse files Browse the repository at this point in the history
  • Loading branch information
roblourens committed May 9, 2024
1 parent 9cb0df2 commit 5f78b58
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 40 deletions.
24 changes: 10 additions & 14 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isMacintosh } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
Expand All @@ -17,11 +18,12 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions';
import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor';
import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp';
import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions';
import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions';
import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions';
import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions';
import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport';
import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions';
Expand All @@ -31,14 +33,17 @@ import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatW
import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService';
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions';
import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick';
import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView';
import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables';
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService';
import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables';
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
import { ChatAgentLocation, ChatAgentService, IChatAgentService, IChatAgentNameService, ChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
Expand All @@ -50,10 +55,6 @@ import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/c
import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import '../common/chatColors';
import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions';
import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry';
import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView';
import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp';

// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
Expand Down Expand Up @@ -182,16 +183,11 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
.filter(a => a.id !== defaultAgent?.id)
.filter(a => a.locations.includes(ChatAgentLocation.Panel))
.map(async a => {
const agentWithLeader = `${chatAgentLeader}${a.name}`;
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
const description = a.description ? `- ${a.description}` : '';
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) ${description}`;
const agentLine = `- ${agentToMarkdown(a, true, chatAgentService)} ${description}`;
const commandText = a.slashCommands.map(c => {
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
const description = c.description ? `- ${c.description}` : '';
return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) ${description}`;
return `\t* ${agentSlashCommandToMarkdown(a, c, chatAgentService)} ${description}`;
}).join('\n');

return (agentLine + '\n' + commandText).trim();
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/chatAgentHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class ChatAgentHover extends Disposable {

let description = agent.description ?? '';
if (description) {
if (!description.match(/\. *$/)) {
if (!description.match(/[\.\?\!] *$/)) {
description += '.';
}
}
Expand Down
146 changes: 123 additions & 23 deletions src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,52 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILabelService } from 'vs/platform/label/common/label';
import { ILogService } from 'vs/platform/log/common/log';
import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { contentRefUrl } from '../common/annotations';
import { IHoverService } from 'vs/platform/hover/browser/hover';
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
import { Button } from 'vs/base/browser/ui/button/button';
import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { asCssVariable } from 'vs/platform/theme/common/colorUtils';

/** For rendering slash commands, variables */
const decorationRefUrl = `http://_vscodedecoration_`;

/** For rendering agent decorations with hover */
const agentRefUrl = `http://_chatagent_`;

/** For rendering agent decorations with hover */
const agentSlashRefUrl = `http://_chatslash_`;

export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, chatAgentService: IChatAgentService): string {
let text = `${chatAgentLeader}${agent.name}`;
const isDupe = agent && chatAgentService.getAgentsByName(agent.name).length > 1;
if (isDupe) {
text += ` (${agent.publisherDisplayName})`;
}

const args: IAgentWidgetArgs = { agentId: agent.id, isClickable };
return `[${text}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`;
}

const variableRefUrl = 'http://_vscodedecoration_';
const agentRefUrl = 'http://_chatagent_';
interface IAgentWidgetArgs {
agentId: string;
isClickable?: boolean;
}

export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand, chatAgentService: IChatAgentService): string {
const text = `${chatSubcommandLeader}${command.name}`;
const args: ISlashCommandWidgetArgs = { agentId: agent.id, command: command.name };
return `[${text}](${agentSlashRefUrl}?${encodeURIComponent(JSON.stringify(args))})`;
}

interface ISlashCommandWidgetArgs {
agentId: string;
command: string;
}

export class ChatMarkdownDecorationsRenderer {
constructor(
Expand All @@ -31,6 +69,8 @@ export class ChatMarkdownDecorationsRenderer {
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IHoverService private readonly hoverService: IHoverService,
@IChatService private readonly chatService: IChatService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
) { }

convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string {
Expand All @@ -39,13 +79,7 @@ export class ChatMarkdownDecorationsRenderer {
if (part instanceof ChatRequestTextPart) {
result += part.text;
} else if (part instanceof ChatRequestAgentPart) {
let text = part.text;
const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1;
if (isDupe) {
text += ` (${part.agent.publisherDisplayName})`;
}

result += `[${text}](${agentRefUrl}?${encodeURIComponent(part.agent.id)})`;
result += agentToMarkdown(part.agent, false, this.chatAgentService);
} else {
const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ?
part.data :
Expand All @@ -55,7 +89,7 @@ export class ChatMarkdownDecorationsRenderer {
'';

const text = part.text;
result += `[${text}](${variableRefUrl}?${title})`;
result += `[${text}](${decorationRefUrl}?${title})`;
}
}

Expand All @@ -68,12 +102,33 @@ export class ChatMarkdownDecorationsRenderer {
const href = a.getAttribute('data-href');
if (href) {
if (href.startsWith(agentRefUrl)) {
const title = decodeURIComponent(href.slice(agentRefUrl.length + 1));
a.parentElement!.replaceChild(
this.renderAgentWidget(a.textContent!, title, store),
a);
} else if (href.startsWith(variableRefUrl)) {
const title = decodeURIComponent(href.slice(variableRefUrl.length + 1));
let args: IAgentWidgetArgs | undefined;
try {
args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1)));
} catch (e) {
this.logService.error('Invalid chat widget render data JSON', toErrorMessage(e));
}

if (args) {
a.parentElement!.replaceChild(
this.renderAgentWidget(a.textContent!, args, store),
a);
}
} else if (href.startsWith(agentSlashRefUrl)) {
let args: ISlashCommandWidgetArgs | undefined;
try {
args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1)));
} catch (e) {
this.logService.error('Invalid chat slash command render data JSON', toErrorMessage(e));
}

if (args) {
a.parentElement!.replaceChild(
this.renderSlashCommandWidget(a.textContent!, args, store),
a);
}
} else if (href.startsWith(decorationRefUrl)) {
const title = decodeURIComponent(href.slice(decorationRefUrl.length + 1));
a.parentElement!.replaceChild(
this.renderResourceWidget(a.textContent!, title),
a);
Expand All @@ -88,17 +143,59 @@ export class ChatMarkdownDecorationsRenderer {
return store;
}

private renderAgentWidget(name: string, id: string, store: DisposableStore): HTMLElement {
const container = dom.$('span.chat-resource-widget', undefined, dom.$('span', undefined, name));
private renderAgentWidget(name: string, args: IAgentWidgetArgs, store: DisposableStore): HTMLElement {
let container: HTMLElement;
if (args.isClickable) {
container = dom.$('span.chat-agent-widget');
const agent = this.chatAgentService.getAgent(args.agentId);
const button = store.add(new Button(container, {
buttonBackground: asCssVariable(chatSlashCommandBackground),
buttonForeground: asCssVariable(chatSlashCommandForeground),
buttonHoverBackground: undefined
}));
button.label = name;
store.add(button.onDidClick(() => {
const widget = this.chatWidgetService.lastFocusedWidget;
if (!widget || !agent) {
return;
}

this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id });
}));
} else {
container = this.renderResourceWidget(name, undefined);
}

store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => {
const hover = store.add(this.instantiationService.createInstance(ChatAgentHover));
hover.setAgent(id);
hover.setAgent(args.agentId);
return hover.domNode;
}));
return container;
}

private renderSlashCommandWidget(name: string, args: ISlashCommandWidgetArgs, store: DisposableStore): HTMLElement {
const container = dom.$('span.chat-agent-widget.chat-command-widget');
const agent = this.chatAgentService.getAgent(args.agentId);
const button = store.add(new Button(container, {
buttonBackground: asCssVariable(chatSlashCommandBackground),
buttonForeground: asCssVariable(chatSlashCommandForeground),
buttonHoverBackground: undefined
}));
button.label = name;
store.add(button.onDidClick(() => {
const widget = this.chatWidgetService.lastFocusedWidget;
if (!widget || !agent) {
return;
}

const command = agent.slashCommands.find(c => c.name === args.command);
this.chatService.sendRequest(widget.viewModel!.sessionId, command?.sampleRequest ?? '', { location: widget.location, agentId: agent.id, slashCommand: args.command });
}));

return container;
}

private renderFileWidget(href: string, a: HTMLAnchorElement): void {
// TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now.
const fullUri = URI.parse(href);
Expand All @@ -125,10 +222,13 @@ export class ChatMarkdownDecorationsRenderer {
}


private renderResourceWidget(name: string, title: string): HTMLElement {
private renderResourceWidget(name: string, title: string | undefined): HTMLElement {
const container = dom.$('span.chat-resource-widget');
const alias = dom.$('span', undefined, name);
alias.title = title;
if (title) {
alias.title = title;
}

container.appendChild(alias);
return container;
}
Expand Down
9 changes: 9 additions & 0 deletions src/vs/workbench/contrib/chat/browser/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -606,11 +606,20 @@
.interactive-item-container .chat-resource-widget {
background-color: var(--vscode-chat-slashCommandBackground);
color: var(--vscode-chat-slashCommandForeground);
}

.interactive-item-container .chat-resource-widget,
.interactive-item-container .chat-agent-widget .monaco-button {
border-radius: 4px;
white-space: nowrap;
padding: 1px 3px;
}

.interactive-item-container .chat-agent-widget .monaco-text-button {
display: inline;
border: none;
}

.interactive-session .chat-used-context.chat-used-context-collapsed .chat-used-context-list {
display: none;
}
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/chat/common/chatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export interface IChatSendRequestOptions {

/** The target agent ID can be specified with this property instead of using @ in 'message' */
agentId?: string;
slashCommand?: string;
}

export const IChatService = createDecorator<IChatService>('IChatService');
Expand Down
5 changes: 3 additions & 2 deletions src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
Expand Down Expand Up @@ -485,7 +485,8 @@ export class ChatService extends Disposable implements IChatService {
throw new Error(`Unknown agent: ${options.agentId}`);
}
parserContext = { selectedAgent: agent };
request = `${chatAgentLeader}${agent.name} ${request}`;
const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : '';
request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`;
}

const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext);
Expand Down

0 comments on commit 5f78b58

Please sign in to comment.