Merge pull request #210976 from microsoft/roblou/wonderful-carp

Add button to chat hover and make it disposable
This commit is contained in:
Rob Lourens 2024-04-22 21:17:36 -07:00 committed by GitHub
commit 3477776559
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 34 deletions

View file

@ -5,24 +5,38 @@
import * as dom from 'vs/base/browser/dom';
import { h } from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { verifiedPublisherIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
export class ChatAgentHover {
export class ChatAgentHover extends Disposable {
public readonly domNode: HTMLElement;
private readonly icon: HTMLElement;
private readonly name: HTMLElement;
private readonly extensionName: HTMLElement;
private readonly verifiedBadge: HTMLElement;
private readonly publisherName: HTMLElement;
private readonly description: HTMLElement;
private currentAgent: IChatAgentData | undefined;
constructor(
id: string,
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService,
@ICommandService private readonly commandService: ICommandService,
) {
const agent = this.chatAgentService.getAgent(id)!;
super();
const hoverElement = h(
'.chat-agent-hover@root',
@ -38,45 +52,75 @@ export class ChatAgentHover {
]),
]),
]),
h('.chat-agent-hover-description@description'),
h('span.chat-agent-hover-description@description'),
h('span.chat-agent-hover-marketplace-button@button'),
]);
this.domNode = hoverElement.root;
this.icon = hoverElement.icon;
this.name = hoverElement.name;
this.extensionName = hoverElement.extensionName;
this.description = hoverElement.description;
hoverElement.separator.textContent = '|';
this.verifiedBadge = dom.$('span.extension-verified-publisher', undefined, renderIcon(verifiedPublisherIcon));
this.verifiedBadge.style.display = 'none';
this.publisherName = dom.$('span.chat-agent-hover-publisher-name');
dom.append(
hoverElement.publisher,
this.verifiedBadge,
this.publisherName);
const label = localize('marketplaceLabel', "View in Marketplace") + '.';
const marketplaceButton = this._register(new Button(hoverElement.button, {
title: label,
buttonBackground: undefined,
buttonBorder: undefined,
buttonForeground: undefined,
buttonHoverBackground: undefined,
buttonSecondaryBackground: undefined,
buttonSecondaryForeground: undefined,
buttonSecondaryHoverBackground: undefined,
buttonSeparator: undefined,
}));
marketplaceButton.label = label;
this._register(marketplaceButton.onDidClick(() => {
if (this.currentAgent) {
this.commandService.executeCommand(showExtensionsWithIdsCommandId, [this.currentAgent.extensionId.value]);
}
}));
}
setAgent(id: string): void {
const agent = this.chatAgentService.getAgent(id)!;
this.currentAgent = agent;
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));
this.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));
this.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon));
}
hoverElement.name.textContent = `@${agent.name}`;
hoverElement.extensionName.textContent = agent.extensionDisplayName;
hoverElement.separator.textContent = '|';
const verifiedBadge = dom.$('span.extension-verified-publisher', undefined, renderIcon(verifiedPublisherIcon));
verifiedBadge.style.display = 'none';
dom.append(
hoverElement.publisher,
verifiedBadge,
agent.extensionPublisher);
this.name.textContent = `@${agent.name}`;
this.extensionName.textContent = agent.extensionDisplayName;
this.publisherName.textContent = agent.extensionPublisher;
const description = agent.description && !agent.description.endsWith('.') ?
`${agent.description}. ` :
(agent.description || '');
hoverElement.description.textContent = description;
this.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.extensionService.getExtensions([{ id: agent.extensionId.value }], CancellationToken.None).then(extensions => {
const cancel = this._register(new CancellationTokenSource());
this.extensionService.getExtensions([{ id: agent.extensionId.value }], cancel.token).then(extensions => {
cancel.dispose();
const extension = extensions[0];
if (extension?.publisherDomain?.verified) {
verifiedBadge.style.display = '';
this.verifiedBadge.style.display = '';
}
});
}

View file

@ -93,6 +93,7 @@ interface IChatListItemTemplate {
readonly contextKeyService: IContextKeyService;
readonly templateDisposables: IDisposable;
readonly elementDisposables: DisposableStore;
readonly agentHover: ChatAgentHover;
}
interface IItemHeightChangeParams {
@ -293,16 +294,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}));
}
const agentHover = templateDisposables.add(this.instantiationService.createInstance(ChatAgentHover));
templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), header, () => {
if (isResponseVM(template.currentElement) && template.currentElement.agent) {
const hover = this.instantiationService.createInstance(ChatAgentHover, template.currentElement.agent.id);
return hover.domNode;
agentHover.setAgent(template.currentElement.agent.id);
return agentHover.domNode;
}
return undefined;
}));
const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService };
const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, agentHover };
return template;
}

View file

@ -91,7 +91,8 @@ export class ChatMarkdownDecorationsRenderer {
const container = dom.$('span.chat-resource-widget', undefined, dom.$('span', undefined, name));
store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => {
const hover = this.instantiationService.createInstance(ChatAgentHover, id);
const hover = store.add(this.instantiationService.createInstance(ChatAgentHover));
hover.setAgent(id);
return hover.domNode;
}));
return container;

View file

@ -14,7 +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 'vs/css!./media/chatAgentHover';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { MenuId } from 'vs/platform/actions/common/actions';

View file

@ -51,6 +51,14 @@
opacity: 0.7;
}
.chat-agent-hover-description {
.chat-agent-hover-description,
.chat-agent-hover-marketplace-button .monaco-text-button {
font-size: 13px;
}
.chat-agent-hover .chat-agent-hover-marketplace-button .monaco-text-button {
display: unset;
padding: unset;
border: unset;
line-height: unset;
}

View file

@ -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)