Agent hover basically works

This commit is contained in:
Rob Lourens
2024-04-15 16:27:46 -07:00
parent 98785f537e
commit 8b8d3e64b5
7 changed files with 130 additions and 11 deletions
@@ -14,8 +14,16 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, 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 { h } from 'vs/base/browser/dom';
import { FileAccess } from 'vs/base/common/network';
import { ThemeIcon } from 'vs/base/common/themables';
import { localize } from 'vs/nls';
import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
const variableRefUrl = 'http://_vscodedecoration_';
const agentRefUrl = 'http://_chatagent_';
export class ChatMarkdownDecorationsRenderer {
constructor(
@@ -23,6 +31,7 @@ export class ChatMarkdownDecorationsRenderer {
@ILabelService private readonly labelService: ILabelService,
@ILogService private readonly logService: ILogService,
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IHoverService private readonly hoverService: IHoverService,
) { }
convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string {
@@ -30,6 +39,14 @@ export class ChatMarkdownDecorationsRenderer {
for (const part of parsedRequest.parts) {
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.extensionPublisher})`;
}
result += `[${text}](${agentRefUrl}?${encodeURIComponent(part.agent.id)})`;
} else {
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
|| undefined;
@@ -37,14 +54,7 @@ export class ChatMarkdownDecorationsRenderer {
part instanceof ChatRequestAgentPart ? part.agent.id :
'';
let text = part.text;
if (part instanceof ChatRequestAgentPart) {
const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1;
if (isDupe) {
text += ` (${part.agent.extensionPublisher})`;
}
}
const text = part.text;
result += `[${text}](${variableRefUrl}?${title})`;
}
}
@@ -56,7 +66,12 @@ export class ChatMarkdownDecorationsRenderer {
element.querySelectorAll('a').forEach(a => {
const href = a.getAttribute('data-href');
if (href) {
if (href.startsWith(variableRefUrl)) {
if (href.startsWith(agentRefUrl)) {
const title = decodeURIComponent(href.slice(agentRefUrl.length + 1));
a.parentElement!.replaceChild(
this.renderAgentWidget(a.textContent!, title),
a);
} else if (href.startsWith(variableRefUrl)) {
const title = decodeURIComponent(href.slice(variableRefUrl.length + 1));
a.parentElement!.replaceChild(
this.renderResourceWidget(a.textContent!, title),
@@ -70,6 +85,58 @@ export class ChatMarkdownDecorationsRenderer {
});
}
private renderAgentWidget(name: string, id: string): HTMLElement {
const agent = this.chatAgentService.getAgent(id)!;
const container = dom.$('span.chat-resource-widget');
const alias = dom.$('span', undefined, name);
const hoverElement = h(
'.chat-agent-hover@root',
[
h('.chat-agent-hover-header', [
h('.chat-agent-hover-icon@icon'),
h('.chat-agent-hover-details', [
h('.chat-agent-hover-name@name'),
h('.chat-agent-hover-extension', [
h('.chat-agent-hover-extension-name@extensionName'),
h('.chat-agent-hover-separator@separator'),
h('.chat-agent-hover-publisher@publisher'),
]),
]),
]),
h('.chat-agent-hover-description@description'),
]);
if (agent.metadata.icon instanceof URI) {
const avatarIcon = dom.$<HTMLImageElement>('img.icon');
avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true);
hoverElement.icon.replaceChildren(dom.$('.avatar', undefined, avatarIcon));
} else if (agent.metadata.themeIcon) {
const avatarIcon = dom.$(ThemeIcon.asCSSSelector(agent.metadata.themeIcon));
hoverElement.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon));
}
hoverElement.name.textContent = `@${agent.name}`;
hoverElement.extensionName.textContent = agent.extensionDisplayName;
hoverElement.separator.textContent = ' | ';
hoverElement.publisher.textContent = agent.extensionPublisher;
const description = agent.description && !agent.description.endsWith('.') ?
`${agent.description}. ` :
(agent.description || '');
hoverElement.description.textContent = description;
const marketplaceLink = document.createElement('a');
marketplaceLink.setAttribute('href', `command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([agent.extensionId.value]))}`);
marketplaceLink.textContent = localize('marketplaceLabel', "View in Marketplace") + '.';
hoverElement.description.appendChild(marketplaceLink);
this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, hoverElement.root);
container.appendChild(alias);
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);
@@ -203,6 +203,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
{
extensionId: extension.description.identifier,
extensionPublisher: extension.description.publisherDisplayName ?? extension.description.publisher, // May not be present in OSS
extensionDisplayName: extension.description.displayName ?? extension.description.name, // ?
id: providerDescriptor.id,
description: providerDescriptor.description,
metadata: {
@@ -14,6 +14,7 @@ import { isEqual } from 'vs/base/common/resources';
import { isDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/chat';
import 'vs/css!./media/chatHover';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { MenuId } from 'vs/platform/actions/common/actions';
@@ -101,7 +101,7 @@
width: 24px;
height: 24px;
border-radius: 50%;
outline: 1px solid var(--vscode-chat-requestBorder)
outline: 1px solid var(--vscode-chat-requestBorder);
}
.interactive-item-container .header .avatar.codicon-avatar {
@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.chat-agent-hover {
line-height: unset;
padding: 6px 0px;
}
.chat-agent-hover-header {
display: flex;
gap: 5px;
}
.chat-agent-hover-icon img,
.chat-agent-hover-icon .codicon {
width: 32px;
height: 32px;
border-radius: 50%;
outline: 1px solid var(--vscode-chat-requestBorder);
}
.chat-agent-hover-icon .codicon {
font-size: 23px;
display: flex;
justify-content: center;
align-items: center;
}
.chat-agent-hover-header .chat-agent-hover-name {
font-size: 15px;
font-weight: 600;
}
.chat-agent-hover-extension {
display: flex;
}
.chat-agent-hover-separator {
opacity: 0.7;
margin: 0px 6px;
}
.chat-agent-hover-description {
font-size: 13px;
}
@@ -52,6 +52,7 @@ export interface IChatAgentData {
description?: string;
extensionId: ExtensionIdentifier;
extensionPublisher: string;
extensionDisplayName: string;
/** The agent invoked when no agent is specified */
isDefault?: boolean;
metadata: IChatAgentMetadata;
@@ -317,6 +318,7 @@ export class MergedChatAgent implements IChatAgent {
get description(): string { return this.data.description ?? ''; }
get extensionId(): ExtensionIdentifier { return this.data.extensionId; }
get extensionPublisher(): string { return this.data.extensionPublisher; }
get extensionDisplayName(): string { return this.data.extensionDisplayName; }
get isDefault(): boolean | undefined { return this.data.isDefault; }
get metadata(): IChatAgentMetadata { return this.data.metadata; }
get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; }
@@ -2975,7 +2975,8 @@ CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForL
});
});
CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWithIds', function (accessor: ServicesAccessor, extensionIds: string[]) {
export const showExtensionsWithIdsCommandId = 'workbench.extensions.action.showExtensionsWithIds';
CommandsRegistry.registerCommand(showExtensionsWithIdsCommandId, function (accessor: ServicesAccessor, extensionIds: string[]) {
const paneCompositeService = accessor.get(IPaneCompositePartService);
return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true)