From 93a13f64e1510aeb27995af6df388b4965e93fe5 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 18 Mar 2026 19:00:54 +0100 Subject: [PATCH] chat input: make slash command clickable (#302881) * chat input: make slash command clickable * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * update --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../input/editor/chatInputEditorContrib.ts | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts index 0bf73505e4e..d22c7463cf9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts @@ -8,7 +8,9 @@ import { Disposable, MutableDisposable } from '../../../../../../../base/common/ import { autorun } from '../../../../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../../../../base/common/themables.js'; import { URI } from '../../../../../../../base/common/uri.js'; +import { MouseTargetType } from '../../../../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../../../editor/common/editorCommon.js'; import { TrackedRangeStickiness } from '../../../../../../../editor/common/model.js'; @@ -17,6 +19,7 @@ import { ILabelService } from '../../../../../../../platform/label/common/label. import { inputPlaceholderForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../../../common/participants/chatAgents.js'; +import { localize } from '../../../../../../../nls.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../common/widget/chatColors.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../../../common/requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../../../../common/requestParser/chatRequestParser.js'; @@ -29,10 +32,12 @@ import { NativeEditContextRegistry } from '../../../../../../../editor/browser/c import { TextAreaEditContextRegistry } from '../../../../../../../editor/browser/controller/editContext/textArea/textAreaEditContextRegistry.js'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; import { ThrottledDelayer } from '../../../../../../../base/common/async.js'; +import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; const decorationDescription = 'chat'; const placeholderDecorationType = 'chat-session-detail'; const slashCommandTextDecorationType = 'chat-session-text'; +const clickableSlashPromptTextDecorationType = 'chat-session-clickable-text'; const variableTextDecorationType = 'chat-variable-text'; function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string { @@ -69,6 +74,8 @@ class InputEditorDecorations extends Disposable { public readonly id = 'inputEditorDecorations'; private readonly previouslyUsedAgents = new Set(); + private clickablePromptSlashCommand: { range: Range; uri: URI } | undefined; + private mouseDownPromptSlashCommand: { position: Position; uri: URI; range: Range } | undefined; private readonly viewModelDisposables = this._register(new MutableDisposable()); @@ -82,6 +89,7 @@ class InputEditorDecorations extends Disposable { @IChatAgentService private readonly chatAgentService: IChatAgentService, @ILabelService private readonly labelService: ILabelService, @IPromptsService private readonly promptsService: IPromptsService, + @IEditorService private readonly editorService: IEditorService, ) { super(); @@ -97,6 +105,38 @@ class InputEditorDecorations extends Disposable { this._register(this.widget.onDidSubmitAgent((e) => { this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name)); })); + this._register(this.widget.inputEditor.onMouseDown(e => { + this.mouseDownPromptSlashCommand = undefined; + + if (!e.event.leftButton || e.target.type !== MouseTargetType.CONTENT_TEXT || !e.target.position) { + return; + } + + const clickablePromptSlashCommand = this.clickablePromptSlashCommand; + if (!clickablePromptSlashCommand || !clickablePromptSlashCommand.range.containsPosition(e.target.position)) { + return; + } + + this.mouseDownPromptSlashCommand = { + position: Position.lift(e.target.position), + uri: clickablePromptSlashCommand.uri, + range: clickablePromptSlashCommand.range, + }; + })); + this._register(this.widget.inputEditor.onMouseUp(e => { + const mouseDownPromptSlashCommand = this.mouseDownPromptSlashCommand; + this.mouseDownPromptSlashCommand = undefined; + + if (!mouseDownPromptSlashCommand || e.target.type !== MouseTargetType.CONTENT_TEXT || !e.target.position) { + return; + } + + if (!mouseDownPromptSlashCommand.range.containsPosition(e.target.position) || !Position.equals(mouseDownPromptSlashCommand.position, e.target.position)) { + return; + } + + void this.editorService.openEditor({ resource: mouseDownPromptSlashCommand.uri }); + })); this._register(this.chatAgentService.onDidChangeAgents(() => this.triggerInputEditorDecorationsUpdate())); this._register(this.promptsService.onDidChangeSlashCommands(() => this.triggerInputEditorDecorationsUpdate())); this._register(autorun(reader => { @@ -128,6 +168,12 @@ class InputEditorDecorations extends Disposable { backgroundColor: themeColorFromId(chatSlashCommandBackground), borderRadius: '3px' })); + this._register(this.codeEditorService.registerDecorationType(decorationDescription, clickableSlashPromptTextDecorationType, { + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), + borderRadius: '3px', + cursor: 'pointer' + })); this._register(this.codeEditorService.registerDecorationType(decorationDescription, variableTextDecorationType, { color: themeColorFromId(chatSlashCommandForeground), backgroundColor: themeColorFromId(chatSlashCommandBackground), @@ -253,6 +299,8 @@ class InputEditorDecorations extends Disposable { } private async updateAsyncInputEditorDecorations(token: CancellationToken): Promise { + this.clickablePromptSlashCommand = undefined; + this.widget.inputEditor.setDecorationsByType(decorationDescription, clickableSlashPromptTextDecorationType, []); const parsedRequest = this.widget.parsedInput.parts; @@ -299,7 +347,21 @@ class InputEditorDecorations extends Disposable { } if (slashPromptPart && promptSlashCommand) { - textDecorations.push({ range: slashPromptPart.editorRange }); + this.clickablePromptSlashCommand = { + range: Range.lift(slashPromptPart.editorRange), + uri: promptSlashCommand.promptPath.uri, + }; + const promptHoverMessage = new MarkdownString(); + promptHoverMessage.appendText(localize( + 'chatInput.promptSlashCommand.open', + "Click to open {0}", + this.labelService.getUriLabel(promptSlashCommand.promptPath.uri, { relative: true }) + )); + const promptDecoration = { + range: slashPromptPart.editorRange, + hoverMessage: promptHoverMessage, + }; + this.widget.inputEditor.setDecorationsByType(decorationDescription, clickableSlashPromptTextDecorationType, [promptDecoration]); } this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations);