diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7d45fc86091..44064deca56 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -43,6 +43,7 @@ import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/b import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover'; 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'; diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 9163fd24fdb..9e48ba6bc18 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -9,6 +9,7 @@ import { IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -29,6 +30,9 @@ export class ChatAgentHover extends Disposable { private readonly publisherName: HTMLElement; private readonly description: HTMLElement; + private readonly _onDidChangeContents = this._register(new Emitter()); + public readonly onDidChangeContents: Event = this._onDidChangeContents.event; + constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, @@ -110,6 +114,7 @@ export class ChatAgentHover extends Disposable { const extension = extensions[0]; if (extension?.publisherDomain?.verified) { this.domNode.classList.toggle('verifiedPublisher', true); + this._onDidChangeContents.fire(); } }); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index bafc22d77ee..e58d0fd8294 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -197,10 +197,7 @@ class InputEditorDecorations extends Disposable { const textDecorations: IDecorationOptions[] | undefined = []; if (agentPart) { - const isDupe = !!this.chatAgentService.getAgents().find(other => other.name === agentPart.agent.name && other.id !== agentPart.agent.id); - const publisher = isDupe ? `(${agentPart.agent.publisherDisplayName}) ` : ''; - const agentHover = `${publisher}${agentPart.agent.description}`; - textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) }); + textDecorations.push({ range: agentPart.editorRange }); if (agentSubcommandPart) { textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts new file mode 100644 index 00000000000..2ad7122eb92 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatEditorHoverWrapper } from 'vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper'; +import { IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; + +export class ChatAgentHoverParticipant implements IEditorHoverParticipant { + + public readonly hoverOrdinal: number = 1; + + constructor( + private readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + ) { } + + public computeSync(anchor: HoverAnchor, _lineDecorations: IModelDecoration[]): ChatAgentHoverPart[] { + if (!this.editor.hasModel()) { + return []; + } + + const widget = this.chatWidgetService.getWidgetByInputUri(this.editor.getModel().uri); + if (!widget) { + return []; + } + + const { agentPart } = extractAgentAndCommand(widget.parsedInput); + if (!agentPart) { + return []; + } + + if (Range.containsPosition(agentPart.editorRange, anchor.range.getStartPosition())) { + return [new ChatAgentHoverPart(this, Range.lift(agentPart.editorRange), agentPart.agent)]; + } + + return []; + } + + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ChatAgentHoverPart[]): IDisposable { + if (!hoverParts.length) { + return Disposable.None; + } + + const store = new DisposableStore(); + const hover = store.add(this.instantiationService.createInstance(ChatAgentHover)); + store.add(hover.onDidChangeContents(() => context.onContentsChanged())); + const agent = hoverParts[0].agent; + hover.setAgent(agent.id); + + const actions = getChatAgentHoverOptions(() => agent, this.commandService).actions; + const wrapper = this.instantiationService.createInstance(ChatEditorHoverWrapper, hover.domNode, actions); + context.fragment.appendChild(wrapper.domNode); + + return store; + } +} + +export class ChatAgentHoverPart implements IHoverPart { + + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range, + public readonly agent: IChatAgentData + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +HoverParticipantRegistry.register(ChatAgentHoverParticipant); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts new file mode 100644 index 00000000000..5cbc7d932cc --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/editorHoverWrapper'; +import * as dom from 'vs/base/browser/dom'; +import { IHoverAction } from 'vs/base/browser/ui/hover/hover'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +const $ = dom.$; +const h = dom.h; + +/** + * This borrows some of HoverWidget so that a chat editor hover can be rendered in the same way as a workbench hover. + * Maybe it can be reusable in a generic way. + */ +export class ChatEditorHoverWrapper { + public readonly domNode: HTMLElement; + + constructor( + hoverContentElement: HTMLElement, + actions: IHoverAction[] | undefined, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + const hoverElement = h( + '.chat-editor-hover-wrapper@root', + [h('.chat-editor-hover-wrapper-content@content')]); + this.domNode = hoverElement.root; + hoverElement.content.appendChild(hoverContentElement); + + if (actions && actions.length > 0) { + const statusBarElement = $('.hover-row.status-bar'); + const actionsElement = $('.actions'); + actions.forEach(action => { + const keybinding = this.keybindingService.lookupKeybinding(action.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + HoverAction.render(actionsElement, { + label: action.label, + commandId: action.commandId, + run: e => { + action.run(e); + }, + iconClass: action.iconClass + }, keybindingLabel); + }); + statusBarElement.appendChild(actionsElement); + this.domNode.appendChild(statusBarElement); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css new file mode 100644 index 00000000000..d95fd395255 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-editor-hover-wrapper-content { + padding: 2px 8px; +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 1599c4ffeac..29e38f48cad 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -22,8 +22,8 @@ outline: 1px solid var(--vscode-chat-requestBorder); } -.monaco-hover .markdown-hover .hover-contents .chat-agent-hover-icon .codicon { - font-size: 23px; +.chat-agent-hover .chat-agent-hover-icon .codicon { + font-size: 23px !important; /* Override workbench hover styles */ display: flex; justify-content: center; align-items: center; @@ -34,7 +34,7 @@ gap: 4px; } -.monaco-hover .chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { +.chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { color: var(--vscode-extensionIcon-verifiedForeground); } @@ -60,6 +60,10 @@ font-weight: 600; } +.chat-agent-hover-header .chat-agent-hover-details { + font-size: 12px; +} + .chat-agent-hover-extension { display: flex; gap: 6px;