From eb3ef0c7b9f88794fa025fb2c5bb9d7670878f12 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 4 Jan 2026 08:29:17 -0800 Subject: [PATCH 1/4] Add selection to chat action on terminal selection --- .../contrib/terminal/terminal.all.ts | 1 + .../media/terminalSelectionDecoration.css | 32 ++++ ...rminal.selectionDecoration.contribution.ts | 165 ++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css create mode 100644 src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts diff --git a/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index 6f08c6293479b..feba214ee511a 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -28,6 +28,7 @@ import '../terminalContrib/zoom/browser/terminal.zoom.contribution.js'; import '../terminalContrib/stickyScroll/browser/terminal.stickyScroll.contribution.js'; import '../terminalContrib/quickAccess/browser/terminal.quickAccess.contribution.js'; import '../terminalContrib/quickFix/browser/terminal.quickFix.contribution.js'; +import '../terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.js'; import '../terminalContrib/typeAhead/browser/terminal.typeAhead.contribution.js'; import '../terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.js'; import '../terminalContrib/sendSequence/browser/terminal.sendSequence.contribution.js'; diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css new file mode 100644 index 0000000000000..6930b06cea15c --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.terminal-selection-decoration { + z-index: 10; + pointer-events: auto !important; +} + +.terminal-selection-decoration .terminal-selection-attach-button { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 4px; + cursor: pointer; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + font-size: 14px; + transition: background-color 0.1s ease-in-out; + transform: translateY(-100%); +} + +.terminal-selection-decoration .terminal-selection-attach-button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.terminal-selection-decoration .terminal-selection-attach-button:active { + background-color: var(--vscode-button-activeBackground, var(--vscode-button-hoverBackground)); +} diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts new file mode 100644 index 0000000000000..d0a6b4642fd12 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IDecoration, Terminal as RawXtermTerminal } from '@xterm/xterm'; +import * as dom from '../../../../../base/browser/dom.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; +import { IChatWidgetService } from '../../../chat/browser/chat.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; +import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; +import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; +import { IChatRequestStringVariableEntry } from '../../../chat/common/attachments/chatVariableEntries.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import './media/terminalSelectionDecoration.css'; + +class TerminalSelectionDecorationContribution extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.selectionDecoration'; + + static get(instance: ITerminalInstance): TerminalSelectionDecorationContribution | null { + return instance.getContribution(TerminalSelectionDecorationContribution.ID); + } + + private _xterm: IXtermTerminal & { raw: RawXtermTerminal } | undefined; + private readonly _decoration = this._register(new MutableDisposable()); + private readonly _decorationListeners = this._register(new DisposableStore()); + private readonly _showDecorationScheduler: RunOnceScheduler; + + constructor( + _ctx: ITerminalContributionContext | IDetachedCompatibleTerminalContributionContext, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + ) { + super(); + this._showDecorationScheduler = this._register(new RunOnceScheduler(() => this._showDecoration(), 200)); + } + + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + this._xterm = xterm; + this._register(xterm.raw.onSelectionChange(() => this._onSelectionChange())); + } + + private _onSelectionChange(): void { + // Clear decoration immediately when selection changes + this._decoration.clear(); + this._decorationListeners.clear(); + + // Only schedule showing the decoration if there's a selection + if (this._xterm?.raw.hasSelection()) { + this._showDecorationScheduler.schedule(); + } else { + this._showDecorationScheduler.cancel(); + } + } + + private _showDecoration(): void { + if (!this._xterm) { + return; + } + + // Only show if there's a selection and chat supports terminal attachments + if (!this._xterm.raw.hasSelection()) { + return; + } + + const chatIsEnabled = this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).some(w => w.attachmentCapabilities.supportsTerminalAttachments); + if (!chatIsEnabled) { + return; + } + + const selectionPosition = this._xterm.raw.getSelectionPosition(); + if (!selectionPosition) { + return; + } + + // Create a marker at the start of the selection + const marker = this._xterm.raw.registerMarker(selectionPosition.start.y - (this._xterm.raw.buffer.active.baseY + this._xterm.raw.buffer.active.cursorY)); + if (!marker) { + return; + } + + // Register the decoration + const decoration = this._xterm.raw.registerDecoration({ + marker, + x: selectionPosition.start.x, + layer: 'top' + }); + + if (!decoration) { + marker.dispose(); + return; + } + + this._decoration.value = decoration; + + this._decorationListeners.add(decoration.onRender(element => { + if (!element.classList.contains('terminal-selection-decoration')) { + this._setupDecorationElement(element); + } + })); + } + + private _setupDecorationElement(element: HTMLElement): void { + element.classList.add('terminal-selection-decoration'); + + // Create the attach button + const button = dom.append(element, dom.$('.terminal-selection-attach-button')); + button.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkle)); + button.title = localize('terminal.attachSelectionToChat', "Attach Selection to Chat"); + + this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.CLICK, async (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + await this._attachSelectionToChat(); + })); + + this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.MOUSE_DOWN, (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + })); + } + + private async _attachSelectionToChat(): Promise { + if (!this._xterm?.raw.hasSelection()) { + return; + } + + const selection = this._xterm.raw.getSelection(); + if (!selection) { + return; + } + + let widget = this._chatWidgetService.lastFocusedWidget ?? this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)?.find(w => w.attachmentCapabilities.supportsTerminalAttachments); + + if (!widget) { + widget = await this._chatWidgetService.revealWidget(); + } + + if (!widget || !widget.attachmentCapabilities.supportsTerminalAttachments) { + return; + } + + // Clear the selection after attaching + const selectionText = selection; + this._xterm.raw.clearSelection(); + + // Attach the selection as a string attachment + const attachment: IChatRequestStringVariableEntry = { + kind: 'string', + id: `terminalSelection:${Date.now()}`, + name: localize('terminal.selection', "Terminal Selection"), + value: selectionText, + icon: Codicon.terminal, + uri: URI.parse(`terminal-selection:${Date.now()}`), + }; + + widget.attachmentModel.addContext(attachment); + widget.focusInput(); + } +} + +registerTerminalContribution(TerminalSelectionDecorationContribution.ID, TerminalSelectionDecorationContribution, true); From 85bca08866b13b685ac333466f91d4bd181ed01c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 4 Jan 2026 08:38:40 -0800 Subject: [PATCH 2/4] Integrate with menu system --- src/vs/platform/actions/common/actions.ts | 1 + .../media/terminalSelectionDecoration.css | 8 +- ...rminal.selectionDecoration.contribution.ts | 104 +++++++++++++----- 3 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 09782cdf11673..b065ab2c13eb8 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -234,6 +234,7 @@ export class MenuId { static readonly TerminalTabContext = new MenuId('TerminalTabContext'); static readonly TerminalTabEmptyAreaContext = new MenuId('TerminalTabEmptyAreaContext'); static readonly TerminalStickyScrollContext = new MenuId('TerminalStickyScrollContext'); + static readonly TerminalSelectionContext = new MenuId('TerminalSelectionContext'); static readonly WebviewContext = new MenuId('WebviewContext'); static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); static readonly InlineEditsActions = new MenuId('InlineEditsActions'); diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css index 6930b06cea15c..f224b7acc6025 100644 --- a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css @@ -8,7 +8,7 @@ pointer-events: auto !important; } -.terminal-selection-decoration .terminal-selection-attach-button { +.terminal-selection-decoration .terminal-selection-action-button { display: flex; align-items: center; justify-content: center; @@ -23,10 +23,10 @@ transform: translateY(-100%); } -.terminal-selection-decoration .terminal-selection-attach-button:hover { +.terminal-selection-decoration .terminal-selection-action-button:hover { background-color: var(--vscode-button-hoverBackground); } -.terminal-selection-decoration .terminal-selection-attach-button:active { - background-color: var(--vscode-button-activeBackground, var(--vscode-button-hoverBackground)); +.terminal-selection-decoration .terminal-selection-action-button:active { + background-color: var(--vscode-button-hoverBackground); } diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts index d0a6b4642fd12..dafa1f3386dc8 100644 --- a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts @@ -7,17 +7,25 @@ import type { IDecoration, Terminal as RawXtermTerminal } from '@xterm/xterm'; import * as dom from '../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { localize } from '../../../../../nls.js'; -import { IChatWidgetService } from '../../../chat/browser/chat.js'; -import { ChatAgentLocation } from '../../../chat/common/constants.js'; +import { localize, localize2 } from '../../../../../nls.js'; import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { IMenu, IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; +import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; +import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js'; +import { IChatWidgetService } from '../../../chat/browser/chat.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { IChatRequestStringVariableEntry } from '../../../chat/common/attachments/chatVariableEntries.js'; import { URI } from '../../../../../base/common/uri.js'; -import { RunOnceScheduler } from '../../../../../base/common/async.js'; import './media/terminalSelectionDecoration.css'; +// #region Terminal Contribution + class TerminalSelectionDecorationContribution extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.selectionDecoration'; @@ -29,13 +37,17 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer private readonly _decoration = this._register(new MutableDisposable()); private readonly _decorationListeners = this._register(new DisposableStore()); private readonly _showDecorationScheduler: RunOnceScheduler; + private readonly _menu: IMenu; constructor( _ctx: ITerminalContributionContext | IDetachedCompatibleTerminalContributionContext, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, ) { super(); this._showDecorationScheduler = this._register(new RunOnceScheduler(() => this._showDecoration(), 200)); + this._menu = this._register(menuService.createMenu(MenuId.TerminalSelectionContext, contextKeyService)); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -44,6 +56,7 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer } private _onSelectionChange(): void { + // TODO: Upstream to allow listening while selection is in progress // Clear decoration immediately when selection changes this._decoration.clear(); this._decorationListeners.clear(); @@ -61,13 +74,14 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer return; } - // Only show if there's a selection and chat supports terminal attachments + // Only show if there's a selection if (!this._xterm.raw.hasSelection()) { return; } - const chatIsEnabled = this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).some(w => w.attachmentCapabilities.supportsTerminalAttachments); - if (!chatIsEnabled) { + // Check if menu has any actions + const actions = getFlatContextMenuActions(this._menu.getActions({ shouldForwardArgs: true })); + if (actions.length === 0) { return; } @@ -106,15 +120,14 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer private _setupDecorationElement(element: HTMLElement): void { element.classList.add('terminal-selection-decoration'); - // Create the attach button - const button = dom.append(element, dom.$('.terminal-selection-attach-button')); - button.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkle)); - button.title = localize('terminal.attachSelectionToChat', "Attach Selection to Chat"); + // Create the action button + const button = dom.append(element, dom.$('.terminal-selection-action-button')); + button.classList.add('codicon', 'codicon-sparkle'); - this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.CLICK, async (e) => { + this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.CLICK, (e) => { e.stopImmediatePropagation(); e.preventDefault(); - await this._attachSelectionToChat(); + this._showContextMenu(e, button); })); this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.MOUSE_DOWN, (e) => { @@ -123,20 +136,53 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer })); } - private async _attachSelectionToChat(): Promise { - if (!this._xterm?.raw.hasSelection()) { + private _showContextMenu(e: MouseEvent, anchor: HTMLElement): void { + const actions = getFlatContextMenuActions(this._menu.getActions({ shouldForwardArgs: true })); + if (actions.length === 0) { return; } - const selection = this._xterm.raw.getSelection(); + // If only one action, run it directly + if (actions.length === 1) { + actions[0].run(); + return; + } + + const standardEvent = new StandardMouseEvent(dom.getWindow(anchor), e); + this._contextMenuService.showContextMenu({ + getAnchor: () => standardEvent, + getActions: () => actions, + }); + } +} + +registerTerminalContribution(TerminalSelectionDecorationContribution.ID, TerminalSelectionDecorationContribution, true); + +// #endregion + +// #region Actions + +const enum TerminalSelectionCommandId { + AttachSelectionToChat = 'workbench.action.terminal.attachSelectionToChat', +} + +registerActiveXtermAction({ + id: TerminalSelectionCommandId.AttachSelectionToChat, + title: localize2('workbench.action.terminal.attachSelectionToChat', 'Attach Selection to Chat'), + icon: Codicon.sparkle, + precondition: TerminalContextKeys.textSelectedInFocused, + run: async (_xterm, accessor, activeInstance) => { + const chatWidgetService = accessor.get(IChatWidgetService); + + const selection = activeInstance.selection; if (!selection) { return; } - let widget = this._chatWidgetService.lastFocusedWidget ?? this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)?.find(w => w.attachmentCapabilities.supportsTerminalAttachments); + let widget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)?.find(w => w.attachmentCapabilities.supportsTerminalAttachments); if (!widget) { - widget = await this._chatWidgetService.revealWidget(); + widget = await chatWidgetService.revealWidget(); } if (!widget || !widget.attachmentCapabilities.supportsTerminalAttachments) { @@ -144,22 +190,28 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer } // Clear the selection after attaching - const selectionText = selection; - this._xterm.raw.clearSelection(); + activeInstance.clearSelection(); // Attach the selection as a string attachment const attachment: IChatRequestStringVariableEntry = { kind: 'string', id: `terminalSelection:${Date.now()}`, name: localize('terminal.selection', "Terminal Selection"), - value: selectionText, + value: selection, icon: Codicon.terminal, uri: URI.parse(`terminal-selection:${Date.now()}`), }; widget.attachmentModel.addContext(attachment); widget.focusInput(); - } -} + }, + menu: [ + { + id: MenuId.TerminalSelectionContext, + group: 'navigation', + order: 1, + } + ] +}); -registerTerminalContribution(TerminalSelectionDecorationContribution.ID, TerminalSelectionDecorationContribution, true); +// #endregion From 1a831e30d339b56e19676f029e8befa30e485259 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 4 Jan 2026 08:46:19 -0800 Subject: [PATCH 3/4] Text button --- .../browser/media/terminalSelectionDecoration.css | 10 ++++------ .../terminal.selectionDecoration.contribution.ts | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css index f224b7acc6025..40df6dd3b0631 100644 --- a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css @@ -9,16 +9,14 @@ } .terminal-selection-decoration .terminal-selection-action-button { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; + display: inline-block; + white-space: nowrap; + padding: 2px 8px; border-radius: 4px; cursor: pointer; background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); - font-size: 14px; + font-size: 12px; transition: background-color 0.1s ease-in-out; transform: translateY(-100%); } diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts index dafa1f3386dc8..a72f9388fd9ea 100644 --- a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts @@ -56,6 +56,7 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer } private _onSelectionChange(): void { + // TODO: Show decoration in intuitive position regardless of where it starts // TODO: Upstream to allow listening while selection is in progress // Clear decoration immediately when selection changes this._decoration.clear(); @@ -122,7 +123,7 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer // Create the action button const button = dom.append(element, dom.$('.terminal-selection-action-button')); - button.classList.add('codicon', 'codicon-sparkle'); + button.textContent = localize('addSelectionToChat', "Add Selection to Chat"); this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.CLICK, (e) => { e.stopImmediatePropagation(); From d43588250764f5d83126687d4f2379c18fe18d14 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:57:29 -0800 Subject: [PATCH 4/4] Use tool bar --- .../media/terminalSelectionDecoration.css | 25 ++++---- ...rminal.selectionDecoration.contribution.ts | 63 ++++++------------- 2 files changed, 29 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css index 40df6dd3b0631..2427af7f97db0 100644 --- a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/media/terminalSelectionDecoration.css @@ -8,23 +8,20 @@ pointer-events: auto !important; } -.terminal-selection-decoration .terminal-selection-action-button { - display: inline-block; - white-space: nowrap; - padding: 2px 8px; - border-radius: 4px; - cursor: pointer; - background-color: var(--vscode-button-background); - color: var(--vscode-button-foreground); - font-size: 12px; - transition: background-color 0.1s ease-in-out; +.terminal-selection-decoration .terminal-selection-action-bar { + display: inline-flex; transform: translateY(-100%); + background-color: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-editorWidget-border); + border-radius: 4px; + padding: 2px; } -.terminal-selection-decoration .terminal-selection-action-button:hover { - background-color: var(--vscode-button-hoverBackground); +.terminal-selection-decoration .terminal-selection-action-bar .monaco-action-bar { + background: transparent; } -.terminal-selection-decoration .terminal-selection-action-button:active { - background-color: var(--vscode-button-hoverBackground); +.terminal-selection-decoration .terminal-selection-action-bar .monaco-action-bar .action-item .action-label { + padding: 2px 4px; + border-radius: 2px; } diff --git a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts index a72f9388fd9ea..ed0d9ac62fcee 100644 --- a/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/selectionDecoration/browser/terminal.selectionDecoration.contribution.ts @@ -11,17 +11,15 @@ import { localize, localize2 } from '../../../../../nls.js'; import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; -import { IMenu, IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; -import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js'; import { IChatWidgetService } from '../../../chat/browser/chat.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { IChatRequestStringVariableEntry } from '../../../chat/common/attachments/chatVariableEntries.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import './media/terminalSelectionDecoration.css'; // #region Terminal Contribution @@ -37,17 +35,13 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer private readonly _decoration = this._register(new MutableDisposable()); private readonly _decorationListeners = this._register(new DisposableStore()); private readonly _showDecorationScheduler: RunOnceScheduler; - private readonly _menu: IMenu; constructor( _ctx: ITerminalContributionContext | IDetachedCompatibleTerminalContributionContext, - @IMenuService menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._showDecorationScheduler = this._register(new RunOnceScheduler(() => this._showDecoration(), 200)); - this._menu = this._register(menuService.createMenu(MenuId.TerminalSelectionContext, contextKeyService)); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -80,12 +74,6 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer return; } - // Check if menu has any actions - const actions = getFlatContextMenuActions(this._menu.getActions({ shouldForwardArgs: true })); - if (actions.length === 0) { - return; - } - const selectionPosition = this._xterm.raw.getSelectionPosition(); if (!selectionPosition) { return; @@ -121,40 +109,25 @@ class TerminalSelectionDecorationContribution extends Disposable implements ITer private _setupDecorationElement(element: HTMLElement): void { element.classList.add('terminal-selection-decoration'); - // Create the action button - const button = dom.append(element, dom.$('.terminal-selection-action-button')); - button.textContent = localize('addSelectionToChat', "Add Selection to Chat"); - - this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.CLICK, (e) => { - e.stopImmediatePropagation(); - e.preventDefault(); - this._showContextMenu(e, button); - })); + // Create the action bar container + const actionBarContainer = dom.append(element, dom.$('.terminal-selection-action-bar')); + + // Create a MenuWorkbenchToolBar for the actions + this._decorationListeners.add(this._instantiationService.createInstance( + MenuWorkbenchToolBar, + actionBarContainer, + MenuId.TerminalSelectionContext, + { + menuOptions: { shouldForwardArgs: true }, + toolbarOptions: { primaryGroup: () => true } + } + )); - this._decorationListeners.add(dom.addDisposableListener(button, dom.EventType.MOUSE_DOWN, (e) => { + this._decorationListeners.add(dom.addDisposableListener(actionBarContainer, dom.EventType.MOUSE_DOWN, (e) => { e.stopImmediatePropagation(); e.preventDefault(); })); } - - private _showContextMenu(e: MouseEvent, anchor: HTMLElement): void { - const actions = getFlatContextMenuActions(this._menu.getActions({ shouldForwardArgs: true })); - if (actions.length === 0) { - return; - } - - // If only one action, run it directly - if (actions.length === 1) { - actions[0].run(); - return; - } - - const standardEvent = new StandardMouseEvent(dom.getWindow(anchor), e); - this._contextMenuService.showContextMenu({ - getAnchor: () => standardEvent, - getActions: () => actions, - }); - } } registerTerminalContribution(TerminalSelectionDecorationContribution.ID, TerminalSelectionDecorationContribution, true);