From 08ee8cdf2b5bb1ab5c9ef49b2f120be2f0c9a8e7 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 20 Feb 2026 17:56:59 +0100 Subject: [PATCH 1/7] sessions feedback improvements --- src/vs/base/browser/ui/tree/abstractTree.ts | 18 +- src/vs/sessions/browser/workbench.ts | 28 +- .../browser/agentFeedback.contribution.ts | 9 +- .../browser/agentFeedbackAttachmentWidget.ts | 4 +- .../browser/agentFeedbackEditorActions.ts | 1 - .../agentFeedbackEditorInputContribution.ts | 52 +- .../agentFeedbackEditorWidgetContribution.ts | 543 ++++++++++++++++++ .../agentFeedbackGlyphMarginContribution.ts | 188 ------ .../browser/agentFeedbackHover.ts | 359 +++++++++--- ...agentFeedbackLineDecorationContribution.ts | 31 +- .../browser/agentFeedbackService.ts | 55 +- .../browser/media/agentFeedbackAttachment.css | 79 +-- .../media/agentFeedbackEditorInput.css | 40 +- .../media/agentFeedbackEditorWidget.css | 190 ++++++ .../media/agentFeedbackGlyphMargin.css | 25 - .../media/agentFeedbackLineDecoration.css | 10 +- .../test/browser/agentFeedbackService.test.ts | 200 +++++++ .../contrib/chat/browser/chat.contribution.ts | 5 +- .../contrib/chat/browser/runScriptAction.ts | 65 ++- .../chatAttachmentWidgetRegistry.ts | 8 +- .../browser/widget/input/chatInputPart.ts | 2 +- 21 files changed, 1485 insertions(+), 427 deletions(-) create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts delete mode 100644 src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css delete mode 100644 src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css create mode 100644 src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index d3fc8596e457c..9a06fa89094d5 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -310,6 +310,7 @@ export enum RenderIndentGuides { interface ITreeRendererOptions { readonly indent?: number; + readonly defaultIndent?: number; readonly renderIndentGuides?: RenderIndentGuides; // TODO@joao replace this with collapsible: boolean | 'ondemand' readonly hideTwistiesOfChildlessElements?: boolean; @@ -347,6 +348,7 @@ export class TreeRenderer implements IListR private renderedElements = new Map>(); private renderedNodes = new Map, ITreeListTemplateData>(); private indent: number = TreeRenderer.DefaultIndent; + private defaultIndent: number = TreeRenderer.DefaultIndent; private hideTwistiesOfChildlessElements: boolean = false; private twistieAdditionalCssClass?: (element: T) => string | undefined; @@ -372,14 +374,19 @@ export class TreeRenderer implements IListR } updateOptions(options: ITreeRendererOptions = {}): void { - if (typeof options.indent !== 'undefined') { - const indent = clamp(options.indent, 0, 40); + if (typeof options.defaultIndent !== 'undefined') { + this.defaultIndent = options.defaultIndent; + } + + if (typeof options.indent !== 'undefined' || typeof options.defaultIndent !== 'undefined') { + const indent = typeof options.indent !== 'undefined' ? clamp(options.indent, 0, 40) : this.indent; + const needsRerender = indent !== this.indent || typeof options.defaultIndent !== 'undefined'; - if (indent !== this.indent) { + if (needsRerender) { this.indent = indent; for (const [node, templateData] of this.renderedNodes) { - templateData.indentSize = TreeRenderer.DefaultIndent + (node.depth - 1) * this.indent; + templateData.indentSize = this.defaultIndent + (node.depth - 1) * this.indent; this.renderTreeElement(node, templateData); } } @@ -427,7 +434,7 @@ export class TreeRenderer implements IListR } renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData, details?: IListElementRenderDetails): void { - templateData.indentSize = TreeRenderer.DefaultIndent + (node.depth - 1) * this.indent; + templateData.indentSize = this.defaultIndent + (node.depth - 1) * this.indent; this.renderedNodes.set(node, templateData); this.renderedElements.set(node.element, node); @@ -2192,6 +2199,7 @@ function asTreeContextMenuEvent(event: IListContextMenuEv } export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { + readonly defaultIndent?: number; // Only recommended for compact layouts. Leave unchanged otherwise readonly multipleSelectionSupport?: boolean; readonly typeNavigationEnabled?: boolean; readonly typeNavigationMode?: TypeNavigationMode; diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 3ba1a8bb2cc0c..3f7db29b96022 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -1067,9 +1067,15 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !hidden, ); + // If sidebar becomes hidden, also hide the current active pane composite + if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); + } + // If sidebar becomes visible, show last active Viewlet or default viewlet if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { - const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); + const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar) ?? + this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; if (viewletToOpen) { this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.Sidebar); } @@ -1090,9 +1096,15 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !hidden, ); - // If auxiliary bar becomes visible, show last active pane composite + // If auxiliary bar becomes hidden, also hide the current active pane composite + if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); + } + + // If auxiliary bar becomes visible, show last active pane composite or default if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { - const paneCompositeToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); + const paneCompositeToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar) ?? + this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id; if (paneCompositeToOpen) { this.paneCompositeService.openPaneComposite(paneCompositeToOpen, ViewContainerLocation.AuxiliaryBar); } @@ -1135,9 +1147,15 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !hidden, ); - // If panel becomes visible, show last active panel + // If panel becomes hidden, also hide the current active pane composite + if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); + } + + // If panel becomes visible, show last active panel or default if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { - const panelToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Panel); + const panelToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Panel) ?? + this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id; if (panelToOpen) { this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts index 6bb6704f60278..3b062517e4754 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import './agentFeedbackEditorInputContribution.js'; +import './agentFeedbackEditorWidgetContribution.js'; import './agentFeedbackLineDecorationContribution.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { AgentFeedbackService, IAgentFeedbackService } from './agentFeedbackService.js'; import { AgentFeedbackAttachmentContribution } from './agentFeedbackAttachment.js'; @@ -25,8 +27,11 @@ registerSingleton(IAgentFeedbackService, AgentFeedbackService, InstantiationType // Register the custom attachment widget for agentFeedback attachments class AgentFeedbackAttachmentWidgetContribution { static readonly ID = 'workbench.contrib.agentFeedbackAttachmentWidgetFactory'; - constructor(@IChatAttachmentWidgetRegistry registry: IChatAttachmentWidgetRegistry) { - registry.registerFactory('agentFeedback', (instantiationService, attachment, options, container) => { + constructor( + @IChatAttachmentWidgetRegistry registry: IChatAttachmentWidgetRegistry, + @IInstantiationService instantiationService: IInstantiationService, + ) { + registry.registerFactory('agentFeedback', (attachment, options, container) => { return instantiationService.createInstance(AgentFeedbackAttachmentWidget, attachment as IAgentFeedbackVariableEntry, options, container); }); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts index 0a420da416111..fb2b68188e315 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts @@ -50,8 +50,10 @@ export class AgentFeedbackAttachmentWidget extends Disposable { const label = dom.$('span.chat-attached-context-custom-text', {}, this._attachment.name); this.element.appendChild(label); + const deletionCurrentlyNotSupported = true; + // Clear button - if (options.supportsDeletion) { + if (options.supportsDeletion && !deletionCurrentlyNotSupported) { const clearBtn = dom.append(this.element, dom.$('.chat-attached-context-clear-button')); const clearIcon = dom.$('span'); clearIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.close)); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts index a1258d70b5cc7..620cf99ae3db9 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts @@ -131,7 +131,6 @@ class NavigateFeedbackAction extends AgentFeedbackEditorAction { editorService.openEditor({ resource: feedback.resourceUri, options: { - selection: feedback.range, preserveFocus: false, revealIfVisible: true, } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts index bfc4bd97f7e6c..2b690a82bad95 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts @@ -22,12 +22,16 @@ import { localize } from '../../../../nls.js'; class AgentFeedbackInputWidget implements IOverlayWidget { private static readonly _ID = 'agentFeedback.inputWidget'; + private static readonly _MIN_WIDTH = 150; + private static readonly _MAX_WIDTH = 400; readonly allowEditorOverflow = false; private readonly _domNode: HTMLElement; - private readonly _inputElement: HTMLInputElement; + private readonly _inputElement: HTMLTextAreaElement; + private readonly _measureElement: HTMLElement; private _position: IOverlayWidgetPosition | null = null; + private _lineHeight = 0; constructor( private readonly _editor: ICodeEditor, @@ -36,12 +40,19 @@ class AgentFeedbackInputWidget implements IOverlayWidget { this._domNode.classList.add('agent-feedback-input-widget'); this._domNode.style.display = 'none'; - this._inputElement = document.createElement('input'); - this._inputElement.type = 'text'; + this._inputElement = document.createElement('textarea'); + this._inputElement.rows = 1; this._inputElement.placeholder = localize('agentFeedback.addFeedback', "Add Feedback"); this._domNode.appendChild(this._inputElement); + // Hidden element used to measure text width for auto-growing + this._measureElement = document.createElement('span'); + this._measureElement.classList.add('agent-feedback-input-measure'); + this._domNode.appendChild(this._measureElement); + this._editor.applyFontInfo(this._inputElement); + this._editor.applyFontInfo(this._measureElement); + this._lineHeight = this._editor.getOption(EditorOption.lineHeight); } getId(): string { @@ -56,7 +67,7 @@ class AgentFeedbackInputWidget implements IOverlayWidget { return this._position; } - get inputElement(): HTMLInputElement { + get inputElement(): HTMLTextAreaElement { return this._inputElement; } @@ -75,6 +86,28 @@ class AgentFeedbackInputWidget implements IOverlayWidget { clearInput(): void { this._inputElement.value = ''; + this._autoSize(); + } + + autoSize(): void { + this._autoSize(); + } + + private _autoSize(): void { + const text = this._inputElement.value || this._inputElement.placeholder; + + // Measure the text width using the hidden span + this._measureElement.textContent = text; + const textWidth = this._measureElement.scrollWidth; + + // Clamp width between min and max + const width = Math.max(AgentFeedbackInputWidget._MIN_WIDTH, Math.min(textWidth + 10, AgentFeedbackInputWidget._MAX_WIDTH)); + this._inputElement.style.width = `${width}px`; + + // Reset height to auto then expand to fit all content, with a minimum of 1 line + this._inputElement.style.height = 'auto'; + const newHeight = Math.max(this._inputElement.scrollHeight, this._lineHeight + 4 /* padding */); + this._inputElement.style.height = `${newHeight}px`; } } @@ -110,8 +143,11 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements this._mouseDown = true; this._hide(); })); - this._store.add(this._editor.onMouseUp(() => { + this._store.add(this._editor.onMouseUp((e) => { this._mouseDown = false; + if (this._isWidgetTarget(e.event.target)) { + return; + } this._onSelectionChanged(); })); this._store.add(this._editor.onDidBlurEditorWidget(() => { @@ -262,6 +298,12 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements e.stopPropagation(); })); + // Auto-size the textarea as the user types + this._widgetListeners.add(addStandardDisposableListener(widget.inputElement, 'input', () => { + widget.autoSize(); + this._updatePosition(); + })); + // Hide when input loses focus to something outside both editor and widget this._widgetListeners.add(addStandardDisposableListener(widget.inputElement, 'blur', () => { const win = getWindow(widget.inputElement); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts new file mode 100644 index 0000000000000..3e420275939f7 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -0,0 +1,543 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/agentFeedbackEditorWidget.css'; + +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Event } from '../../../../base/common/event.js'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js'; +import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { $, addDisposableListener, clearNode, getTotalWidth } from '../../../../base/browser/dom.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { overviewRulerRangeHighlight } from '../../../../editor/common/core/editorColorRegistry.js'; +import { OverviewRulerLane } from '../../../../editor/common/model.js'; +import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; +import * as nls from '../../../../nls.js'; +import { IAgentFeedback, IAgentFeedbackService } from './agentFeedbackService.js'; +import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { getSessionForResource } from './agentFeedbackEditorUtils.js'; + +/** + * Groups nearby feedback items within a threshold number of lines. + */ +function groupNearbyFeedback(items: readonly IAgentFeedback[], lineThreshold: number = 5): IAgentFeedback[][] { + if (items.length === 0) { + return []; + } + + // Sort by start line number + const sorted = [...items].sort((a, b) => a.range.startLineNumber - b.range.startLineNumber); + + const groups: IAgentFeedback[][] = []; + let currentGroup: IAgentFeedback[] = [sorted[0]]; + + for (let i = 1; i < sorted.length; i++) { + const firstItem = currentGroup[0]; + const currentItem = sorted[i]; + + const verticalSpan = currentItem.range.startLineNumber - firstItem.range.startLineNumber; + + if (verticalSpan <= lineThreshold) { + currentGroup.push(currentItem); + } else { + groups.push(currentGroup); + currentGroup = [currentItem]; + } + } + + if (currentGroup.length > 0) { + groups.push(currentGroup); + } + + return groups; +} + +/** + * Widget that displays agent feedback comments for a group of nearby feedback items. + * Positioned on the right side of the editor like a speech bubble. + */ +export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWidget { + + private static _idPool = 0; + private readonly _id: string = `agent-feedback-widget-${AgentFeedbackEditorWidget._idPool++}`; + + private readonly _domNode: HTMLElement; + private readonly _headerNode: HTMLElement; + private readonly _titleNode: HTMLElement; + private readonly _dismissButton: HTMLElement; + private readonly _toggleButton: HTMLElement; + private readonly _bodyNode: HTMLElement; + private readonly _itemElements = new Map(); + + private _position: IOverlayWidgetPosition | null = null; + private _isExpanded: boolean = false; + private _disposed: boolean = false; + private _startLineNumber: number = 1; + private readonly _rangeHighlightDecoration: IEditorDecorationsCollection; + + private readonly _eventStore = this._register(new DisposableStore()); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _feedbackItems: readonly IAgentFeedback[], + private readonly _agentFeedbackService: IAgentFeedbackService, + private readonly _sessionResource: URI, + ) { + super(); + + this._rangeHighlightDecoration = this._editor.createDecorationsCollection(); + + // Create DOM structure + this._domNode = $('div.agent-feedback-widget'); + this._domNode.classList.add('collapsed'); + + // Header + this._headerNode = $('div.agent-feedback-widget-header'); + + // Title showing feedback count + this._titleNode = $('span.agent-feedback-widget-title'); + this._updateTitle(); + this._headerNode.appendChild(this._titleNode); + + // Spacer + this._headerNode.appendChild($('span.agent-feedback-widget-spacer')); + + // Toggle expand/collapse button + this._toggleButton = $('div.agent-feedback-widget-toggle'); + this._updateToggleButton(); + this._headerNode.appendChild(this._toggleButton); + + // Dismiss button + this._dismissButton = $('div.agent-feedback-widget-dismiss'); + this._dismissButton.appendChild(renderIcon(Codicon.close)); + this._dismissButton.title = nls.localize('dismiss', "Dismiss"); + this._headerNode.appendChild(this._dismissButton); + + this._domNode.appendChild(this._headerNode); + + // Body (collapsible) — starts collapsed + this._bodyNode = $('div.agent-feedback-widget-body'); + this._bodyNode.classList.add('collapsed'); + this._buildFeedbackItems(); + this._domNode.appendChild(this._bodyNode); + + // Arrow pointer + const arrow = $('div.agent-feedback-widget-arrow'); + this._domNode.appendChild(arrow); + + // Event handlers + this._setupEventHandlers(); + + // Add visible class for initial display + this._domNode.classList.add('visible'); + + // Add to editor + this._editor.addOverlayWidget(this); + } + + private _setupEventHandlers(): void { + // Toggle button click - expand/collapse + this._eventStore.add(addDisposableListener(this._toggleButton, 'click', (e) => { + e.stopPropagation(); + this._toggleExpanded(); + })); + + // Header click - also toggles expand/collapse + this._eventStore.add(addDisposableListener(this._headerNode, 'click', () => { + this._toggleExpanded(); + })); + + // Dismiss button click + this._eventStore.add(addDisposableListener(this._dismissButton, 'click', (e) => { + e.stopPropagation(); + this._dismiss(); + })); + } + + private _toggleExpanded(): void { + if (this._isExpanded) { + this.collapse(); + } else { + this.expand(); + } + } + + private _dismiss(): void { + // Remove all feedback items in this widget from the service + for (const feedback of this._feedbackItems) { + this._agentFeedbackService.removeFeedback(this._sessionResource, feedback.id); + } + + this._domNode.classList.add('fadeOut'); + + const dispose = () => { + this.dispose(); + }; + + const handle = setTimeout(dispose, 150); + this._domNode.addEventListener('animationend', () => { + clearTimeout(handle); + dispose(); + }, { once: true }); + } + + private _updateTitle(): void { + const count = this._feedbackItems.length; + if (count === 1) { + this._titleNode.textContent = nls.localize('oneComment', "1 comment"); + } else { + this._titleNode.textContent = nls.localize('nComments', "{0} comments", count); + } + } + + private _updateToggleButton(): void { + clearNode(this._toggleButton); + if (this._isExpanded) { + this._toggleButton.appendChild(renderIcon(Codicon.chevronUp)); + this._toggleButton.title = nls.localize('collapse', "Collapse"); + } else { + this._toggleButton.appendChild(renderIcon(Codicon.chevronDown)); + this._toggleButton.title = nls.localize('expand', "Expand"); + } + } + + private _buildFeedbackItems(): void { + clearNode(this._bodyNode); + this._itemElements.clear(); + + for (const feedback of this._feedbackItems) { + const item = $('div.agent-feedback-widget-item'); + this._itemElements.set(feedback.id, item); + + // Line indicator + const lineInfo = $('span.agent-feedback-widget-line-info'); + if (feedback.range.startLineNumber === feedback.range.endLineNumber) { + lineInfo.textContent = nls.localize('lineNumber', "Line {0}", feedback.range.startLineNumber); + } else { + lineInfo.textContent = nls.localize('lineRange', "Lines {0}-{1}", feedback.range.startLineNumber, feedback.range.endLineNumber); + } + item.appendChild(lineInfo); + + // Feedback text + const text = $('span.agent-feedback-widget-text'); + text.textContent = feedback.text; + item.appendChild(text); + + // Hover handlers for range highlighting + this._eventStore.add(addDisposableListener(item, 'mouseenter', () => { + this._highlightRange(feedback); + })); + + this._eventStore.add(addDisposableListener(item, 'mouseleave', () => { + this._rangeHighlightDecoration.clear(); + })); + + this._bodyNode.appendChild(item); + } + } + + /** + * Expand the widget body. + */ + expand(): void { + this._isExpanded = true; + this._domNode.classList.remove('collapsed'); + this._bodyNode.classList.remove('collapsed'); + this._updateToggleButton(); + this._editor.layoutOverlayWidget(this); + } + + /** + * Collapse the widget body. + */ + collapse(): void { + this._isExpanded = false; + this._domNode.classList.add('collapsed'); + this._bodyNode.classList.add('collapsed'); + this._updateToggleButton(); + this.clearFocus(); + this._editor.layoutOverlayWidget(this); + } + + /** + * Focus a specific feedback item within this widget. + * Highlights its range in the editor and marks it as focused. + */ + focusFeedback(feedbackId: string): void { + // Clear previous focus + for (const el of this._itemElements.values()) { + el.classList.remove('focused'); + } + + const feedback = this._feedbackItems.find(f => f.id === feedbackId); + if (!feedback) { + return; + } + + // Add focused class to the item + const itemEl = this._itemElements.get(feedbackId); + itemEl?.classList.add('focused'); + + // Show range highlighting + this._highlightRange(feedback); + } + + /** + * Clear focus state and range highlighting. + */ + clearFocus(): void { + for (const el of this._itemElements.values()) { + el.classList.remove('focused'); + } + this._rangeHighlightDecoration.clear(); + } + + private _highlightRange(feedback: IAgentFeedback): void { + const endLineNumber = feedback.range.endLineNumber; + const range = new Range( + feedback.range.startLineNumber, 1, + endLineNumber, this._editor.getModel()?.getLineMaxColumn(endLineNumber) ?? 1 + ); + this._rangeHighlightDecoration.set([ + { + range, + options: { + description: 'agent-feedback-range-highlight', + className: 'rangeHighlight', + isWholeLine: true, + linesDecorationsClassName: 'agent-feedback-widget-range-glyph', + } + }, + { + range, + options: { + description: 'agent-feedback-range-highlight-overview', + overviewRuler: { + color: themeColorFromId(overviewRulerRangeHighlight), + position: OverviewRulerLane.Full, + } + } + } + ]); + } + + /** + * Returns true if this widget contains the given feedback item (by id). + */ + containsFeedback(feedbackId: string): boolean { + return this._feedbackItems.some(f => f.id === feedbackId); + } + + /** + * Updates the widget position and layout. + */ + layout(startLineNumber: number): void { + if (this._disposed) { + return; + } + + this._startLineNumber = startLineNumber; + + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const { contentLeft, contentWidth, verticalScrollbarWidth } = this._editor.getLayoutInfo(); + const scrollTop = this._editor.getScrollTop(); + + const widgetWidth = getTotalWidth(this._domNode) || 280; + + this._position = { + stackOrdinal: 2, + preference: { + top: this._editor.getTopForLineNumber(startLineNumber) - scrollTop - lineHeight, + left: contentLeft + contentWidth - (2 * verticalScrollbarWidth + widgetWidth) + } + }; + + this._editor.layoutOverlayWidget(this); + } + + /** + * Shows or hides the widget. + */ + toggle(show: boolean): void { + this._domNode.classList.toggle('visible', show); + if (show && this._feedbackItems.length > 0) { + this.layout(this._feedbackItems[0].range.startLineNumber); + } + } + + /** + * Relayouts the widget at its current line number. + */ + relayout(): void { + if (this._startLineNumber) { + this.layout(this._startLineNumber); + } + } + + // IOverlayWidget implementation + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return this._position; + } + + override dispose(): void { + if (this._disposed) { + return; + } + this._disposed = true; + this._rangeHighlightDecoration.clear(); + this._editor.removeOverlayWidget(this); + super.dispose(); + } +} + +/** + * Editor contribution that manages agent feedback widgets. + * Groups feedback items and creates combined widgets for nearby items. + * Widgets start collapsed and expand when navigated to. + */ +class AgentFeedbackEditorWidgetContribution extends Disposable implements IEditorContribution { + + static readonly ID = 'agentFeedback.editorWidgetContribution'; + + private readonly _widgets: AgentFeedbackEditorWidget[] = []; + private _sessionResource: URI | undefined; + + constructor( + private readonly _editor: ICodeEditor, + @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + ) { + super(); + + this._store.add(this._agentFeedbackService.onDidChangeFeedback(e => { + if (this._sessionResource && e.sessionResource.toString() === this._sessionResource.toString()) { + this._rebuildWidgets(); + } + })); + + this._store.add(this._agentFeedbackService.onDidChangeNavigation(sessionResource => { + if (this._sessionResource && sessionResource.toString() === this._sessionResource.toString()) { + this._handleNavigation(); + } + })); + + this._store.add(this._editor.onDidChangeModel(() => { + this._resolveSession(); + this._rebuildWidgets(); + })); + + this._store.add(Event.any(this._editor.onDidScrollChange, this._editor.onDidLayoutChange)(() => { + for (const widget of this._widgets) { + widget.relayout(); + } + })); + + this._resolveSession(); + this._rebuildWidgets(); + } + + private _resolveSession(): void { + const model = this._editor.getModel(); + if (!model) { + this._sessionResource = undefined; + return; + } + this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); + } + + private _rebuildWidgets(): void { + this._clearWidgets(); + + if (!this._sessionResource) { + return; + } + + const model = this._editor.getModel(); + if (!model) { + return; + } + + const allFeedback = this._agentFeedbackService.getFeedback(this._sessionResource); + // Filter to feedback items belonging to this editor's file + const fileFeedback = allFeedback.filter(f => f.resourceUri.toString() === model.uri.toString()); + if (fileFeedback.length === 0) { + return; + } + + const groups = groupNearbyFeedback(fileFeedback, 5); + + for (const group of groups) { + const widget = new AgentFeedbackEditorWidget(this._editor, group, this._agentFeedbackService, this._sessionResource); + this._widgets.push(widget); + this._register(widget); + + widget.layout(group[0].range.startLineNumber); + } + } + + private _handleNavigation(): void { + if (!this._sessionResource) { + return; + } + + const bearing = this._agentFeedbackService.getNavigationBearing(this._sessionResource); + if (bearing.activeIdx < 0) { + return; + } + + const allFeedback = this._agentFeedbackService.getFeedback(this._sessionResource); + const activeFeedback = allFeedback[bearing.activeIdx]; + if (!activeFeedback) { + return; + } + + // Expand the widget containing the active feedback, collapse all others + for (const widget of this._widgets) { + if (widget.containsFeedback(activeFeedback.id)) { + widget.expand(); + widget.focusFeedback(activeFeedback.id); + } else { + widget.collapse(); + } + } + + // Reveal the feedback range in the editor + const range = new Range( + activeFeedback.range.startLineNumber, 1, + activeFeedback.range.endLineNumber, 1 + ); + this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); + } + + private _clearWidgets(): void { + for (const widget of this._widgets) { + widget.dispose(); + } + this._widgets.length = 0; + } + + override dispose(): void { + this._clearWidgets(); + super.dispose(); + } +} + +registerEditorContribution(AgentFeedbackEditorWidgetContribution.ID, AgentFeedbackEditorWidgetContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts deleted file mode 100644 index bc7805d4b1720..0000000000000 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts +++ /dev/null @@ -1,188 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/agentFeedbackGlyphMargin.css'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IAgentFeedbackService } from './agentFeedbackService.js'; -import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; -import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { getSessionForResource } from './agentFeedbackEditorUtils.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; - -const feedbackGlyphDecoration = ModelDecorationOptions.register({ - description: 'agent-feedback-glyph', - linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.comment)} agent-feedback-glyph`, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, -}); - -const addFeedbackHintDecoration = ModelDecorationOptions.register({ - description: 'agent-feedback-add-hint', - linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, -}); - -export class AgentFeedbackGlyphMarginContribution extends Disposable implements IEditorContribution { - - static readonly ID = 'agentFeedback.glyphMarginContribution'; - - private readonly _feedbackDecorations; - - private _hintDecorationId: string | null = null; - private _hintLine = -1; - private _sessionResource: URI | undefined; - private _feedbackLines = new Set(); - - constructor( - private readonly _editor: ICodeEditor, - @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, - ) { - super(); - - this._feedbackDecorations = this._editor.createDecorationsCollection(); - - this._store.add(this._agentFeedbackService.onDidChangeFeedback(() => this._updateFeedbackDecorations())); - this._store.add(this._editor.onDidChangeModel(() => this._onModelChanged())); - this._store.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onMouseMove(e))); - this._store.add(this._editor.onMouseLeave(() => this._updateHintDecoration(-1))); - this._store.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onMouseDown(e))); - - this._resolveSession(); - this._updateFeedbackDecorations(); - } - - private _onModelChanged(): void { - this._updateHintDecoration(-1); - this._resolveSession(); - this._updateFeedbackDecorations(); - } - - private _resolveSession(): void { - const model = this._editor.getModel(); - if (!model) { - this._sessionResource = undefined; - return; - } - this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); - } - - private _updateFeedbackDecorations(): void { - if (!this._sessionResource) { - this._feedbackDecorations.clear(); - this._feedbackLines.clear(); - return; - } - - const feedbackItems = this._agentFeedbackService.getFeedback(this._sessionResource); - const decorations: IModelDeltaDecoration[] = []; - const lines = new Set(); - - for (const item of feedbackItems) { - const model = this._editor.getModel(); - if (!model || item.resourceUri.toString() !== model.uri.toString()) { - continue; - } - - const line = item.range.startLineNumber; - lines.add(line); - decorations.push({ - range: new Range(line, 1, line, 1), - options: feedbackGlyphDecoration, - }); - } - - this._feedbackLines = lines; - this._feedbackDecorations.set(decorations); - } - - private _onMouseMove(e: IEditorMouseEvent): void { - if (!this._sessionResource) { - this._updateHintDecoration(-1); - return; - } - - const isLineDecoration = e.target.type === MouseTargetType.GUTTER_LINE_DECORATIONS && !e.target.detail.isAfterLines; - const isContentArea = e.target.type === MouseTargetType.CONTENT_TEXT || e.target.type === MouseTargetType.CONTENT_EMPTY; - if (e.target.position - && (isLineDecoration || isContentArea) - && !this._feedbackLines.has(e.target.position.lineNumber) - ) { - this._updateHintDecoration(e.target.position.lineNumber); - } else { - this._updateHintDecoration(-1); - } - } - - private _updateHintDecoration(line: number): void { - if (line === this._hintLine) { - return; - } - - this._hintLine = line; - this._editor.changeDecorations(accessor => { - if (this._hintDecorationId) { - accessor.removeDecoration(this._hintDecorationId); - this._hintDecorationId = null; - } - if (line !== -1) { - this._hintDecorationId = accessor.addDecoration( - new Range(line, 1, line, 1), - addFeedbackHintDecoration, - ); - } - }); - } - - private _onMouseDown(e: IEditorMouseEvent): void { - if (!e.target.position - || e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS - || e.target.detail.isAfterLines - || !this._sessionResource - ) { - return; - } - - const lineNumber = e.target.position.lineNumber; - - // Lines with existing feedback - do nothing - if (this._feedbackLines.has(lineNumber)) { - return; - } - - // Select the line content and focus the editor - const model = this._editor.getModel(); - if (!model) { - return; - } - - const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); - const endColumn = model.getLineLastNonWhitespaceColumn(lineNumber); - if (startColumn === 0 || endColumn === 0) { - // Empty line - select the whole line range - this._editor.setSelection(new Selection(lineNumber, model.getLineMaxColumn(lineNumber), lineNumber, 1)); - } else { - this._editor.setSelection(new Selection(lineNumber, endColumn, lineNumber, startColumn)); - } - this._editor.focus(); - } - - override dispose(): void { - this._feedbackDecorations.clear(); - this._updateHintDecoration(-1); - super.dispose(); - } -} - -registerEditorContribution(AgentFeedbackGlyphMarginContribution.ID, AgentFeedbackGlyphMarginContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts index a1a5baa2f209b..8924068b591a4 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts @@ -4,24 +4,198 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; -import { HoverStyle } from '../../../../base/browser/ui/hover/hover.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { HoverStyle, IDelayedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; +import { IObjectTreeElement, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; +import { Action } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { basename } from '../../../../base/common/path.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; +import { FileKind } from '../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../../../workbench/browser/labels.js'; +import { WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; +import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from '../../../../workbench/browser/labels.js'; import { IAgentFeedbackService } from './agentFeedbackService.js'; import { IAgentFeedbackVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; +import { editorHoverBackground } from '../../../../platform/theme/common/colorRegistry.js'; + +const $ = dom.$; + +// --- Tree Element Types --- + +interface IFeedbackFileElement { + readonly type: 'file'; + readonly uri: URI; + readonly items: ReadonlyArray; +} + +interface IFeedbackCommentElement { + readonly type: 'comment'; + readonly id: string; + readonly text: string; + readonly resourceUri: URI; + readonly range: IRange; +} + +type FeedbackTreeElement = IFeedbackFileElement | IFeedbackCommentElement; + +function isFeedbackFileElement(element: FeedbackTreeElement): element is IFeedbackFileElement { + return element.type === 'file'; +} + +// --- Tree Delegate --- + +class FeedbackTreeDelegate implements IListVirtualDelegate { + getHeight(_element: FeedbackTreeElement): number { + return 22; + } + + getTemplateId(element: FeedbackTreeElement): string { + return isFeedbackFileElement(element) + ? FeedbackFileRenderer.TEMPLATE_ID + : FeedbackCommentRenderer.TEMPLATE_ID; + } +} + +// --- File Renderer --- + +interface IFeedbackFileTemplate { + readonly label: IResourceLabel; + readonly actionBar: ActionBar; + readonly templateDisposables: DisposableStore; +} + +class FeedbackFileRenderer implements ITreeRenderer { + static readonly TEMPLATE_ID = 'feedbackFile'; + readonly templateId = FeedbackFileRenderer.TEMPLATE_ID; + + constructor( + private readonly _labels: ResourceLabels, + private readonly _agentFeedbackService: IAgentFeedbackService, + private readonly _sessionResource: URI, + ) { } + + renderTemplate(container: HTMLElement): IFeedbackFileTemplate { + const templateDisposables = new DisposableStore(); + + const label = templateDisposables.add(this._labels.create(container, { supportHighlights: true, supportIcons: true })); + + const actionBarContainer = $('div.agent-feedback-hover-action-bar'); + label.element.appendChild(actionBarContainer); + const actionBar = templateDisposables.add(new ActionBar(actionBarContainer)); + + return { label, actionBar, templateDisposables }; + } + + renderElement(node: ITreeNode, _index: number, templateData: IFeedbackFileTemplate): void { + const element = node.element; + templateData.label.element.style.display = 'flex'; + + const name = basename(element.uri.path); + + + templateData.label.setResource( + { resource: element.uri, name }, + { fileKind: FileKind.FILE }, + ); + + templateData.actionBar.clear(); + templateData.actionBar.push(new Action( + 'agentFeedback.removeFileComments', + localize('agentFeedbackHover.removeAll', "Remove All"), + ThemeIcon.asClassName(Codicon.close), + true, + () => { + for (const item of element.items) { + this._agentFeedbackService.removeFeedback(this._sessionResource, item.id); + } + } + ), { icon: true, label: false }); + } + + disposeTemplate(templateData: IFeedbackFileTemplate): void { + templateData.templateDisposables.dispose(); + } +} + +// --- Comment Renderer --- + +interface IFeedbackCommentTemplate { + readonly textElement: HTMLElement; + readonly actionBar: ActionBar; + readonly templateDisposables: DisposableStore; + element: IFeedbackCommentElement | undefined; +} + +class FeedbackCommentRenderer implements ITreeRenderer { + static readonly TEMPLATE_ID = 'feedbackComment'; + readonly templateId = FeedbackCommentRenderer.TEMPLATE_ID; + + constructor( + private readonly _agentFeedbackService: IAgentFeedbackService, + private readonly _sessionResource: URI, + ) { } + + renderTemplate(container: HTMLElement): IFeedbackCommentTemplate { + const templateDisposables = new DisposableStore(); + + const row = dom.append(container, $('div.agent-feedback-hover-comment-row')); + + const textElement = dom.append(row, $('div.agent-feedback-hover-comment-text')); + + const actionBarContainer = dom.append(row, $('div.agent-feedback-hover-action-bar')); + const actionBar = templateDisposables.add(new ActionBar(actionBarContainer)); + + const templateData: IFeedbackCommentTemplate = { textElement, actionBar, templateDisposables, element: undefined }; + + templateDisposables.add(dom.addDisposableListener(row, dom.EventType.CLICK, (e) => { + const data = templateData.element; + if (data) { + e.preventDefault(); + e.stopPropagation(); + this._agentFeedbackService.revealFeedback(this._sessionResource, data.id); + } + })); + + return templateData; + } + + renderElement(node: ITreeNode, _index: number, templateData: IFeedbackCommentTemplate): void { + const element = node.element; + + templateData.textElement.textContent = element.text; + templateData.element = element; + + templateData.actionBar.clear(); + templateData.actionBar.push(new Action( + 'agentFeedback.removeComment', + localize('agentFeedbackHover.remove', "Remove"), + ThemeIcon.asClassName(Codicon.close), + true, + () => { + this._agentFeedbackService.removeFeedback(this._sessionResource, element.id); + } + ), { icon: true, label: false }); + } + + disposeTemplate(templateData: IFeedbackCommentTemplate): void { + templateData.templateDisposables.dispose(); + } +} + +// --- Hover --- /** * Creates the custom hover content for the "N comments" attachment. - * Shows each feedback item with its file, range, text, and actions (remove / go to). + * Uses a WorkbenchObjectTree to render files as parent nodes and comments as children, + * with per-row action bars for removal. */ export class AgentFeedbackHover extends Disposable { @@ -30,7 +204,6 @@ export class AgentFeedbackHover extends Disposable { private readonly _attachment: IAgentFeedbackVariableEntry, @IHoverService private readonly _hoverService: IHoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService, @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, ) { super(); @@ -38,7 +211,7 @@ export class AgentFeedbackHover extends Disposable { // Show on hover (delayed) this._store.add(this._hoverService.setupDelayedHover( this._element, - () => this._buildHoverContent(), + () => this._store.add(this._buildHoverContent()), // needs a better disposable story { groupId: 'chat-attachments' } )); @@ -52,6 +225,7 @@ export class AgentFeedbackHover extends Disposable { private _showHoverNow(): void { const opts = this._buildHoverContent(); + this._register(opts); this._hoverService.showInstantHover({ content: opts.content, target: this._element, @@ -61,89 +235,126 @@ export class AgentFeedbackHover extends Disposable { }); } - private _buildHoverContent(): { content: HTMLElement; style: HoverStyle; position: { hoverPosition: HoverPosition }; trapFocus: boolean; dispose: () => void } { + private _buildHoverContent(): IDelayedHoverOptions & IDisposable { const disposables = new DisposableStore(); - const hoverElement = dom.$('div.agent-feedback-hover'); + const hoverElement = $('div.agent-feedback-hover'); - const title = dom.$('div.agent-feedback-hover-title'); - title.textContent = this._attachment.feedbackItems.length === 1 - ? localize('agentFeedbackHover.titleOne', "1 feedback comment") - : localize('agentFeedbackHover.titleMany', "{0} feedback comments", this._attachment.feedbackItems.length); - hoverElement.appendChild(title); + // Tree container + const treeContainer = dom.append(hoverElement, $('.results.show-file-icons.file-icon-themable-tree.agent-feedback-hover-tree')); - const list = dom.$('div.agent-feedback-hover-list'); - hoverElement.appendChild(list); - - // Create ResourceLabels for file icons + // Resource labels (shared across all file renderers) const resourceLabels = disposables.add(this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); - // Group feedback items by file - const byFile = new Map(); - for (const item of this._attachment.feedbackItems) { - const key = item.resourceUri.toString(); - let group = byFile.get(key); - if (!group) { - group = []; - byFile.set(key, group); - } - group.push(item); - } + // Build tree data + const { children, commentElements } = this._buildTreeData(); - for (const [, items] of byFile) { - // File header with icon via ResourceLabels - const fileHeader = dom.$('div.agent-feedback-hover-file-header'); - list.appendChild(fileHeader); - const label = resourceLabels.create(fileHeader); - label.setFile(items[0].resourceUri, { hidePath: false }); - - for (const item of items) { - const row = dom.$('div.agent-feedback-hover-row'); - list.appendChild(row); - - // Feedback text - clicking goes to location - const text = dom.$('div.agent-feedback-hover-text'); - text.textContent = item.text; - row.appendChild(text); - - row.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this._goToFeedback(item.resourceUri, item.range); - }); - - // Remove button - const removeBtn = dom.$('a.agent-feedback-hover-remove'); - removeBtn.title = localize('agentFeedbackHover.remove', "Remove feedback"); - const removeIcon = dom.$('span'); - removeIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.close)); - removeBtn.appendChild(removeIcon); - row.appendChild(removeBtn); - - removeBtn.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this._agentFeedbackService.removeFeedback(this._attachment.sessionResource, item.id); - }); + // Create tree + const tree = disposables.add(this._instantiationService.createInstance( + WorkbenchObjectTree, + 'AgentFeedbackHoverTree', + treeContainer, + new FeedbackTreeDelegate(), + [ + new FeedbackFileRenderer(resourceLabels, this._agentFeedbackService, this._attachment.sessionResource), + new FeedbackCommentRenderer(this._agentFeedbackService, this._attachment.sessionResource), + ], + { + defaultIndent: 0, + alwaysConsumeMouseWheel: false, + accessibilityProvider: { + getAriaLabel: (element: FeedbackTreeElement) => { + if (isFeedbackFileElement(element)) { + return basename(element.uri.path); + } + return element.text; + }, + getWidgetAriaLabel: () => localize('agentFeedbackHover.tree', "Feedback Comments"), + }, + identityProvider: { + getId: (element: FeedbackTreeElement) => { + if (isFeedbackFileElement(element)) { + return `file:${element.uri.toString()}`; + } + return `comment:${element.id}`; + } + }, + overrideStyles: { + listFocusBackground: undefined, + listInactiveFocusBackground: undefined, + listActiveSelectionBackground: undefined, + listFocusAndSelectionBackground: undefined, + listInactiveSelectionBackground: undefined, + listBackground: editorHoverBackground, + listFocusForeground: undefined, + treeStickyScrollBackground: editorHoverBackground, + } } - } + )); + + // Set tree data + tree.setChildren(null, children); + + // Layout tree: clamp to reasonable height + const ROW_HEIGHT = 22; + const MAX_ROWS = 8; + const totalRows = commentElements.length + children.length; + const treeHeight = Math.min(totalRows * ROW_HEIGHT, MAX_ROWS * ROW_HEIGHT); + tree.layout(treeHeight, 380); + treeContainer.style.height = `${treeHeight}px`; return { content: hoverElement, style: HoverStyle.Pointer, - position: { hoverPosition: HoverPosition.BELOW }, + position: { hoverPosition: HoverPosition.ABOVE }, trapFocus: true, + appearance: { compact: true }, + additionalClasses: ['agent-feedback-hover-container'], dispose: () => disposables.dispose(), }; } - private _goToFeedback(resourceUri: URI, range: IRange): void { - this._editorService.openEditor({ - resource: resourceUri, - options: { - selection: range, - preserveFocus: false, - revealIfVisible: true, + private _buildTreeData(): { children: IObjectTreeElement[]; commentElements: IFeedbackCommentElement[] } { + // Group feedback items by file + const byFile = new Map(); + for (const item of this._attachment.feedbackItems) { + const key = item.resourceUri.toString(); + let group = byFile.get(key); + if (!group) { + group = { uri: item.resourceUri, comments: [] }; + byFile.set(key, group); } - }); + group.comments.push({ + type: 'comment', + id: item.id, + text: item.text, + resourceUri: item.resourceUri, + range: item.range, + }); + } + + const children: IObjectTreeElement[] = []; + const allComments: IFeedbackCommentElement[] = []; + + for (const [, group] of byFile) { + const fileElement: IFeedbackFileElement = { + type: 'file', + uri: group.uri, + items: group.comments, + }; + + allComments.push(...group.comments); + + children.push({ + element: fileElement, + collapsible: true, + collapsed: false, + children: group.comments.map(comment => ({ + element: comment, + collapsible: false, + })), + }); + } + + return { children, commentElements: allComments }; } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts index 63f29606303a3..119bbad2fc0ae 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts @@ -8,7 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../editor/common/model.js'; +import { TrackedRangeStickiness } from '../../../../editor/common/model.js'; import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; import { Range } from '../../../../editor/common/core/range.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -20,12 +20,6 @@ import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browse import { getSessionForResource } from './agentFeedbackEditorUtils.js'; import { Selection } from '../../../../editor/common/core/selection.js'; -const feedbackLineDecoration = ModelDecorationOptions.register({ - description: 'agent-feedback-line-decoration', - linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.comment)} agent-feedback-line-decoration`, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, -}); - const addFeedbackHintDecoration = ModelDecorationOptions.register({ description: 'agent-feedback-add-hint', linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, @@ -36,8 +30,6 @@ export class AgentFeedbackLineDecorationContribution extends Disposable implemen static readonly ID = 'agentFeedback.lineDecorationContribution'; - private readonly _feedbackDecorations; - private _hintDecorationId: string | null = null; private _hintLine = -1; private _sessionResource: URI | undefined; @@ -51,22 +43,20 @@ export class AgentFeedbackLineDecorationContribution extends Disposable implemen ) { super(); - this._feedbackDecorations = this._editor.createDecorationsCollection(); - - this._store.add(this._agentFeedbackService.onDidChangeFeedback(() => this._updateFeedbackDecorations())); + this._store.add(this._agentFeedbackService.onDidChangeFeedback(() => this._updateFeedbackLines())); this._store.add(this._editor.onDidChangeModel(() => this._onModelChanged())); this._store.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onMouseMove(e))); this._store.add(this._editor.onMouseLeave(() => this._updateHintDecoration(-1))); this._store.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onMouseDown(e))); this._resolveSession(); - this._updateFeedbackDecorations(); + this._updateFeedbackLines(); } private _onModelChanged(): void { this._updateHintDecoration(-1); this._resolveSession(); - this._updateFeedbackDecorations(); + this._updateFeedbackLines(); } private _resolveSession(): void { @@ -78,15 +68,13 @@ export class AgentFeedbackLineDecorationContribution extends Disposable implemen this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); } - private _updateFeedbackDecorations(): void { + private _updateFeedbackLines(): void { if (!this._sessionResource) { - this._feedbackDecorations.clear(); this._feedbackLines.clear(); return; } const feedbackItems = this._agentFeedbackService.getFeedback(this._sessionResource); - const decorations: IModelDeltaDecoration[] = []; const lines = new Set(); for (const item of feedbackItems) { @@ -95,16 +83,10 @@ export class AgentFeedbackLineDecorationContribution extends Disposable implemen continue; } - const line = item.range.startLineNumber; - lines.add(line); - decorations.push({ - range: new Range(line, 1, line, 1), - options: feedbackLineDecoration, - }); + lines.add(item.range.startLineNumber); } this._feedbackLines = lines; - this._feedbackDecorations.set(decorations); } private _onMouseMove(e: IEditorMouseEvent): void { @@ -179,7 +161,6 @@ export class AgentFeedbackLineDecorationContribution extends Disposable implemen } override dispose(): void { - this._feedbackDecorations.clear(); this._updateHintDecoration(-1); super.dispose(); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 4813a045bafec..c324cca0f1afd 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -13,6 +13,7 @@ import { isEqual } from '../../../../base/common/resources.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { agentSessionContainsResource, editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; // --- Types -------------------------------------------------------------------- @@ -64,6 +65,11 @@ export interface IAgentFeedbackService { */ getMostRecentSessionForResource(resourceUri: URI): URI | undefined; + /** + * Set the navigation anchor to a specific feedback item, open its editor, and fire a navigation event. + */ + revealFeedback(sessionResource: URI, feedbackId: string): Promise; + /** * Navigate to next/previous feedback item in a session. */ @@ -100,6 +106,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe constructor( @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + @IEditorService private readonly _editorService: IEditorService, ) { super(); } @@ -119,7 +126,35 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe range, sessionResource, }; - feedbackItems.push(feedback); + + // Insert at the correct sorted position. + // Files are grouped by recency: first feedback for a new file appears after + // all existing files. Within a file, items are sorted by startLineNumber. + const resourceStr = resourceUri.toString(); + const hasExistingForFile = feedbackItems.some(f => f.resourceUri.toString() === resourceStr); + + if (!hasExistingForFile) { + // New file — append at the end + feedbackItems.push(feedback); + } else { + // Find insertion point: after the last item for a different file that + // precedes this file's block, then within this file's block by line number. + let insertIdx = feedbackItems.length; + for (let i = 0; i < feedbackItems.length; i++) { + if (feedbackItems[i].resourceUri.toString() === resourceStr + && feedbackItems[i].range.startLineNumber > range.startLineNumber) { + insertIdx = i; + break; + } + // If we passed the last item for this file without finding a larger + // line number, insert right after the file's block. + if (feedbackItems[i].resourceUri.toString() === resourceStr) { + insertIdx = i + 1; + } + } + feedbackItems.splice(insertIdx, 0, feedback); + } + this._sessionUpdatedOrder.set(key, ++this._sessionUpdatedSequence); this._onDidChangeNavigation.fire(sessionResource); @@ -208,6 +243,24 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe return false; } + async revealFeedback(sessionResource: URI, feedbackId: string): Promise { + const key = sessionResource.toString(); + const feedbackItems = this._feedbackBySession.get(key); + const feedback = feedbackItems?.find(f => f.id === feedbackId); + if (!feedback) { + return; + } + await this._editorService.openEditor({ + resource: feedback.resourceUri, + options: { + preserveFocus: false, + revealIfVisible: true, + } + }); + this._navigationAnchorBySession.set(key, feedbackId); + this._onDidChangeNavigation.fire(sessionResource); + } + getNextFeedback(sessionResource: URI, next: boolean): IAgentFeedback | undefined { const key = sessionResource.toString(); const feedbackItems = this._feedbackBySession.get(key); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackAttachment.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackAttachment.css index c67d88f542272..2121c65ecf0c0 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackAttachment.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackAttachment.css @@ -4,83 +4,44 @@ *--------------------------------------------------------------------------------------------*/ .agent-feedback-hover { - max-width: 400px; - padding: 4px 0; + width: 200px; } -.agent-feedback-hover-title { - font-weight: bold; - font-size: 12px; - padding: 2px 8px 6px; - border-bottom: 1px solid var(--vscode-editorWidget-border); - margin-bottom: 4px; -} - -.agent-feedback-hover-list { - max-height: 300px; - overflow-y: auto; -} - -.agent-feedback-hover-file-header { - font-size: 11px; - font-weight: bold; - color: var(--vscode-foreground); - padding: 6px 8px 2px; - font-family: var(--monaco-monospace-font); +.agent-feedback-hover-container .hover-contents { + padding: 0 !important; } -.agent-feedback-hover-file-header:not(:first-child) { - border-top: 1px solid var(--vscode-editorWidget-border); - margin-top: 4px; - padding-top: 8px; +/* Tree container */ +.agent-feedback-hover-tree { + overflow: hidden; } -.agent-feedback-hover-row { - padding: 4px 8px; +/* Comment row inside tree */ +.agent-feedback-hover-comment-row { display: flex; align-items: center; - gap: 4px; - border-radius: 4px; + width: 100%; cursor: pointer; - position: relative; -} - -.agent-feedback-hover-row:hover { - background-color: var(--vscode-list-hoverBackground); } -.agent-feedback-hover-line { - font-size: 11px; - color: var(--vscode-descriptionForeground); -} - -.agent-feedback-hover-text { +.agent-feedback-hover-comment-text { font-size: 12px; - white-space: pre-wrap; - word-break: break-word; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; flex: 1; } -.agent-feedback-hover-remove { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: var(--vscode-descriptionForeground); - opacity: 0; +/* Action bar: hidden by default, shown on row hover */ +.agent-feedback-hover-action-bar { + display: none; flex-shrink: 0; - width: 20px; - height: 20px; - border-radius: 4px; + margin-left: auto; + padding-right: 4px; } -.agent-feedback-hover-row:hover .agent-feedback-hover-remove { - opacity: 1; -} - -.agent-feedback-hover-remove:hover { - color: var(--vscode-foreground); - background-color: var(--vscode-toolbar-hoverBackground); +.agent-feedback-hover-tree .monaco-list-row:hover .agent-feedback-hover-action-bar { + display: flex; } /* Attachment widget pill styling */ diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css index 5212171ceae4c..62b05cc303cec 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css @@ -5,28 +5,44 @@ .agent-feedback-input-widget { position: absolute; - z-index: 100; - background-color: var(--vscode-editorWidget-background); - border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); + z-index: 10000; + background-color: var(--vscode-panel-background); + border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); box-shadow: 0 2px 8px var(--vscode-widget-shadow); - border-radius: 4px; + border-radius: 8px; padding: 4px; } -.agent-feedback-input-widget input { - background-color: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border, transparent); +.agent-feedback-input-widget textarea { + background-color: var(--vscode-panel-background); + border: none; color: var(--vscode-input-foreground); - border-radius: 2px; - padding: 2px 6px; + border-radius: 4px; + padding: none; outline: none; - width: 240px; + min-width: 150px; + max-width: 400px; + resize: none; + overflow-y: hidden; + white-space: pre-wrap; + word-wrap: break-word; + box-sizing: border-box; + display: block; } -.agent-feedback-input-widget input:focus { +.agent-feedback-input-widget textarea:focus { border-color: var(--vscode-focusBorder); + outline: none !important; } -.agent-feedback-input-widget input::placeholder { +.agent-feedback-input-widget textarea::placeholder { color: var(--vscode-input-placeholderForeground); } + +.agent-feedback-input-widget .agent-feedback-input-measure { + position: absolute; + visibility: hidden; + height: 0; + overflow: hidden; + white-space: pre; +} diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css new file mode 100644 index 0000000000000..c74d158efffbf --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Main widget container - speech bubble style */ +.agent-feedback-widget { + position: absolute; + max-width: 280px; + min-width: 180px; + background-color: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); + border-radius: 8px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + font-size: 12px; + line-height: 1.4; + opacity: 0; + transition: opacity 0.2s ease-in-out; + z-index: 10; +} + +.agent-feedback-widget.visible { + opacity: 1; +} + +.agent-feedback-widget.fadeOut { + animation: agentFeedbackFadeOut 150ms ease-out forwards; +} + +@keyframes agentFeedbackFadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +/* Arrow pointer pointing left toward the code */ +.agent-feedback-widget-arrow { + position: absolute; + left: -8px; + top: 12px; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-right: 8px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); +} + +.agent-feedback-widget.collapsed .agent-feedback-widget-arrow { + display: none; +} + +.agent-feedback-widget-arrow::after { + content: ''; + position: absolute; + left: 2px; + top: -7px; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + border-right: 7px solid var(--vscode-editorWidget-background); +} + +/* Header */ +.agent-feedback-widget-header { + display: flex; + align-items: center; + padding: 8px 10px; + border-bottom: 1px solid var(--vscode-editorWidget-border, var(--vscode-widget-border)); + border-radius: 8px 8px 0 0; + overflow: hidden; + cursor: pointer; + gap: 6px; +} + +.agent-feedback-widget.collapsed .agent-feedback-widget-header { + border-bottom: none; +} + +.agent-feedback-widget-header:hover { + background-color: var(--vscode-list-hoverBackground); +} + +/* Title */ +.agent-feedback-widget-title { + font-weight: 500; + color: var(--vscode-foreground); + white-space: nowrap; +} + +/* Spacer to push buttons to the right */ +.agent-feedback-widget-spacer { + flex: 1; +} + +/* Toggle button */ +.agent-feedback-widget-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + cursor: pointer; + color: var(--vscode-foreground); + opacity: 0.7; + transition: opacity 0.1s; +} + +.agent-feedback-widget-toggle:hover { + opacity: 1; + background-color: var(--vscode-toolbar-hoverBackground); +} + +/* Dismiss button */ +.agent-feedback-widget-dismiss { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + cursor: pointer; + color: var(--vscode-foreground); + opacity: 0.7; + transition: opacity 0.1s; +} + +.agent-feedback-widget-dismiss:hover { + opacity: 1; + background-color: var(--vscode-toolbar-hoverBackground); +} + +/* Body - collapsible */ +.agent-feedback-widget-body { + transition: max-height 0.2s ease-in-out, padding 0.2s ease-in-out; + border-radius: 0 0 8px 8px; + overflow: hidden; +} + +.agent-feedback-widget-body.collapsed { + max-height: 0; + overflow: hidden; + padding: 0; +} + +/* Individual feedback item */ +.agent-feedback-widget-item { + display: flex; + flex-direction: column; + padding: 8px 10px; + border-bottom: 1px solid var(--vscode-editorWidget-border, var(--vscode-widget-border)); + cursor: pointer; + position: relative; +} + +.agent-feedback-widget-item:last-child { + border-bottom: none; +} + +.agent-feedback-widget-item:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.agent-feedback-widget-item.focused { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); +} + +/* Line info */ +.agent-feedback-widget-line-info { + font-size: 10px; + font-weight: 600; + color: var(--vscode-descriptionForeground); + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Feedback text */ +.agent-feedback-widget-text { + color: var(--vscode-foreground); + word-wrap: break-word; +} + +/* Gutter decoration for range indicator on hover */ +.agent-feedback-widget-range-glyph { + margin-left: 8px; + z-index: 5; + border-left: 2px solid var(--vscode-editorGutter-modifiedBackground); +} diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css deleted file mode 100644 index 33bf495578f92..0000000000000 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-editor .agent-feedback-glyph, -.monaco-editor .agent-feedback-add-hint { - border-radius: 3px; - display: flex !important; - align-items: center; - justify-content: center; -} - -.monaco-editor .agent-feedback-glyph { - background-color: var(--vscode-toolbar-hoverBackground); -} - -.monaco-editor .agent-feedback-add-hint { - background-color: var(--vscode-toolbar-hoverBackground); - opacity: 0.7; -} - -.monaco-editor .agent-feedback-add-hint:hover { - opacity: 1; -} diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css index 890791c597367..6f503b0143fbb 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css @@ -9,14 +9,18 @@ display: flex !important; align-items: center; justify-content: center; + background-color: var(--vscode-editorHoverWidget-background); + cursor: pointer; + border: 1px solid var(--vscode-editorHoverWidget-border); + box-sizing: border-box; } -.monaco-editor .agent-feedback-line-decoration { - background-color: var(--vscode-toolbar-hoverBackground); +.monaco-editor .agent-feedback-line-decoration:hover, +.monaco-editor .agent-feedback-add-hint:hover { + background-color: var(--vscode-editorHoverWidget-border); } .monaco-editor .agent-feedback-add-hint { - background-color: var(--vscode-toolbar-hoverBackground); opacity: 0.7; } diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts new file mode 100644 index 0000000000000..7818c7e172d09 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { AgentFeedbackService, IAgentFeedbackService } from '../../browser/agentFeedbackService.js'; +import { IChatEditingService } from '../../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; + +function r(startLine: number, endLine: number = startLine): Range { + return new Range(startLine, 1, endLine, 1); +} + +function feedbackSummary(items: readonly { resourceUri: URI; range: { startLineNumber: number } }[]): string[] { + return items.map(f => `${f.resourceUri.path}:${f.range.startLineNumber}`); +} + +suite('AgentFeedbackService - Ordering', () => { + + const store = new DisposableStore(); + let service: IAgentFeedbackService; + let session: URI; + let fileA: URI; + let fileB: URI; + let fileC: URI; + + setup(() => { + const instantiationService = store.add(new TestInstantiationService()); + + instantiationService.stub(IChatEditingService, new class extends mock() { }); + instantiationService.stub(IAgentSessionsService, new class extends mock() { }); + + service = store.add(instantiationService.createInstance(AgentFeedbackService)); + session = URI.parse('test://session/1'); + fileA = URI.parse('file:///a.ts'); + fileB = URI.parse('file:///b.ts'); + fileC = URI.parse('file:///c.ts'); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('single file - items sorted by line number', () => { + service.addFeedback(session, fileA, r(20), 'line 20'); + service.addFeedback(session, fileA, r(5), 'line 5'); + service.addFeedback(session, fileA, r(10), 'line 10'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:5', + '/a.ts:10', + '/a.ts:20', + ]); + }); + + test('multiple files - files ordered by recency, items within file sorted by line', () => { + service.addFeedback(session, fileA, r(10), 'A:10'); + service.addFeedback(session, fileA, r(5), 'A:5'); + service.addFeedback(session, fileB, r(20), 'B:20'); + service.addFeedback(session, fileB, r(3), 'B:3'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:5', + '/a.ts:10', + '/b.ts:3', + '/b.ts:20', + ]); + }); + + test('new file appended to end', () => { + service.addFeedback(session, fileA, r(1), 'A:1'); + service.addFeedback(session, fileB, r(1), 'B:1'); + service.addFeedback(session, fileC, r(1), 'C:1'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:1', + '/b.ts:1', + '/c.ts:1', + ]); + }); + + test('adding to existing file does not change file ordering', () => { + service.addFeedback(session, fileA, r(10), 'A:10'); + service.addFeedback(session, fileB, r(10), 'B:10'); + // Add more feedback to fileA — should stay before fileB + service.addFeedback(session, fileA, r(5), 'A:5'); + service.addFeedback(session, fileA, r(20), 'A:20'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:5', + '/a.ts:10', + '/a.ts:20', + '/b.ts:10', + ]); + }); + + test('interleaved adds across files maintain file recency and line sort', () => { + service.addFeedback(session, fileA, r(30), 'A:30'); + service.addFeedback(session, fileB, r(50), 'B:50'); + service.addFeedback(session, fileA, r(10), 'A:10'); + service.addFeedback(session, fileC, r(1), 'C:1'); + service.addFeedback(session, fileB, r(5), 'B:5'); + service.addFeedback(session, fileA, r(20), 'A:20'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:10', + '/a.ts:20', + '/a.ts:30', + '/b.ts:5', + '/b.ts:50', + '/c.ts:1', + ]); + }); + + test('navigation follows sorted order', () => { + service.addFeedback(session, fileA, r(20), 'A:20'); + service.addFeedback(session, fileB, r(10), 'B:10'); + service.addFeedback(session, fileA, r(5), 'A:5'); + + // Expected order: A:5, A:20, B:10 + const first = service.getNextFeedback(session, true)!; + assert.strictEqual(first.resourceUri.path, '/a.ts'); + assert.strictEqual(first.range.startLineNumber, 5); + + const second = service.getNextFeedback(session, true)!; + assert.strictEqual(second.resourceUri.path, '/a.ts'); + assert.strictEqual(second.range.startLineNumber, 20); + + const third = service.getNextFeedback(session, true)!; + assert.strictEqual(third.resourceUri.path, '/b.ts'); + assert.strictEqual(third.range.startLineNumber, 10); + + // Wraps around + const fourth = service.getNextFeedback(session, true)!; + assert.strictEqual(fourth.resourceUri.path, '/a.ts'); + assert.strictEqual(fourth.range.startLineNumber, 5); + }); + + test('navigation bearings reflect sorted position', () => { + service.addFeedback(session, fileA, r(20), 'A:20'); + service.addFeedback(session, fileA, r(5), 'A:5'); + service.addFeedback(session, fileB, r(1), 'B:1'); + + // Before navigation, no anchor + let bearing = service.getNavigationBearing(session); + assert.strictEqual(bearing.activeIdx, -1); + assert.strictEqual(bearing.totalCount, 3); + + // Navigate to first (A:5) + service.getNextFeedback(session, true); + bearing = service.getNavigationBearing(session); + assert.strictEqual(bearing.activeIdx, 0); + + // Navigate to second (A:20) + service.getNextFeedback(session, true); + bearing = service.getNavigationBearing(session); + assert.strictEqual(bearing.activeIdx, 1); + + // Navigate to third (B:1) + service.getNextFeedback(session, true); + bearing = service.getNavigationBearing(session); + assert.strictEqual(bearing.activeIdx, 2); + }); + + test('removing feedback preserves ordering', () => { + const f1 = service.addFeedback(session, fileA, r(30), 'A:30'); + service.addFeedback(session, fileA, r(10), 'A:10'); + service.addFeedback(session, fileA, r(20), 'A:20'); + + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:10', + '/a.ts:20', + '/a.ts:30', + ]); + + service.removeFeedback(session, f1.id); + assert.deepStrictEqual(feedbackSummary(service.getFeedback(session)), [ + '/a.ts:10', + '/a.ts:20', + ]); + }); + + test('same line number items are stable', () => { + const f1 = service.addFeedback(session, fileA, r(10), 'first'); + const f2 = service.addFeedback(session, fileA, r(10), 'second'); + + const items = service.getFeedback(session); + assert.strictEqual(items[0].id, f1.id); + assert.strictEqual(items[1].id, f2.id); + }); +}); diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 8fa8db33b1c52..6ed91b344a5eb 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -20,8 +20,6 @@ import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browse import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js'; import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; -import { TERMINAL_VIEW_ID } from '../../../../workbench/contrib/terminal/common/terminal.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { Menus } from '../../../browser/menus.js'; import { BranchChatSessionAction } from './branchChatSessionAction.js'; import { RunScriptContribution } from './runScriptAction.js'; @@ -129,7 +127,6 @@ export class OpenSessionInTerminalAction extends Action2 { override async run(accessor: ServicesAccessor,): Promise { const terminalService = accessor.get(ITerminalService); - const viewsService = accessor.get(IViewsService); const sessionsManagementService = accessor.get(ISessionsManagementService); const activeSession = sessionsManagementService.activeSession.get(); @@ -142,7 +139,7 @@ export class OpenSessionInTerminalAction extends Action2 { terminalService.setActiveInstance(instance); } } - await viewsService.openView(TERMINAL_VIEW_ID, true); + await terminalService.focusActiveInstance(); } } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index dfa11fd710d40..b52ed0298df68 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -13,12 +13,13 @@ import { IQuickInputService } from '../../../../platform/quickinput/common/quick import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalInstance, ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { Menus } from '../../../browser/menus.js'; import { ISessionsConfigurationService, ISessionScript } from './sessionsConfigurationService.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; + // Menu IDs - exported for use in auxiliary bar part export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdown'); @@ -42,6 +43,9 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr private readonly _activeRunState: IObservable; + /** Maps `cwd.toString() + '\n' + script.command` to the terminal instance for reuse. */ + private readonly _scriptTerminals = new Map(); + constructor( @ITerminalService private readonly _terminalService: ITerminalService, @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, @@ -61,6 +65,15 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr return { session: activeSession, scripts, cwd }; }); + this._register(this._terminalService.onDidDisposeInstance(instance => { + for (const [key, id] of this._scriptTerminals) { + if (id === instance.instanceId) { + this._scriptTerminals.delete(key); + break; + } + } + })); + this._registerActions(); } @@ -169,16 +182,48 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } private async _runScript(cwd: URI, script: ISessionScript): Promise { - const terminal = await this._terminalService.createTerminal({ - location: TerminalLocation.Panel, - config: { - name: script.name - }, - cwd - }); + const key = this._terminalKey(cwd, script); + let terminal = this._getReusableTerminal(key); + + if (!terminal) { + terminal = await this._terminalService.createTerminal({ + location: TerminalLocation.Panel, + config: { + name: script.name + }, + cwd + }); + this._scriptTerminals.set(key, terminal.instanceId); + } + + await terminal.sendText(script.command, true); + this._terminalService.setActiveInstance(terminal); + await this._terminalService.revealActiveTerminal(); + } + + private _terminalKey(cwd: URI, script: ISessionScript): string { + return `${cwd.toString()}\n${script.command}`; + } + + private _getReusableTerminal(key: string): ITerminalInstance | undefined { + const instanceId = this._scriptTerminals.get(key); + if (instanceId === undefined) { + return undefined; + } + + const instance = this._terminalService.getInstanceFromId(instanceId); + if (!instance || instance.isDisposed || instance.exitCode !== undefined) { + this._scriptTerminals.delete(key); + return undefined; + } + + // Only reuse if the cwd hasn't changed from the initial cwd and nothing is actively running + if (instance.cwd !== instance.initialCwd || instance.hasChildProcesses) { + this._scriptTerminals.delete(key); + return undefined; + } - terminal.sendText(script.command, true); - await this._terminalService.revealTerminal(terminal); + return instance; } } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts index 9cf993aa7bb07..e4cb7c0fc57e6 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts @@ -5,7 +5,7 @@ import * as event from '../../../../../base/common/event.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; /** @@ -19,10 +19,8 @@ export interface IChatAttachmentWidgetInstance extends IDisposable { /** * Factory function type for creating attachment widgets. - * Receives the instantiation service so it can create DI-injected widget instances. */ export type ChatAttachmentWidgetFactory = ( - instantiationService: IInstantiationService, attachment: IChatRequestVariableEntry, options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, container: HTMLElement, @@ -43,7 +41,6 @@ export interface IChatAttachmentWidgetRegistry { * Returns undefined if no factory is registered for the attachment's kind. */ createWidget( - instantiationService: IInstantiationService, attachment: IChatRequestVariableEntry, options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, container: HTMLElement, @@ -68,7 +65,6 @@ export class ChatAttachmentWidgetRegistry implements IChatAttachmentWidgetRegist } createWidget( - instantiationService: IInstantiationService, attachment: IChatRequestVariableEntry, options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, container: HTMLElement, @@ -77,6 +73,6 @@ export class ChatAttachmentWidgetRegistry implements IChatAttachmentWidgetRegist if (!factory) { return undefined; } - return factory(instantiationService, attachment, options, container); + return factory(attachment, options, container); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 388ec5fb65589..be91ace42b3a5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2443,7 +2443,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); } else { - attachmentWidget = this._chatAttachmentWidgetRegistry.createWidget(this.instantiationService, attachment, options, container) + attachmentWidget = this._chatAttachmentWidgetRegistry.createWidget(attachment, options, container) ?? this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, lm, options, container, this._contextResourceLabels); } From 18594e0ae9d3bfdb671b8470cb2dce22841a467d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:22:55 +0100 Subject: [PATCH 2/7] Update src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/agentFeedbackEditorWidgetContribution.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index 3e420275939f7..5232f8633eef4 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -487,7 +487,6 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito for (const group of groups) { const widget = new AgentFeedbackEditorWidget(this._editor, group, this._agentFeedbackService, this._sessionResource); this._widgets.push(widget); - this._register(widget); widget.layout(group[0].range.startLineNumber); } From cbd59b9bd7e901f576b7e54403952088694842c8 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:23:19 +0100 Subject: [PATCH 3/7] Update src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agentFeedback/browser/media/agentFeedbackEditorInput.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css index 62b05cc303cec..f8d62a4fe28e7 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css @@ -18,7 +18,7 @@ border: none; color: var(--vscode-input-foreground); border-radius: 4px; - padding: none; + padding: 0; outline: none; min-width: 150px; max-width: 400px; From 6e91d50e716e10d08f69dab525b801e9cfa022c7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 21 Feb 2026 08:40:25 +0100 Subject: [PATCH 4/7] No way to open keybindings json from modal (fix #296682) (#296700) --- .../preferences/browser/keybindingsEditor.ts | 16 ++++++--- .../browser/media/keybindingsEditor.css | 36 ++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 6fce0410f8468..5ac483ffef20a 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -15,6 +15,7 @@ import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/h import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IAction, Action, Separator } from '../../../../base/common/actions.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -42,13 +43,13 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { MenuRegistry, MenuId, isIMenuItem } from '../../../../platform/actions/common/actions.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { WORKBENCH_BACKGROUND } from '../../../common/theme.js'; -import { IKeybindingItemEntry, IKeybindingsEditorPane } from '../../../services/preferences/common/preferences.js'; +import { IKeybindingItemEntry, IKeybindingsEditorPane, IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { keybindingsRecordKeysIcon, keybindingsSortIcon, keybindingsAddIcon, preferencesClearInputIcon, keybindingsEditIcon } from './preferencesIcons.js'; import { ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js'; import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js'; import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js'; -import { defaultKeybindingLabelStyles, defaultToggleStyles, getInputBoxStyle } from '../../../../platform/theme/browser/defaultStyles.js'; +import { defaultButtonStyles, defaultKeybindingLabelStyles, defaultToggleStyles, getInputBoxStyle } from '../../../../platform/theme/browser/defaultStyles.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { isString } from '../../../../base/common/types.js'; @@ -130,7 +131,8 @@ export class KeybindingsEditor extends EditorPane imp @IEditorService private readonly editorService: IEditorService, @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IPreferencesService private readonly preferencesService: IPreferencesService ) { super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = this._register(new Delayer(300)); @@ -364,7 +366,8 @@ export class KeybindingsEditor extends EditorPane imp const clearInputAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults())); - const searchContainer = DOM.append(this.headerContainer, $('.search-container')); + const searchRowContainer = DOM.append(this.headerContainer, $('.search-row-container')); + const searchContainer = DOM.append(searchRowContainer, $('.search-container')); this.searchWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, searchContainer, { ariaLabel: fullTextSearchPlaceholder, placeholder: fullTextSearchPlaceholder, @@ -426,6 +429,11 @@ export class KeybindingsEditor extends EditorPane imp })); toolBar.setActions(actions); this._register(this.keybindingsService.onDidUpdateKeybindings(() => toolBar.setActions(actions))); + + const openKeybindingsJsonContainer = DOM.append(searchRowContainer, $('.open-keybindings-json')); + const openKeybindingsJsonButton = this._register(new Button(openKeybindingsJsonContainer, { secondary: true, title: true, ...defaultButtonStyles })); + openKeybindingsJsonButton.label = localize('openKeybindingsJson', "Edit as JSON"); + this._register(openKeybindingsJsonButton.onDidClick(() => this.preferencesService.openGlobalKeybindingSettings(true, { groupId: this.group.id }))); } private updateSearchOptions(): void { diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css index 1078698880874..f93706d61206b 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css @@ -23,11 +23,13 @@ padding: 0px 10px 11px 0; } -.keybindings-editor > .keybindings-header > .search-container { +.keybindings-editor > .keybindings-header > .search-row-container > .search-container { position: relative; + flex: 1; + min-width: 0; } -.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container { +.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container { position: absolute; top: 0; right: 10px; @@ -35,24 +37,25 @@ display: flex; } -.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container > .recording-badge { +.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container > .recording-badge { margin-right: 8px; padding: 4px; } -.keybindings-editor > .keybindings-header.small > .search-container > .keybindings-search-actions-container > .recording-badge, -.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container > .recording-badge.disabled { +.keybindings-editor > .keybindings-header.small > .search-row-container > .search-container > .keybindings-search-actions-container > .recording-badge, +.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container > .recording-badge.disabled { display: none; } -.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item > .icon { - width:16px; +.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item > .icon { + width: 16px; height: 18px; } -.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item { +.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item { margin-right: 4px; } + .keybindings-editor .monaco-action-bar .action-item .monaco-custom-toggle { margin: 0; padding: 2px; @@ -85,6 +88,21 @@ opacity: 1; } +.keybindings-editor > .keybindings-header > .search-row-container { + display: flex; + align-items: center; + gap: 8px; +} + +.keybindings-editor > .keybindings-header > .search-row-container > .open-keybindings-json { + flex-shrink: 0; +} + +.keybindings-editor > .keybindings-header > .search-row-container > .open-keybindings-json > .monaco-button { + padding: 2px 8px; + line-height: 18px; +} + /** Table styling **/ .keybindings-editor > .keybindings-body .keybindings-table-container { @@ -117,7 +135,7 @@ } .keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table-tr .monaco-table-td .monaco-action-bar .action-item > .icon { - width:16px; + width: 16px; height: 16px; cursor: pointer; margin-top: 3px; From c442c75023f22ae07b5ef8b96e7253f2d962d7eb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 21 Feb 2026 09:23:01 +0100 Subject: [PATCH 5/7] sessions - tweaks to use of modal editors (#296698) * sessions - tweaks to use of modal editors * ccr * ffs * ffs * ffs --- .github/skills/sessions/SKILL.md | 2 +- src/vs/sessions/LAYOUT.md | 10 ++++----- .../browser/aiCustomizationOverviewView.ts | 6 +++--- .../browser/configuration.contribution.ts | 2 +- .../customizationsToolbar.contribution.ts | 6 +++--- .../parts/editor/diffEditorCommands.ts | 4 +--- .../browser/workbench.contribution.ts | 21 +++++++++++++++---- .../browser/chatEditing/chatEditingSession.ts | 4 ++-- .../languageModelsConfigurationService.ts | 6 +++--- .../chatChangesSummaryPart.ts | 4 +--- .../quickaccess/gotoLineQuickAccess.ts | 6 ++---- .../quickaccess/gotoSymbolQuickAccess.ts | 6 ++---- .../browser/userDataProfile.ts | 5 ++--- .../editor/common/editorGroupFinder.ts | 4 ++-- .../services/editor/common/editorService.ts | 2 +- .../test/browser/modalEditorGroup.test.ts | 8 +++---- .../browser/workingCopyBackupTracker.ts | 4 +--- .../common/workingCopyBackupTracker.ts | 4 +--- .../workingCopyBackupTracker.ts | 4 +--- .../browser/workingCopyBackupTracker.test.ts | 3 +-- .../workingCopyBackupTracker.test.ts | 3 +-- 21 files changed, 55 insertions(+), 59 deletions(-) diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index 4889632066bf7..fc49548b7a384 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -139,7 +139,7 @@ Use the `agent-sessions-layout` skill for detailed guidance on the layout. Key p ### 4.3 Editor Modal -The main editor part is hidden (`display:none`). All editors open via `MODAL_GROUP` into the standard `ModalEditorPart` overlay (created on-demand by `EditorParts.createModalEditorPart`). The sessions configuration sets `workbench.editor.useModal` to `'on'`, which causes `findGroup()` to redirect all editor opens to the modal. Click backdrop or press Escape to dismiss. +The main editor part is hidden (`display:none`). All editors open via `MODAL_GROUP` into the standard `ModalEditorPart` overlay (created on-demand by `EditorParts.createModalEditorPart`). The sessions configuration sets `workbench.editor.useModal` to `'all'`, which causes `findGroup()` to redirect all editor opens to the modal. Click backdrop or press Escape to dismiss. ## 5. Chat Widget diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 7cd6cc059bade..fea9a1c370b86 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -178,9 +178,9 @@ The main editor part is created but hidden (`display:none`). It exists for futur #### How It Works -The sessions configuration sets `workbench.editor.useModal` to `'on'` (in `contrib/configuration/browser/configuration.contribution.ts`). This causes `findGroup()` in `editorGroupFinder.ts` to redirect all editor opens (that do not specify an explicit preferred group) to `createModalEditorPart()`, which creates the standard workbench `ModalEditorPart` overlay on-demand. +The sessions configuration sets `workbench.editor.useModal` to `'all'` (in `contrib/configuration/browser/configuration.contribution.ts`). This causes `findGroup()` in `editorGroupFinder.ts` to redirect all editor opens (that do not specify an explicit preferred group) to `createModalEditorPart()`, which creates the standard workbench `ModalEditorPart` overlay on-demand. -When the setting is `'on'`: +When the setting is `'all'`: - All editors without an explicit preferred group open in the modal editor part - The modal is not auto-closed when editors open without explicit `MODAL_GROUP` as preferred group @@ -197,8 +197,8 @@ When the setting is `'on'`: The setting `workbench.editor.useModal` is an enum with three values: - `'off'`: Editors never open in a modal overlay -- `'default'`: Certain editors (e.g. Settings, Keyboard Shortcuts) may open in a modal overlay when requested via `MODAL_GROUP` -- `'on'`: All editors open in a modal overlay (used by sessions window) +- `'some'`: Certain editors (e.g. Settings, Keyboard Shortcuts) may open in a modal overlay when requested via `MODAL_GROUP` +- `'all'`: All editors open in a modal overlay (used by sessions window) --- @@ -640,7 +640,7 @@ interface IPartVisibilityState { | Date | Change | |------|--------| -| 2026-02-20 | Replaced custom `EditorModal` with standard `ModalEditorPart` via `MODAL_GROUP`; main editor part created but hidden; changed `workbench.editor.useModal` from boolean to enum (`off`/`default`/`on`); sessions config uses `on`; removed `editorModal.ts` and editor modal CSS | +| 2026-02-20 | Replaced custom `EditorModal` with standard `ModalEditorPart` via `MODAL_GROUP`; main editor part created but hidden; changed `workbench.editor.useModal` from boolean to enum (`off`/`some`/`all`); sessions config uses `all`; removed `editorModal.ts` and editor modal CSS | | 2026-02-17 | Added `-webkit-app-region: drag` to sidebar title area so it can be used to drag the window; interactive children (actions, composite bar, labels) marked `no-drag`; CSS rules scoped to `.agent-sessions-workbench` in `parts/media/sidebarPart.css` | | 2026-02-13 | Documentation sync: Updated all file names, class names, and references to match current implementation. `AgenticWorkbench` → `Workbench`, `AgenticSidebarPart` → `SidebarPart`, `AgenticAuxiliaryBarPart` → `AuxiliaryBarPart`, `AgenticPanelPart` → `PanelPart`, `agenticWorkbench.ts` → `workbench.ts`, `agenticWorkbenchMenus.ts` → `menus.ts`, `agenticLayoutActions.ts` → `layoutActions.ts`, `AgenticTitleBarWidget` → `SessionsTitleBarWidget`, `AgenticTitleBarContribution` → `SessionsTitleBarContribution`. Removed references to deleted files (`sidebarRevealButton.ts`, `floatingToolbar.ts`, `agentic.contributions.ts`, `agenticTitleBarWidget.ts`). Updated pane composite architecture from `SyncDescriptor`-based to `AgenticPaneCompositePartService`. Moved account widget docs from titlebar to sidebar footer. Added documentation for sidebar footer, project bar, traffic light spacer, card appearance styling, widget directory, and new contrib structure (`accountMenu/`, `chat/`, `configuration/`, `sessions/`). Updated titlebar actions to reflect Run Script split button and Open submenu. Removed Toggle Maximize panel action (no longer registered). Updated contributions section with all current contributions and their locations. | | 2026-02-13 | Changed grid structure: sidebar now spans full window height at root level (HORIZONTAL root orientation); Titlebar moved inside right section; Grid is now `Sidebar \| [Titlebar / TopRight / Panel]` instead of `Titlebar / [Sidebar \| RightSection]`; Panel maximize now excludes both titlebar and sidebar; Floating toolbar positioning no longer depends on titlebar height | diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index 0eedb76a519eb..36a04c2ebc0fd 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -19,7 +19,6 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; @@ -28,6 +27,7 @@ import { AICustomizationManagementEditor } from '../../../../workbench/contrib/c import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; const $ = DOM.$; @@ -63,7 +63,7 @@ export class AICustomizationOverviewView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IEditorService private readonly editorService: IEditorService, @IPromptsService private readonly promptsService: IPromptsService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @@ -187,7 +187,7 @@ export class AICustomizationOverviewView extends ViewPane { private async openSection(sectionId: AICustomizationManagementSection): Promise { const input = AICustomizationManagementEditorInput.getOrCreate(); - const editor = await this.editorGroupsService.activeGroup.openEditor(input, { pinned: true }); + const editor = await this.editorService.openEditor(input, { pinned: true }); // Deep-link to the section if (editor instanceof AICustomizationManagementEditor) { diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 73601b49ce12d..060a1b2e7d39c 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -36,7 +36,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.startupEditor': 'none', 'workbench.tips.enabled': false, 'workbench.layoutControl.type': 'toggles', - 'workbench.editor.useModal': 'on', + 'workbench.editor.useModal': 'all', 'workbench.editor.labelFormat': 'short', 'window.menuStyle': 'custom', 'window.dialogStyle': 'custom', diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index c4a021a04d242..f2e6d9e45e71b 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -13,7 +13,6 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; @@ -32,6 +31,7 @@ import { ISessionsManagementService } from './sessionsManagementService.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { getPromptSourceCounts, getSkillSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; interface ICustomizationItemConfig { readonly id: string; @@ -250,9 +250,9 @@ class CustomizationsToolbarContribution extends Disposable implements IWorkbench }); } async run(accessor: ServicesAccessor): Promise { - const editorGroupsService = accessor.get(IEditorGroupsService); + const editorService = accessor.get(IEditorService); const input = AICustomizationManagementEditorInput.getOrCreate(); - const editor = await editorGroupsService.activeGroup.openEditor(input, { pinned: true }); + const editor = await editorService.openEditor(input, { pinned: true }); if (editor instanceof AICustomizationManagementEditor) { editor.selectSectionById(config.section); } diff --git a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index ca925a7e8241e..5c28b20e3956d 100644 --- a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -18,7 +18,6 @@ import { DiffEditorInput } from '../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IUntypedEditorInput } from '../../../common/editor.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -40,7 +39,6 @@ export function registerDiffEditorCommands(): void { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.KeyO), handler: async accessor => { const editorService = accessor.get(IEditorService); - const editorGroupsService = accessor.get(IEditorGroupsService); const activeEditor = editorService.activeEditor; const activeTextEditorControl = editorService.activeTextEditorControl; @@ -56,7 +54,7 @@ export function registerDiffEditorCommands(): void { editor = activeEditor.modified; } - return editorGroupsService.activeGroup.openEditor(editor); + return editorService.openEditor(editor); } }); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index a14964c0ef4b4..017648b71fd8e 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -351,14 +351,14 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.editor.useModal': { 'type': 'string', - 'enum': ['off', 'default', 'on'], + 'enum': ['off', 'some', 'all'], 'enumDescriptions': [ localize('useModal.off', "Editors never open in a modal overlay."), - localize('useModal.default', "Certain editors such as Settings and Keyboard Shortcuts may open in a centered modal overlay."), - localize('useModal.on', "All editors open in a centered modal overlay."), + localize('useModal.some', "Certain editors such as Settings and Keyboard Shortcuts may open in a centered modal overlay."), + localize('useModal.all', "All editors open in a centered modal overlay."), ], 'description': localize('useModal', "Controls whether editors open in a modal overlay."), - 'default': product.quality !== 'stable' ? 'default' : 'off', + 'default': product.quality !== 'stable' ? 'some' : 'off', tags: ['experimental'], experiment: { mode: 'auto' @@ -1035,3 +1035,16 @@ Registry.as(Extensions.ConfigurationMigration) return result; } }]); + +Registry.as(Extensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'workbench.editor.useModal', migrateFn: (value: unknown) => { + const result: ConfigurationKeyValuePairs = []; + if (value === 'default') { + result.push(['workbench.editor.useModal', { value: 'some' }]); + } else if (value === 'on') { + result.push(['workbench.editor.useModal', { value: 'all' }]); + } + return result; + } + }]); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 2c2b1b782a598..8439b5518b737 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -442,7 +442,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio if (this._editorPane.isVisible()) { return; } else if (this._editorPane.input) { - await this._editorGroupsService.activeGroup.openEditor(this._editorPane.input, { pinned: true, activation: EditorActivation.ACTIVATE }); + await this._editorService.openEditor(this._editorPane.input, { pinned: true, activation: EditorActivation.ACTIVATE }); return; } } @@ -451,7 +451,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio label: localize('multiDiffEditorInput.name', "Suggested Edits") }, this._instantiationService); - this._editorPane = await this._editorGroupsService.activeGroup.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined; + this._editorPane = await this._editorService.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined; } private _stopPromise: Promise | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts index 1c695e2ded0d0..f89cca85b672e 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts @@ -10,7 +10,7 @@ import { Mutable } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ITextEditorService } from '../../../services/textfile/common/textEditorService.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { equals } from '../../../../base/common/objects.js'; @@ -46,7 +46,7 @@ export class LanguageModelsConfigurationService extends Disposable implements IL @IFileService private readonly fileService: IFileService, @ITextFileService private readonly textFileService: ITextFileService, @ITextModelService private readonly textModelService: ITextModelService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IEditorService private readonly editorService: IEditorService, @ITextEditorService private readonly textEditorService: ITextEditorService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -150,7 +150,7 @@ export class LanguageModelsConfigurationService extends Disposable implements IL } async configureLanguageModels(options?: ConfigureLanguageModelsOptions): Promise { - const editor = await this.editorGroupsService.activeGroup.openEditor(this.textEditorService.createTextEditor({ resource: this.modelsConfigurationFile })); + const editor = await this.editorService.openEditor(this.textEditorService.createTextEditor({ resource: this.modelsConfigurationFile })); if (!editor || !options?.group) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts index 4fd9d3e5337a0..cc7bb156742e9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts @@ -21,7 +21,6 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { WorkbenchList } from '../../../../../../platform/list/browser/listService.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { IResourceLabel, ResourceLabels } from '../../../../../browser/labels.js'; -import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { createFileIconThemableTreeContainerScope } from '../../../../files/browser/views/explorerView.js'; import { MultiDiffEditorInput } from '../../../../multiDiffEditor/browser/multiDiffEditorInput.js'; @@ -53,7 +52,6 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl @IHoverService private readonly hoverService: IHoverService, @IChatService private readonly chatService: IChatService, @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -139,7 +137,7 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl }), false ); - this.editorGroupsService.activeGroup.openEditor(input); + this.editorService.openEditor(input); dom.EventHelper.stop(e, true); })); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 769547a689145..a32cf22280bc7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -6,7 +6,7 @@ import { Event } from '../../../../../base/common/event.js'; import { localize, localize2 } from '../../../../../nls.js'; import { IKeyMods, IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js'; import { IRange } from '../../../../../editor/common/core/range.js'; import { AbstractGotoLineQuickAccessProvider } from '../../../../../editor/contrib/quickAccess/browser/gotoLineQuickAccess.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -19,7 +19,6 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickAccessTextEditorContext } from '../../../../../editor/contrib/quickAccess/browser/editorNavigationQuickAccess.js'; import { ITextEditorOptions } from '../../../../../platform/editor/common/editor.js'; -import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IStorageService } from '../../../../../platform/storage/common/storage.js'; export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { @@ -28,7 +27,6 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv constructor( @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService protected override readonly storageService: IStorageService ) { @@ -60,7 +58,7 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv preserveFocus: options.preserveFocus }; - this.editorGroupService.sideGroup.openEditor(this.editorService.activeEditor, editorOptions); + this.editorService.openEditor(this.editorService.activeEditor, editorOptions, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index d8dea5ca723eb..d7ba992c32dcc 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -6,7 +6,7 @@ import { Event } from '../../../../../base/common/event.js'; import { localize, localize2 } from '../../../../../nls.js'; import { IKeyMods, IQuickPickSeparator, IQuickInputService, IQuickPick, ItemActivation } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js'; import { IRange } from '../../../../../editor/common/core/range.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from '../../../../../platform/quickinput/common/quickAccess.js'; @@ -29,7 +29,6 @@ import { IQuickAccessTextEditorContext } from '../../../../../editor/contrib/qui import { IOutlineService, OutlineTarget } from '../../../../services/outline/browser/outline.js'; import { isCompositeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ITextEditorOptions } from '../../../../../platform/editor/common/editor.js'; -import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -42,7 +41,6 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess constructor( @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IOutlineService private readonly outlineService: IOutlineService, @@ -90,7 +88,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess preserveFocus: options.preserveFocus }; - this.editorGroupService.sideGroup.openEditor(this.editorService.activeEditor, editorOptions); + this.editorService.openEditor(this.editorService.activeEditor, editorOptions, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 48652f3e747b6..1368ec8f48259 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -26,7 +26,6 @@ import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/edit import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEditorInputSerializer } from './userDataProfilesEditor.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -133,14 +132,14 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements async handleDrop(resource: URI, accessor: ServicesAccessor): Promise { const uriIdentityService = accessor.get(IUriIdentityService); const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); - const editorGroupsService = accessor.get(IEditorGroupsService); + const editorService = accessor.get(IEditorService); const textEditorService = accessor.get(ITextEditorService); const notificationService = accessor.get(INotificationService); if (uriIdentityService.extUri.extname(resource) === `.${PROFILE_EXTENSION}`) { const template = await userDataProfileImportExportService.resolveProfileTemplate(resource); if (!template) { notificationService.warn(localize('invalid profile', "The dropped profile is invalid.")); - editorGroupsService.activeGroup.openEditor(textEditorService.createTextEditor({ resource })); + editorService.openEditor(textEditorService.createTextEditor({ resource })); return true; } const editor = await that.openProfilesEditor(); diff --git a/src/vs/workbench/services/editor/common/editorGroupFinder.ts b/src/vs/workbench/services/editor/common/editorGroupFinder.ts index af149b8102874..96f6c8d112fc3 100644 --- a/src/vs/workbench/services/editor/common/editorGroupFinder.ts +++ b/src/vs/workbench/services/editor/common/editorGroupFinder.ts @@ -38,7 +38,7 @@ export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOpt function handleGroupResult(group: IEditorGroup, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined, editorGroupService: IEditorGroupsService, configurationService: IConfigurationService): [IEditorGroup, EditorActivation | undefined] { const modalEditorPart = editorGroupService.activeModalEditorPart; const modalEditorMode = configurationService.getValue('workbench.editor.useModal'); - if (modalEditorPart && preferredGroup !== MODAL_GROUP && modalEditorMode !== 'on') { + if (modalEditorPart && preferredGroup !== MODAL_GROUP && modalEditorMode !== 'all') { // Only allow to open in modal group if MODAL_GROUP is explicitly requested group = handleModalEditorPart(group, editor, modalEditorPart, editorGroupService); } @@ -176,7 +176,7 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer } // Force modal editor part: redirect to the modal group when setting is 'on' - if (!group && configurationService.getValue('workbench.editor.useModal') === 'on') { + if (!group && configurationService.getValue('workbench.editor.useModal') === 'all') { group = editorGroupService.createModalEditorPart(options?.modal) .then(part => part.activeGroup); } diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 5c7b3802c2e92..c0e94a215a2ac 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -270,7 +270,7 @@ export interface IEditorService { openEditor(editor: IUntypedEditorInput, group?: PreferredGroup): Promise; /** - * @deprecated using this method is a sign that your editor has not adopted the editor + * Using this method is a sign that your editor has not adopted the editor * resolver yet. Please use `IEditorResolverService.registerEditor` to make your editor * known to the workbench and then use untyped editor inputs for opening: * diff --git a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts index 1c0db5ecf5835..fe6a33802b2fe 100644 --- a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts +++ b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts @@ -506,13 +506,13 @@ suite('Modal Editor Group', () => { modalPart.close(); }); - suite('useModal: on', () => { + suite('useModal: all', () => { test('findGroup creates modal and returns its active group', async () => { const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); const configurationService = new TestConfigurationService(); - await configurationService.setUserConfiguration('workbench.editor.useModal', 'on'); + await configurationService.setUserConfiguration('workbench.editor.useModal', 'all'); instantiationService.stub(IConfigurationService, configurationService); const parts = await createEditorParts(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, parts); @@ -536,7 +536,7 @@ suite('Modal Editor Group', () => { const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); const configurationService = new TestConfigurationService(); - await configurationService.setUserConfiguration('workbench.editor.useModal', 'on'); + await configurationService.setUserConfiguration('workbench.editor.useModal', 'all'); instantiationService.stub(IConfigurationService, configurationService); const parts = await createEditorParts(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, parts); @@ -559,7 +559,7 @@ suite('Modal Editor Group', () => { modalPart.close(); }); - test('findGroup auto-closes modal when setting is not on', async () => { + test('findGroup auto-closes modal when setting is not all', async () => { const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); const configurationService = new TestConfigurationService(); diff --git a/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts index a0b33b253bed7..b57cf4c650703 100644 --- a/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts @@ -12,7 +12,6 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { WorkingCopyBackupTracker } from '../common/workingCopyBackupTracker.js'; import { IWorkingCopyEditorService } from '../common/workingCopyEditorService.js'; import { IEditorService } from '../../editor/common/editorService.js'; -import { IEditorGroupsService } from '../../editor/common/editorGroupsService.js'; export class BrowserWorkingCopyBackupTracker extends WorkingCopyBackupTracker implements IWorkbenchContribution { @@ -26,9 +25,8 @@ export class BrowserWorkingCopyBackupTracker extends WorkingCopyBackupTracker im @ILogService logService: ILogService, @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(workingCopyBackupService, workingCopyService, logService, lifecycleService, filesConfigurationService, workingCopyEditorService, editorService, editorGroupService); + super(workingCopyBackupService, workingCopyService, logService, lifecycleService, filesConfigurationService, workingCopyEditorService, editorService); } protected onFinalBeforeShutdown(reason: ShutdownReason): boolean { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts index e032ebe1bdb70..eb7608d9fe17d 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts @@ -16,7 +16,6 @@ import { Promises } from '../../../../base/common/async.js'; import { IEditorService } from '../../editor/common/editorService.js'; import { EditorsOrder } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; -import { IEditorGroupsService } from '../../editor/common/editorGroupsService.js'; /** * The working copy backup tracker deals with: @@ -35,7 +34,6 @@ export abstract class WorkingCopyBackupTracker extends Disposable { protected readonly filesConfigurationService: IFilesConfigurationService, private readonly workingCopyEditorService: IWorkingCopyEditorService, protected readonly editorService: IEditorService, - private readonly editorGroupService: IEditorGroupsService ) { super(); @@ -397,7 +395,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Ensure editors are opened for each backup without editor // in the background without stealing focus if (nonOpenedEditorsForBackups.size > 0) { - await this.editorGroupService.activeGroup.openEditors([...nonOpenedEditorsForBackups].map(nonOpenedEditorForBackup => ({ + await this.editorService.openEditors([...nonOpenedEditorsForBackups].map(nonOpenedEditorForBackup => ({ editor: nonOpenedEditorForBackup, options: { pinned: true, diff --git a/src/vs/workbench/services/workingCopy/electron-browser/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-browser/workingCopyBackupTracker.ts index ff7f0885ed42f..6e99537a4a64f 100644 --- a/src/vs/workbench/services/workingCopy/electron-browser/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-browser/workingCopyBackupTracker.ts @@ -24,7 +24,6 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { Promises, raceCancellation } from '../../../../base/common/async.js'; import { IWorkingCopyEditorService } from '../common/workingCopyEditorService.js'; -import { IEditorGroupsService } from '../../editor/common/editorGroupsService.js'; export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker implements IWorkbenchContribution { @@ -44,9 +43,8 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp @IProgressService private readonly progressService: IProgressService, @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(workingCopyBackupService, workingCopyService, logService, lifecycleService, filesConfigurationService, workingCopyEditorService, editorService, editorGroupService); + super(workingCopyBackupService, workingCopyService, logService, lifecycleService, filesConfigurationService, workingCopyEditorService, editorService); } protected async onFinalBeforeShutdown(reason: ShutdownReason): Promise { diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index f86b9a6adec33..3288604f0a95d 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -54,9 +54,8 @@ suite('WorkingCopyBackupTracker (browser)', function () { @ILogService logService: ILogService, @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, logService, workingCopyEditorService, editorService, editorGroupService); + super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, logService, workingCopyEditorService, editorService); } protected override getBackupScheduleDelay(): number { diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts index a7ab03c128963..bc7ce7b557ff2 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts @@ -64,9 +64,8 @@ suite('WorkingCopyBackupTracker (native)', function () { @IEnvironmentService environmentService: IEnvironmentService, @IProgressService progressService: IProgressService, @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, environmentService, progressService, workingCopyEditorService, editorService, editorGroupService); + super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, environmentService, progressService, workingCopyEditorService, editorService); } protected override getBackupScheduleDelay(): number { From a42bcd7c49658ee409f23f4378a0fe1141da0987 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 21 Feb 2026 09:23:21 +0100 Subject: [PATCH 6/7] sessions - hide sessions picker in aux windows (#296703) --- .../contrib/sessions/browser/sessionsTitleBarWidget.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 526fdb358d98e..e21c25d56e9e7 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -30,6 +30,7 @@ import { getAgentSessionProvider, getAgentSessionProviderIcon } from '../../../. import { basename } from '../../../../base/common/resources.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ViewAllSessionChangesAction } from '../../../../workbench/contrib/chat/browser/chatEditing/chatEditingActions.js'; +import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; /** * Sessions Title Bar Widget - renders the active chat session title @@ -380,6 +381,7 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben submenu: Menus.TitleBarControlMenu, title: localize('agentSessionsControl', "Agent Sessions"), order: 101, + when: IsAuxiliaryWindowContext.negate() })); // Register a placeholder action so the submenu appears @@ -389,7 +391,8 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben title: localize('showSessions', "Show Sessions"), }, group: 'a_sessions', - order: 1 + order: 1, + when: IsAuxiliaryWindowContext.negate() })); this._register(actionViewItemService.register(Menus.CommandCenter, Menus.TitleBarControlMenu, (action, options) => { From 367b0f3e4f952622536f83b17ef12da5b2dcdae0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:45:22 +0100 Subject: [PATCH 7/7] Update src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/agentFeedback/browser/agentFeedbackHover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts index 8924068b591a4..c5ee181fa94b5 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts @@ -299,7 +299,7 @@ export class AgentFeedbackHover extends Disposable { const MAX_ROWS = 8; const totalRows = commentElements.length + children.length; const treeHeight = Math.min(totalRows * ROW_HEIGHT, MAX_ROWS * ROW_HEIGHT); - tree.layout(treeHeight, 380); + tree.layout(treeHeight, 200); treeContainer.style.height = `${treeHeight}px`; return {