Show chat agent hover in chat input editor (#214105)

* Show chat agent hover in chat input editor

* Clean up

* Comment
This commit is contained in:
Rob Lourens 2024-06-02 22:09:51 -07:00 committed by GitHub
parent acfe0e20ce
commit 403294d92b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 162 additions and 7 deletions

View file

@ -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';

View file

@ -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<void>());
public readonly onDidChangeContents: Event<void> = 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();
}
});
}

View file

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

View file

@ -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<ChatAgentHoverPart> {
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<ChatAgentHoverPart>,
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);

View file

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

View file

@ -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;
}

View file

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