Skip to content

Commit 6294089

Browse files
authored
Merge pull request microsoft#210488 from microsoft/roblou/chat-agent-hover
Add a nicer hover for chat participants
2 parents f09f068 + db8ce77 commit 6294089

25 files changed

+225
-27
lines changed

build/hygiene.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ function createGitIndexVinyls(paths) {
256256

257257
cp.exec(
258258
process.platform === 'win32' ? `git show :${relativePath}` : `git show ':${relativePath}'`,
259-
{ maxBuffer: 2000 * 1024, encoding: 'buffer' },
259+
{ maxBuffer: stat.size, encoding: 'buffer' },
260260
(err, out) => {
261261
if (err) {
262262
return e(err);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
137137
name: dynamicProps.name,
138138
description: dynamicProps.description,
139139
extensionId: extension,
140+
extensionDisplayName: extensionDescription?.displayName ?? extension.value,
140141
extensionPublisher: extensionDescription?.publisherDisplayName ?? extension.value,
141142
metadata: revive(metadata),
142143
slashCommands: [],
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 * as dom from 'vs/base/browser/dom';
7+
import { h } from 'vs/base/browser/dom';
8+
import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels';
9+
import { CancellationToken } from 'vs/base/common/cancellation';
10+
import { FileAccess } from 'vs/base/common/network';
11+
import { ThemeIcon } from 'vs/base/common/themables';
12+
import { URI } from 'vs/base/common/uri';
13+
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
14+
import { verifiedPublisherIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
15+
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
16+
17+
export class ChatAgentHover {
18+
public readonly domNode: HTMLElement;
19+
20+
constructor(
21+
id: string,
22+
@IChatAgentService private readonly chatAgentService: IChatAgentService,
23+
@IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService,
24+
) {
25+
const agent = this.chatAgentService.getAgent(id)!;
26+
27+
const hoverElement = h(
28+
'.chat-agent-hover@root',
29+
[
30+
h('.chat-agent-hover-header', [
31+
h('.chat-agent-hover-icon@icon'),
32+
h('.chat-agent-hover-details', [
33+
h('.chat-agent-hover-name@name'),
34+
h('.chat-agent-hover-extension', [
35+
h('.chat-agent-hover-extension-name@extensionName'),
36+
h('.chat-agent-hover-separator@separator'),
37+
h('.chat-agent-hover-publisher@publisher'),
38+
]),
39+
]),
40+
]),
41+
h('.chat-agent-hover-description@description'),
42+
]);
43+
this.domNode = hoverElement.root;
44+
45+
if (agent.metadata.icon instanceof URI) {
46+
const avatarIcon = dom.$<HTMLImageElement>('img.icon');
47+
avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true);
48+
hoverElement.icon.replaceChildren(dom.$('.avatar', undefined, avatarIcon));
49+
} else if (agent.metadata.themeIcon) {
50+
const avatarIcon = dom.$(ThemeIcon.asCSSSelector(agent.metadata.themeIcon));
51+
hoverElement.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon));
52+
}
53+
54+
hoverElement.name.textContent = `@${agent.name}`;
55+
hoverElement.extensionName.textContent = agent.extensionDisplayName;
56+
hoverElement.separator.textContent = '|';
57+
58+
const verifiedBadge = dom.$('span.extension-verified-publisher', undefined, renderIcon(verifiedPublisherIcon));
59+
verifiedBadge.style.display = 'none';
60+
dom.append(
61+
hoverElement.publisher,
62+
verifiedBadge,
63+
agent.extensionPublisher);
64+
65+
66+
const description = agent.description && !agent.description.endsWith('.') ?
67+
`${agent.description}. ` :
68+
(agent.description || '');
69+
hoverElement.description.textContent = description;
70+
71+
// const marketplaceLink = document.createElement('a');
72+
// marketplaceLink.setAttribute('href', `command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([agent.extensionId.value]))}`);
73+
// marketplaceLink.textContent = localize('marketplaceLabel', "View in Marketplace") + '.';
74+
// hoverElement.description.appendChild(marketplaceLink);
75+
76+
this.extensionService.getExtensions([{ id: agent.extensionId.value }], CancellationToken.None).then(extensions => {
77+
const extension = extensions[0];
78+
if (extension?.publisherDomain?.verified) {
79+
verifiedBadge.style.display = '';
80+
}
81+
});
82+
}
83+
}

src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom';
77
import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
88
import { alert } from 'vs/base/browser/ui/aria/aria';
99
import { Button } from 'vs/base/browser/ui/button/button';
10+
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
1011
import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels';
1112
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
1213
import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
@@ -16,6 +17,7 @@ import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/t
1617
import { IAction } from 'vs/base/common/actions';
1718
import { distinct } from 'vs/base/common/arrays';
1819
import { disposableTimeout } from 'vs/base/common/async';
20+
import { CancellationTokenSource } from 'vs/base/common/cancellation';
1921
import { Codicon } from 'vs/base/common/codicons';
2022
import { Emitter, Event } from 'vs/base/common/event';
2123
import { FuzzyScore } from 'vs/base/common/filters';
@@ -24,13 +26,18 @@ import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } fr
2426
import { ResourceMap } from 'vs/base/common/map';
2527
import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network';
2628
import { clamp } from 'vs/base/common/numbers';
29+
import { IObservable, autorun, constObservable } from 'vs/base/common/observable';
2730
import { basename } from 'vs/base/common/path';
2831
import { basenameOrAuthority } from 'vs/base/common/resources';
2932
import { equalsIgnoreCase } from 'vs/base/common/strings';
3033
import { ThemeIcon } from 'vs/base/common/themables';
34+
import { isUndefined } from 'vs/base/common/types';
3135
import { URI } from 'vs/base/common/uri';
3236
import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer';
3337
import { Range } from 'vs/editor/common/core/range';
38+
import { TextEdit } from 'vs/editor/common/languages';
39+
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
40+
import { IModelService } from 'vs/editor/common/services/model';
3441
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
3542
import { localize } from 'vs/nls';
3643
import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
@@ -40,6 +47,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
4047
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
4148
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
4249
import { FileKind, FileType } from 'vs/platform/files/common/files';
50+
import { IHoverService } from 'vs/platform/hover/browser/hover';
4351
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
4452
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
4553
import { WorkbenchCompressibleAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService';
@@ -50,6 +58,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme';
5058
import { IThemeService } from 'vs/platform/theme/common/themeService';
5159
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
5260
import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat';
61+
import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover';
5362
import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
5463
import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
5564
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
@@ -66,17 +75,12 @@ import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/f
6675
import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files';
6776
import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations';
6877
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection';
69-
import { IModelService } from 'vs/editor/common/services/model';
70-
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
71-
import { TextEdit } from 'vs/editor/common/languages';
7278
import { IChatListItemRendererOptions } from './chat';
73-
import { CancellationTokenSource } from 'vs/base/common/cancellation';
74-
import { autorun, constObservable, IObservable } from 'vs/base/common/observable';
75-
import { isUndefined } from 'vs/base/common/types';
7679

7780
const $ = dom.$;
7881

7982
interface IChatListItemTemplate {
83+
currentElement?: ChatTreeItem;
8084
readonly rowContainer: HTMLElement;
8185
readonly titleToolbar?: MenuWorkbenchToolBar;
8286
readonly avatarContainer: HTMLElement;
@@ -148,6 +152,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
148152
@ICommandService private readonly commandService: ICommandService,
149153
@ITextModelService private readonly textModelService: ITextModelService,
150154
@IModelService private readonly modelService: IModelService,
155+
@IHoverService private readonly hoverService: IHoverService,
151156
@IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService,
152157
) {
153158
super();
@@ -286,6 +291,16 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
286291
}
287292
}));
288293
}
294+
295+
templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), header, () => {
296+
if (isResponseVM(template.currentElement) && template.currentElement.agent) {
297+
const hover = this.instantiationService.createInstance(ChatAgentHover, template.currentElement.agent.id);
298+
return hover.domNode;
299+
}
300+
301+
return undefined;
302+
}));
303+
289304
const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService };
290305
return template;
291306
}
@@ -295,6 +310,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
295310
}
296311

297312
renderChatTreeItem(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate): void {
313+
templateData.currentElement = element;
298314
const kind = isRequestVM(element) ? 'request' :
299315
isResponseVM(element) ? 'response' :
300316
'welcome';
@@ -416,11 +432,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
416432
}
417433

418434
templateData.detail.textContent = progressMsg;
419-
if (element.agent) {
420-
templateData.detail.title = progressMsg + (element.slashCommand?.description ? `\n${element.slashCommand.description}` : '');
421-
} else {
422-
templateData.detail.title = '';
423-
}
424435
}
425436

426437
private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void {
@@ -1038,7 +1049,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
10381049
disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id)));
10391050
}
10401051

1041-
this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element);
1052+
disposables.add(this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element));
10421053

10431054
orderedDisposablesList.reverse().forEach(d => disposables.add(d));
10441055
return {

src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,73 @@
55

66
import * as dom from 'vs/base/browser/dom';
77
import { toErrorMessage } from 'vs/base/common/errorMessage';
8+
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
89
import { revive } from 'vs/base/common/marshalling';
910
import { URI } from 'vs/base/common/uri';
1011
import { Location } from 'vs/editor/common/languages';
12+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1113
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1214
import { ILabelService } from 'vs/platform/label/common/label';
1315
import { ILogService } from 'vs/platform/log/common/log';
16+
import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover';
1417
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
1518
import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
1619
import { contentRefUrl } from '../common/annotations';
20+
import { IHoverService } from 'vs/platform/hover/browser/hover';
21+
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
1722

1823
const variableRefUrl = 'http://_vscodedecoration_';
24+
const agentRefUrl = 'http://_chatagent_';
1925

2026
export class ChatMarkdownDecorationsRenderer {
2127
constructor(
2228
@IKeybindingService private readonly keybindingService: IKeybindingService,
2329
@ILabelService private readonly labelService: ILabelService,
2430
@ILogService private readonly logService: ILogService,
2531
@IChatAgentService private readonly chatAgentService: IChatAgentService,
32+
@IInstantiationService private readonly instantiationService: IInstantiationService,
33+
@IHoverService private readonly hoverService: IHoverService,
2634
) { }
2735

2836
convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string {
2937
let result = '';
3038
for (const part of parsedRequest.parts) {
3139
if (part instanceof ChatRequestTextPart) {
3240
result += part.text;
41+
} else if (part instanceof ChatRequestAgentPart) {
42+
let text = part.text;
43+
const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1;
44+
if (isDupe) {
45+
text += ` (${part.agent.extensionPublisher})`;
46+
}
47+
48+
result += `[${text}](${agentRefUrl}?${encodeURIComponent(part.agent.id)})`;
3349
} else {
3450
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
3551
|| undefined;
3652
const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) :
3753
part instanceof ChatRequestAgentPart ? part.agent.id :
3854
'';
3955

40-
let text = part.text;
41-
if (part instanceof ChatRequestAgentPart) {
42-
const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1;
43-
if (isDupe) {
44-
text += ` (${part.agent.extensionPublisher})`;
45-
}
46-
}
47-
56+
const text = part.text;
4857
result += `[${text}](${variableRefUrl}?${title})`;
4958
}
5059
}
5160

5261
return result;
5362
}
5463

55-
walkTreeAndAnnotateReferenceLinks(element: HTMLElement): void {
64+
walkTreeAndAnnotateReferenceLinks(element: HTMLElement): IDisposable {
65+
const store = new DisposableStore();
5666
element.querySelectorAll('a').forEach(a => {
5767
const href = a.getAttribute('data-href');
5868
if (href) {
59-
if (href.startsWith(variableRefUrl)) {
69+
if (href.startsWith(agentRefUrl)) {
70+
const title = decodeURIComponent(href.slice(agentRefUrl.length + 1));
71+
a.parentElement!.replaceChild(
72+
this.renderAgentWidget(a.textContent!, title, store),
73+
a);
74+
} else if (href.startsWith(variableRefUrl)) {
6075
const title = decodeURIComponent(href.slice(variableRefUrl.length + 1));
6176
a.parentElement!.replaceChild(
6277
this.renderResourceWidget(a.textContent!, title),
@@ -68,6 +83,18 @@ export class ChatMarkdownDecorationsRenderer {
6883
}
6984
}
7085
});
86+
87+
return store;
88+
}
89+
90+
private renderAgentWidget(name: string, id: string, store: DisposableStore): HTMLElement {
91+
const container = dom.$('span.chat-resource-widget', undefined, dom.$('span', undefined, name));
92+
93+
store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => {
94+
const hover = this.instantiationService.createInstance(ChatAgentHover, id);
95+
return hover.domNode;
96+
}));
97+
return container;
7198
}
7299

73100
private renderFileWidget(href: string, a: HTMLAnchorElement): void {

src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
203203
{
204204
extensionId: extension.description.identifier,
205205
extensionPublisher: extension.description.publisherDisplayName ?? extension.description.publisher, // May not be present in OSS
206+
extensionDisplayName: extension.description.displayName ?? extension.description.name,
206207
id: providerDescriptor.id,
207208
description: providerDescriptor.description,
208209
metadata: {

src/vs/workbench/contrib/chat/browser/chatWidget.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isEqual } from 'vs/base/common/resources';
1414
import { isDefined } from 'vs/base/common/types';
1515
import { URI } from 'vs/base/common/uri';
1616
import 'vs/css!./media/chat';
17+
import 'vs/css!./media/chatHover';
1718
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1819
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
1920
import { MenuId } from 'vs/platform/actions/common/actions';

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
width: 24px;
102102
height: 24px;
103103
border-radius: 50%;
104-
outline: 1px solid var(--vscode-chat-requestBorder)
104+
outline: 1px solid var(--vscode-chat-requestBorder);
105105
}
106106

107107
.interactive-item-container .header .avatar.codicon-avatar {

0 commit comments

Comments
 (0)