Skip to content

Commit 8b8d3e6

Browse files
committed
Agent hover basically works
1 parent 98785f5 commit 8b8d3e6

File tree

7 files changed

+130
-11
lines changed

7 files changed

+130
-11
lines changed

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

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,47 @@ import { ILogService } from 'vs/platform/log/common/log';
1414
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
1515
import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
1616
import { contentRefUrl } from '../common/annotations';
17+
import { IHoverService } from 'vs/platform/hover/browser/hover';
18+
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
19+
import { h } from 'vs/base/browser/dom';
20+
import { FileAccess } from 'vs/base/common/network';
21+
import { ThemeIcon } from 'vs/base/common/themables';
22+
import { localize } from 'vs/nls';
23+
import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
1724

1825
const variableRefUrl = 'http://_vscodedecoration_';
26+
const agentRefUrl = 'http://_chatagent_';
1927

2028
export class ChatMarkdownDecorationsRenderer {
2129
constructor(
2230
@IKeybindingService private readonly keybindingService: IKeybindingService,
2331
@ILabelService private readonly labelService: ILabelService,
2432
@ILogService private readonly logService: ILogService,
2533
@IChatAgentService private readonly chatAgentService: IChatAgentService,
34+
@IHoverService private readonly hoverService: IHoverService,
2635
) { }
2736

2837
convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string {
2938
let result = '';
3039
for (const part of parsedRequest.parts) {
3140
if (part instanceof ChatRequestTextPart) {
3241
result += part.text;
42+
} else if (part instanceof ChatRequestAgentPart) {
43+
let text = part.text;
44+
const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1;
45+
if (isDupe) {
46+
text += ` (${part.agent.extensionPublisher})`;
47+
}
48+
49+
result += `[${text}](${agentRefUrl}?${encodeURIComponent(part.agent.id)})`;
3350
} else {
3451
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
3552
|| undefined;
3653
const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) :
3754
part instanceof ChatRequestAgentPart ? part.agent.id :
3855
'';
3956

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-
57+
const text = part.text;
4858
result += `[${text}](${variableRefUrl}?${title})`;
4959
}
5060
}
@@ -56,7 +66,12 @@ export class ChatMarkdownDecorationsRenderer {
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),
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),
@@ -70,6 +85,58 @@ export class ChatMarkdownDecorationsRenderer {
7085
});
7186
}
7287

88+
private renderAgentWidget(name: string, id: string): HTMLElement {
89+
const agent = this.chatAgentService.getAgent(id)!;
90+
91+
const container = dom.$('span.chat-resource-widget');
92+
const alias = dom.$('span', undefined, name);
93+
94+
const hoverElement = h(
95+
'.chat-agent-hover@root',
96+
[
97+
h('.chat-agent-hover-header', [
98+
h('.chat-agent-hover-icon@icon'),
99+
h('.chat-agent-hover-details', [
100+
h('.chat-agent-hover-name@name'),
101+
h('.chat-agent-hover-extension', [
102+
h('.chat-agent-hover-extension-name@extensionName'),
103+
h('.chat-agent-hover-separator@separator'),
104+
h('.chat-agent-hover-publisher@publisher'),
105+
]),
106+
]),
107+
]),
108+
h('.chat-agent-hover-description@description'),
109+
]);
110+
111+
if (agent.metadata.icon instanceof URI) {
112+
const avatarIcon = dom.$<HTMLImageElement>('img.icon');
113+
avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true);
114+
hoverElement.icon.replaceChildren(dom.$('.avatar', undefined, avatarIcon));
115+
} else if (agent.metadata.themeIcon) {
116+
const avatarIcon = dom.$(ThemeIcon.asCSSSelector(agent.metadata.themeIcon));
117+
hoverElement.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon));
118+
}
119+
120+
hoverElement.name.textContent = `@${agent.name}`;
121+
hoverElement.extensionName.textContent = agent.extensionDisplayName;
122+
hoverElement.separator.textContent = ' | ';
123+
hoverElement.publisher.textContent = agent.extensionPublisher;
124+
125+
const description = agent.description && !agent.description.endsWith('.') ?
126+
`${agent.description}. ` :
127+
(agent.description || '');
128+
hoverElement.description.textContent = description;
129+
130+
const marketplaceLink = document.createElement('a');
131+
marketplaceLink.setAttribute('href', `command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([agent.extensionId.value]))}`);
132+
marketplaceLink.textContent = localize('marketplaceLabel', "View in Marketplace") + '.';
133+
hoverElement.description.appendChild(marketplaceLink);
134+
135+
this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, hoverElement.root);
136+
container.appendChild(alias);
137+
return container;
138+
}
139+
73140
private renderFileWidget(href: string, a: HTMLAnchorElement): void {
74141
// TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now.
75142
const fullUri = URI.parse(href);

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 {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
.chat-agent-hover {
7+
line-height: unset;
8+
padding: 6px 0px;
9+
}
10+
11+
.chat-agent-hover-header {
12+
display: flex;
13+
gap: 5px;
14+
}
15+
16+
.chat-agent-hover-icon img,
17+
.chat-agent-hover-icon .codicon {
18+
width: 32px;
19+
height: 32px;
20+
border-radius: 50%;
21+
outline: 1px solid var(--vscode-chat-requestBorder);
22+
}
23+
24+
.chat-agent-hover-icon .codicon {
25+
font-size: 23px;
26+
display: flex;
27+
justify-content: center;
28+
align-items: center;
29+
}
30+
31+
.chat-agent-hover-header .chat-agent-hover-name {
32+
font-size: 15px;
33+
font-weight: 600;
34+
}
35+
36+
.chat-agent-hover-extension {
37+
display: flex;
38+
}
39+
40+
.chat-agent-hover-separator {
41+
opacity: 0.7;
42+
margin: 0px 6px;
43+
}
44+
45+
.chat-agent-hover-description {
46+
font-size: 13px;
47+
}

src/vs/workbench/contrib/chat/common/chatAgents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface IChatAgentData {
5252
description?: string;
5353
extensionId: ExtensionIdentifier;
5454
extensionPublisher: string;
55+
extensionDisplayName: string;
5556
/** The agent invoked when no agent is specified */
5657
isDefault?: boolean;
5758
metadata: IChatAgentMetadata;
@@ -317,6 +318,7 @@ export class MergedChatAgent implements IChatAgent {
317318
get description(): string { return this.data.description ?? ''; }
318319
get extensionId(): ExtensionIdentifier { return this.data.extensionId; }
319320
get extensionPublisher(): string { return this.data.extensionPublisher; }
321+
get extensionDisplayName(): string { return this.data.extensionDisplayName; }
320322
get isDefault(): boolean | undefined { return this.data.isDefault; }
321323
get metadata(): IChatAgentMetadata { return this.data.metadata; }
322324
get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; }

src/vs/workbench/contrib/extensions/browser/extensionsActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2975,7 +2975,8 @@ CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForL
29752975
});
29762976
});
29772977

2978-
CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWithIds', function (accessor: ServicesAccessor, extensionIds: string[]) {
2978+
export const showExtensionsWithIdsCommandId = 'workbench.extensions.action.showExtensionsWithIds';
2979+
CommandsRegistry.registerCommand(showExtensionsWithIdsCommandId, function (accessor: ServicesAccessor, extensionIds: string[]) {
29792980
const paneCompositeService = accessor.get(IPaneCompositePartService);
29802981

29812982
return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true)

0 commit comments

Comments
 (0)