From 8ff7aae7ab18012adb665dee6d0d4e2b68c59bfc Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 20 Feb 2026 00:12:51 +0100 Subject: [PATCH] sessions polish --- .../browser/account.contribution.ts | 1 - .../browser/agentFeedback.contribution.ts | 16 +- .../browser/agentFeedbackAttachment.ts | 2 +- .../agentFeedbackGlyphMarginContribution.ts | 21 +- ...agentFeedbackLineDecorationContribution.ts | 188 ++++++++++++++++++ .../browser/agentFeedbackService.ts | 36 ---- .../media/agentFeedbackGlyphMargin.css | 13 +- .../media/agentFeedbackLineDecoration.css | 25 +++ .../browser/media/customizationsToolbar.css | 3 +- .../chatAttachmentWidgetRegistry.ts | 82 ++++++++ .../contrib/chat/browser/chat.contribution.ts | 2 + .../browser/widget/input/chatInputPart.ts | 5 +- 12 files changed, 334 insertions(+), 60 deletions(-) create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css create mode 100644 src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts diff --git a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts index 9b4701fe419..cbff23edc4c 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts +++ b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts @@ -307,7 +307,6 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu when: ContextKeyExpr.or( CONTEXT_UPDATE_STATE.isEqualTo(StateType.Ready), CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload), - CONTEXT_UPDATE_STATE.isEqualTo(StateType.CheckingForUpdates), CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloading), CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloaded), CONTEXT_UPDATE_STATE.isEqualTo(StateType.Updating), diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts index 82acbdb1c5b..6bb6704f602 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts @@ -4,13 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import './agentFeedbackEditorInputContribution.js'; -import './agentFeedbackGlyphMarginContribution.js'; +import './agentFeedbackLineDecorationContribution.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { AgentFeedbackService, IAgentFeedbackService } from './agentFeedbackService.js'; import { AgentFeedbackAttachmentContribution } from './agentFeedbackAttachment.js'; +import { AgentFeedbackAttachmentWidget } from './agentFeedbackAttachmentWidget.js'; import { AgentFeedbackEditorOverlay } from './agentFeedbackEditorOverlay.js'; import { registerAgentFeedbackEditorActions } from './agentFeedbackEditorActions.js'; +import { IChatAttachmentWidgetRegistry } from '../../../../workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.js'; +import { IAgentFeedbackVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; registerWorkbenchContribution2(AgentFeedbackEditorOverlay.ID, AgentFeedbackEditorOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentFeedbackAttachmentContribution.ID, AgentFeedbackAttachmentContribution, WorkbenchPhase.AfterRestored); @@ -18,3 +21,14 @@ registerWorkbenchContribution2(AgentFeedbackAttachmentContribution.ID, AgentFeed registerAgentFeedbackEditorActions(); registerSingleton(IAgentFeedbackService, AgentFeedbackService, InstantiationType.Delayed); + +// 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) => { + return instantiationService.createInstance(AgentFeedbackAttachmentWidget, attachment as IAgentFeedbackVariableEntry, options, container); + }); + } +} +registerWorkbenchContribution2(AgentFeedbackAttachmentWidgetContribution.ID, AgentFeedbackAttachmentWidgetContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts index 720eb60b9e2..e45f96e488d 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts @@ -14,7 +14,7 @@ import { IAgentFeedbackService, IAgentFeedback } from './agentFeedbackService.js import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IAgentFeedbackVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; -const ATTACHMENT_ID_PREFIX = 'agentFeedback:'; +export const ATTACHMENT_ID_PREFIX = 'agentFeedback:'; /** * Keeps the "N feedback items" attachment in the chat input in sync with the diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts index 6436e692fc2..bc7805d4b17 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.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 { GlyphMarginLane, IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../editor/common/model.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'; @@ -20,19 +20,15 @@ import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browse import { getSessionForResource } from './agentFeedbackEditorUtils.js'; import { Selection } from '../../../../editor/common/core/selection.js'; -const GLYPH_MARGIN_LANE = GlyphMarginLane.Left; - const feedbackGlyphDecoration = ModelDecorationOptions.register({ description: 'agent-feedback-glyph', - glyphMarginClassName: `${ThemeIcon.asClassName(Codicon.comment)} agent-feedback-glyph`, - glyphMargin: { position: GLYPH_MARGIN_LANE }, + linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.comment)} agent-feedback-glyph`, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }); const addFeedbackHintDecoration = ModelDecorationOptions.register({ description: 'agent-feedback-add-hint', - glyphMarginClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, - glyphMargin: { position: GLYPH_MARGIN_LANE }, + linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }); @@ -117,9 +113,10 @@ export class AgentFeedbackGlyphMarginContribution extends Disposable implements 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 - && e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN - && !e.target.detail.isAfterLines + && (isLineDecoration || isContentArea) && !this._feedbackLines.has(e.target.position.lineNumber) ) { this._updateHintDecoration(e.target.position.lineNumber); @@ -150,7 +147,7 @@ export class AgentFeedbackGlyphMarginContribution extends Disposable implements private _onMouseDown(e: IEditorMouseEvent): void { if (!e.target.position - || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN + || e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS || e.target.detail.isAfterLines || !this._sessionResource ) { @@ -174,9 +171,9 @@ export class AgentFeedbackGlyphMarginContribution extends Disposable implements const endColumn = model.getLineLastNonWhitespaceColumn(lineNumber); if (startColumn === 0 || endColumn === 0) { // Empty line - select the whole line range - this._editor.setSelection(new Selection(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber))); + this._editor.setSelection(new Selection(lineNumber, model.getLineMaxColumn(lineNumber), lineNumber, 1)); } else { - this._editor.setSelection(new Selection(lineNumber, startColumn, lineNumber, endColumn)); + this._editor.setSelection(new Selection(lineNumber, endColumn, lineNumber, startColumn)); } this._editor.focus(); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts new file mode 100644 index 00000000000..63f29606303 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/agentFeedbackLineDecoration.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 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`, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, +}); + +export class AgentFeedbackLineDecorationContribution extends Disposable implements IEditorContribution { + + static readonly ID = 'agentFeedback.lineDecorationContribution'; + + 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: feedbackLineDecoration, + }); + } + + 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(AgentFeedbackLineDecorationContribution.ID, AgentFeedbackLineDecorationContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 701495fc188..4813a045baf 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -13,7 +13,6 @@ 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 { IChatWidget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; // --- Types -------------------------------------------------------------------- @@ -83,8 +82,6 @@ export interface IAgentFeedbackService { // --- Implementation ----------------------------------------------------------- -const AGENT_FEEDBACK_ATTACHMENT_ID_PREFIX = 'agentFeedback:'; - export class AgentFeedbackService extends Disposable implements IAgentFeedbackService { declare readonly _serviceBrand: undefined; @@ -103,41 +100,8 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe constructor( @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { super(); - - this._registerChatWidgetListeners(); - } - - private _registerChatWidgetListeners(): void { - for (const widget of this._chatWidgetService.getAllWidgets()) { - this._registerWidgetListeners(widget); - } - - this._store.add(this._chatWidgetService.onDidAddWidget(widget => { - this._registerWidgetListeners(widget); - })); - } - - private _registerWidgetListeners(widget: IChatWidget): void { - this._store.add(widget.attachmentModel.onDidChange(e => { - for (const deletedId of e.deleted) { - if (!deletedId.startsWith(AGENT_FEEDBACK_ATTACHMENT_ID_PREFIX)) { - continue; - } - - const sessionResourceString = deletedId.slice(AGENT_FEEDBACK_ATTACHMENT_ID_PREFIX.length); - if (!sessionResourceString) { - continue; - } - - const sessionResource = URI.parse(sessionResourceString); - if (this.getFeedback(sessionResource).length > 0) { - this.clearFeedback(sessionResource); - } - } - })); } addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string): IAgentFeedback { diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css index 16aac443e48..33bf495578f 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css @@ -3,24 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-glyph, -.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint { +.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 .glyph-margin-widgets .cgmr.agent-feedback-glyph { - background-color: var(--vscode-editorGutter-commentGlyphForeground, var(--vscode-icon-foreground)); - color: var(--vscode-editor-background); +.monaco-editor .agent-feedback-glyph { + background-color: var(--vscode-toolbar-hoverBackground); } -.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint { +.monaco-editor .agent-feedback-add-hint { background-color: var(--vscode-toolbar-hoverBackground); opacity: 0.7; } -.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint:hover { +.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 new file mode 100644 index 00000000000..890791c5973 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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-line-decoration, +.monaco-editor .agent-feedback-add-hint { + border-radius: 3px; + display: flex !important; + align-items: center; + justify-content: center; +} + +.monaco-editor .agent-feedback-line-decoration { + 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/sessions/browser/media/customizationsToolbar.css b/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css index d671775dbd5..8a4298cf909 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css +++ b/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css @@ -124,10 +124,11 @@ .ai-customization-toolbar .ai-customization-toolbar-content { max-height: 500px; overflow: hidden; - transition: max-height 0.2s ease-out; + transition: max-height 0.2s ease-out, display 0s linear 0.2s; } .ai-customization-toolbar.collapsed .ai-customization-toolbar-content { max-height: 0; + display: none; } } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts new file mode 100644 index 00000000000..9cf993aa7bb --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; + +/** + * Interface for a contributed attachment widget instance. + */ +export interface IChatAttachmentWidgetInstance extends IDisposable { + readonly element: HTMLElement; + readonly onDidDelete: event.Event; + readonly onDidOpen: event.Event; +} + +/** + * 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, +) => IChatAttachmentWidgetInstance; + +export const IChatAttachmentWidgetRegistry = createDecorator('chatAttachmentWidgetRegistry'); + +export interface IChatAttachmentWidgetRegistry { + readonly _serviceBrand: undefined; + + /** + * Register a widget factory for a specific attachment kind. + */ + registerFactory(kind: string, factory: ChatAttachmentWidgetFactory): IDisposable; + + /** + * Try to create a widget for the given attachment using a registered factory. + * Returns undefined if no factory is registered for the attachment's kind. + */ + createWidget( + instantiationService: IInstantiationService, + attachment: IChatRequestVariableEntry, + options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, + container: HTMLElement, + ): IChatAttachmentWidgetInstance | undefined; +} + +export class ChatAttachmentWidgetRegistry implements IChatAttachmentWidgetRegistry { + + declare readonly _serviceBrand: undefined; + + private readonly _factories = new Map(); + + registerFactory(kind: string, factory: ChatAttachmentWidgetFactory): IDisposable { + this._factories.set(kind, factory); + return { + dispose: () => { + if (this._factories.get(kind) === factory) { + this._factories.delete(kind); + } + } + }; + } + + createWidget( + instantiationService: IInstantiationService, + attachment: IChatRequestVariableEntry, + options: { shouldFocusClearButton: boolean; supportsDeletion: boolean }, + container: HTMLElement, + ): IChatAttachmentWidgetInstance | undefined { + const factory = this._factories.get(attachment.kind); + if (!factory) { + return undefined; + } + return factory(instantiationService, attachment, options, container); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e882231c7e1..356a7b23b48 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -96,6 +96,7 @@ import { ChatAccessibilityService } from './accessibility/chatAccessibilityServi import './attachments/chatAttachmentModel.js'; import './widget/input/chatStatusWidget.js'; import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './attachments/chatAttachmentResolveService.js'; +import { ChatAttachmentWidgetRegistry, IChatAttachmentWidgetRegistry } from './attachments/chatAttachmentWidgetRegistry.js'; import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './widget/chatContentParts/chatMarkdownAnchorService.js'; import { ChatContextPickService, IChatContextPickService } from './attachments/chatContextPickService.js'; import { ChatInputBoxContentProvider } from './widget/input/editor/chatEditorInputContentProvider.js'; @@ -1542,6 +1543,7 @@ registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed); registerSingleton(IChatModeService, ChatModeService, InstantiationType.Delayed); registerSingleton(IChatAttachmentResolveService, ChatAttachmentResolveService, InstantiationType.Delayed); +registerSingleton(IChatAttachmentWidgetRegistry, ChatAttachmentWidgetRegistry, InstantiationType.Delayed); registerSingleton(IChatTodoListService, ChatTodoListService, InstantiationType.Delayed); registerSingleton(IChatOutputRendererService, ChatOutputRendererService, InstantiationType.Delayed); registerSingleton(IChatLayoutService, ChatLayoutService, InstantiationType.Delayed); 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 3c47743a275..68bf52c7377 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -100,6 +100,7 @@ import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionCon import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; +import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentWidgetRegistry.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; @@ -512,6 +513,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IChatAttachmentWidgetRegistry private readonly _chatAttachmentWidgetRegistry: IChatAttachmentWidgetRegistry, ) { super(); @@ -2439,7 +2441,8 @@ 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.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, lm, options, container, this._contextResourceLabels); + attachmentWidget = this._chatAttachmentWidgetRegistry.createWidget(this.instantiationService, attachment, options, container) + ?? this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, lm, options, container, this._contextResourceLabels); } if (shouldFocusClearButton) {