From fcb0499dea9b0be6c175c1a7123dbed61296aff9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:22:56 +0000 Subject: [PATCH 01/36] Initial plan From d0f4f50af20f422a9375385d356dd3d14e81bc87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:32:37 +0000 Subject: [PATCH 02/36] Fix: Show keybinding in tooltip for Keep/Undo chat edit buttons The tooltip for the Keep and Undo buttons in the chat editing overlay was not showing the keybinding (e.g. Cmd+Shift+Y for Keep). The getTooltip() override was short-circuiting when options.keybinding was set, but the base class never appended the keybinding to action.tooltip (only to action.label). Remove the short-circuit so the keybinding is always appended via IKeybindingService.appendKeybinding(). Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --- .../chat/browser/chatEditing/chatEditingEditorOverlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index cbaa0bc4753a7..9c87527e8edbb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -92,7 +92,7 @@ export class ChatEditingAcceptRejectActionViewItem extends ActionViewItem { protected override getTooltip(): string | undefined { const value = super.getTooltip(); - if (!value || this.options.keybinding) { + if (!value) { return value; } return this._keybindingService.appendKeybinding(value, this._action.id); From 2c0602cbff0784c82075dbf62257144e9e199a45 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 17 Feb 2026 17:05:53 +0100 Subject: [PATCH 03/36] inline chat gutter affordance - menu, lightbulb icon, and color support --- .../components/gutterIndicatorMenu.ts | 34 +++++++---- .../components/gutterIndicatorView.ts | 7 ++- .../browser/view/inlineEdits/view.css | 5 +- .../browser/inlineChatAffordance.ts | 5 +- .../browser/inlineChatGutterAffordance.ts | 57 +++++++++++++------ .../media/inlineChatEditorAffordance.css | 11 +++- 6 files changed, 85 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 52015f27148fa..4f18305361308 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -65,6 +65,29 @@ export class GutterIndicatorMenuContent { }; }; + const extensionCommandGroups = this._data.extensionCommands.map(group => + group.map((c, idx) => option(createOptionArgs({ + id: c.command.id + '_' + idx, + title: c.command.title, + icon: c.icon ?? Codicon.symbolEvent, + commandId: c.command.id, + commandArgs: c.command.arguments + }))) + ); + + const extensionCommandNodes: ChildNode = []; + for (const group of extensionCommandGroups) { + if (group.length > 0) { + extensionCommandNodes.push(separator()); + extensionCommandNodes.push(...group); + } + } + + if (this._data.extensionCommandsOnly) { + // drop leading separator + return hoverContent(extensionCommandNodes.slice(1)); + } + const title = header(this._data.displayName); const gotoAndAccept = option(createOptionArgs({ @@ -88,14 +111,6 @@ export class GutterIndicatorMenuContent { commandId: inlineSuggestCommitAlternativeActionId, })) : undefined; - const extensionCommands = this._data.extensionCommands.map((c, idx) => option(createOptionArgs({ - id: c.command.id + '_' + idx, - title: c.command.title, - icon: c.icon ?? Codicon.symbolEvent, - commandId: c.command.id, - commandArgs: c.command.arguments - }))); - const showModelEnabled = false; const modelOptions = showModelEnabled ? this._data.modelInfo?.models.map((m: { id: string; name: string }) => option({ title: m.name, @@ -160,11 +175,10 @@ export class GutterIndicatorMenuContent { toggleCollapsedMode, modelOptions.length ? separator() : undefined, ...modelOptions, - extensionCommands.length ? separator() : undefined, snooze, settings, - ...extensionCommands, + ...extensionCommandNodes, actionBarFooter ? separator() : undefined, actionBarFooter diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 3929438266304..a2850bbc30f19 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -58,10 +58,11 @@ export class InlineEditsGutterIndicatorData { export class InlineSuggestionGutterMenuData { public static fromInlineSuggestion(suggestion: InlineSuggestionItem): InlineSuggestionGutterMenuData { const alternativeAction = suggestion.action?.kind === 'edit' ? suggestion.action.alternativeAction : undefined; + const commands = suggestion.source.inlineSuggestions.commands ?? []; return new InlineSuggestionGutterMenuData( suggestion.gutterMenuLinkAction, suggestion.source.provider.displayName ?? localize('inlineSuggestion', "Inline Suggestion"), - suggestion.source.inlineSuggestions.commands ?? [], + commands.length > 0 ? [commands] : [], alternativeAction, suggestion.source.provider.modelInfo, suggestion.source.provider.setModelId?.bind(suggestion.source.provider), @@ -71,10 +72,11 @@ export class InlineSuggestionGutterMenuData { constructor( readonly action: Command | undefined, readonly displayName: string, - readonly extensionCommands: InlineCompletionCommand[], + readonly extensionCommands: InlineCompletionCommand[][], readonly alternativeAction: InlineSuggestAlternativeAction | undefined, readonly modelInfo: IInlineCompletionModelInfo | undefined, readonly setModelId: ((modelId: string) => Promise) | undefined, + readonly extensionCommandsOnly: boolean = false, ) { } } @@ -584,6 +586,7 @@ export class InlineEditsGutterIndicator extends Disposable { width: layout.map(l => l.iconRect.width), position: 'relative', right: layout.map(l => l.iconDirection === 'top' ? '1px' : '0'), + color: this._data.map(d => d?.customization?.icon?.color ? asCssVariable(d.customization.icon.color.id) : undefined), } }, [ layout.map((l, reader) => withStyles(renderIcon(l.icon.read(reader)), { fontSize: toPx(Math.min(l.iconRect.width - CODICON_PADDING_PX, CODICON_SIZE_PX)) })), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index cbc244fc3edc2..5359dc95b9d5f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -217,6 +217,10 @@ .monaco-keybinding-key { font-size: 13px; opacity: 0.7; + padding: 0; + border: none; + margin: 0; + min-width: unset; } &.active { @@ -270,4 +274,3 @@ background-position: center; background-repeat: no-repeat; } - diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 58ecc761706fc..67dd59dcf9507 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -126,10 +126,11 @@ export class InlineChatAffordance extends Disposable { })); this._store.add(autorun(r => { - const isEditor = affordance.read(r) === 'editor'; + const mode = affordance.read(r); + const hideWithSelection = mode === 'editor' || mode === 'gutter'; const controller = CodeActionController.get(this.#editor); if (controller) { - controller.onlyLightBulbWithEmptySelection = isEditor; + controller.onlyLightBulbWithEmptySelection = hideWithSelection; } })); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 26c3a60a33908..aa6033e319f48 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -4,21 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { autorun, constObservable, derived, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { InlineCompletionCommand } from '../../../../editor/common/languages.js'; +import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; -import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; -import { ACTION_START } from '../common/inlineChat.js'; export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { @@ -26,19 +29,44 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { private readonly _myEditorObs: ObservableCodeEditor, selection: IObservable, private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, - @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IKeybindingService _keybindingService: IKeybindingService, @IHoverService hoverService: HoverService, @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService accessibilityService: IAccessibilityService, @IThemeService themeService: IThemeService, @IUserInteractionService userInteractionService: IUserInteractionService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, ) { + + const menu = menuService.createMenu(MenuId.InlineChatEditorAffordance, contextKeyService); + const menuObs = observableFromEvent(menu.onDidChange, () => menu.getActions({ renderShortTitle: true })); + + const codeActionController = CodeActionController.get(_myEditorObs.editor); + const lightBulbObs = codeActionController?.lightBulbState; + const data = derived(r => { const value = selection.read(r); if (!value) { return undefined; } + const commandGroups: InlineCompletionCommand[][] = []; + for (const [, groupActions] of menuObs.read(r)) { + const group: InlineCompletionCommand[] = []; + for (const action of groupActions) { + if (action instanceof MenuItemAction) { + group.push({ + command: { id: action.item.id, title: action.label }, + icon: ThemeIcon.isThemeIcon(action.item.icon) ? action.item.icon : undefined + }); + } + } + if (group.length > 0) { + commandGroups.push(group); + } + } + // Use the cursor position (active end of selection) to determine the line const cursorPosition = value.getPosition(); const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); @@ -47,20 +75,23 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const gutterMenuData = new InlineSuggestionGutterMenuData( undefined, // action '', // displayName - [], // extensionCommands + commandGroups, // extensionCommands undefined, // alternativeAction undefined, // modelInfo undefined, // setModelId + true, // extensionCommandsOnly ); + // Use lightbulb icon/color when code actions are available, otherwise sparkle + const lightBulbInfo = lightBulbObs?.read(r); + const icon = lightBulbInfo ? lightBulbInfo.icon : Codicon.sparkle; + return new InlineEditsGutterIndicatorData( gutterMenuData, lineRange, new SimpleInlineSuggestModel(() => { }, () => this._doShowHover()), undefined, // altAction - { - icon: Codicon.sparkle, - } + { icon } ); }); @@ -71,20 +102,14 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { hoverService, instantiationService, accessibilityService, themeService, userInteractionService ); + this._store.add(menu); + this._store.add(autorun(r => { const element = _hover.read(r); this._hoverVisible.set(!!element, undefined); })); } - protected override _showHover(): void { - this._hoverService.showInstantHover({ - target: this._iconRef.element, - content: this._keybindingService.appendKeybinding(localize('inlineChatGutterHover', "Inline Chat"), ACTION_START), - // appearance: { showPointer: true } - }); - } - private _doShowHover(): void { if (this._hoverVisible.get()) { return; diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index 14913c7eb451a..1cd7c1d50cbe4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -17,18 +17,23 @@ } .inline-chat-content-widget .action-label.codicon.codicon-light-bulb, -.inline-chat-content-widget .action-label.codicon.codicon-lightbulb-sparkle { +.inline-chat-content-widget .action-label.codicon.codicon-lightbulb-sparkle, +.inline-edits-view-gutter-indicator .codicon.codicon-light-bulb, +.inline-edits-view-gutter-indicator .codicon.codicon-lightbulb-sparkle { margin: 0; color: var(--vscode-editorLightBulb-foreground); } .inline-chat-content-widget .action-label.codicon.codicon-lightbulb-autofix, -.inline-chat-content-widget .action-label.codicon.codicon-lightbulb-sparkle-autofix { +.inline-chat-content-widget .action-label.codicon.codicon-lightbulb-sparkle-autofix, +.inline-edits-view-gutter-indicator .codicon.codicon-lightbulb-autofix, +.inline-edits-view-gutter-indicator .codicon.codicon-lightbulb-sparkle-autofix { margin: 0; color: var(--vscode-editorLightBulbAutoFix-foreground, var(--vscode-editorLightBulb-foreground)); } -.inline-chat-content-widget .action-label.codicon.codicon-sparkle-filled { +.inline-chat-content-widget .action-label.codicon.codicon-sparkle-filled, +.inline-edits-view-gutter-indicator .codicon.codicon-sparkle-filled { margin: 0; color: var(--vscode-editorLightBulbAi-foreground, var(--vscode-icon-foreground)); } From 253edfac44b820b1c71a6e314d2ab3d5520f2812 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 17 Feb 2026 17:14:56 +0100 Subject: [PATCH 04/36] move attach actions to InlineChatEditorAffordance menu with attach icon --- .../chat/browser/actions/chatContextActions.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index e5781a9fc8839..ccb4de93996ac 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -129,6 +129,7 @@ class AttachFileToChatAction extends AttachResourceAction { id: AttachFileToChatAction.ID, title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"), category: CHAT_CATEGORY, + icon: Codicon.attach, precondition: ChatContextKeys.enabled, f1: true, menu: [{ @@ -173,9 +174,9 @@ class AttachFileToChatAction extends AttachResourceAction { ) ) }, { - id: MenuId.ChatEditorInlineGutter, - group: '2_chat', - order: 2, + id: MenuId.InlineChatEditorAffordance, + group: '0_chat', + order: 3, when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate()) }] }); @@ -290,6 +291,7 @@ class AttachSelectionToChatAction extends Action2 { id: AttachSelectionToChatAction.ID, title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), category: CHAT_CATEGORY, + icon: Codicon.attach, f1: true, precondition: ChatContextKeys.enabled, menu: [{ @@ -307,9 +309,9 @@ class AttachSelectionToChatAction extends Action2 { ) ) }, { - id: MenuId.ChatEditorInlineGutter, - group: '2_chat', - order: 1, + id: MenuId.InlineChatEditorAffordance, + group: '0_chat', + order: 2, when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection) }] }); From 623812c0dc01b963138549de2f17186a7a2a6892 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 17 Feb 2026 17:40:49 +0100 Subject: [PATCH 05/36] Fix: Update options parameter type for ChatEditingAcceptRejectActionViewItem instantiation --- .../chat/browser/chatEditing/chatEditingEditorOverlay.ts | 4 ++-- .../contrib/inlineChat/browser/inlineChatOverlayWidget.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index 9c87527e8edbb..439ee6ac39412 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -10,7 +10,7 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platfor import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; -import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction, IActionRunner } from '../../../../../base/common/actions.js'; import { $, addDisposableGenericMouseMoveListener, append } from '../../../../../base/browser/dom.js'; import { assertType } from '../../../../../base/common/types.js'; @@ -35,7 +35,7 @@ export class ChatEditingAcceptRejectActionViewItem extends ActionViewItem { constructor( action: IAction, - options: IBaseActionViewItemOptions, + options: IActionViewItemOptions, private readonly _entry: IObservable, private readonly _editor: { focus(): void } | undefined, private readonly _keybindingService: IKeybindingService, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 0284206c2bf80..15d2f20c19cdf 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -452,7 +452,7 @@ export class InlineChatSessionOverlayWidget extends Disposable { return undefined; // use default action view item with label } - return new ChatEditingAcceptRejectActionViewItem(action, options, entry, undefined, that._keybindingService, primaryActions); + return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that._keybindingService, primaryActions); } })); From de93d651449bf47e0e955eada2a0b5652aabfeb8 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:17:10 -0800 Subject: [PATCH 06/36] Remove a few more chat session id usages For #274403 Confirmed that the api change should not effect copilot-chat --- src/vs/workbench/api/common/extHostLanguageModelTools.ts | 1 - src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts | 1 - .../chat/browser/widgetHosts/viewPane/chatViewPane.ts | 5 +++-- .../contrib/chat/common/chatService/chatServiceImpl.ts | 2 +- .../contrib/chat/common/chatService/chatServiceTelemetry.ts | 5 +++-- .../contrib/chat/common/tools/languageModelToolsService.ts | 2 -- src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts | 2 -- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index f3d03c9d08a01..cf52a6f3e3011 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -264,7 +264,6 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape const options: vscode.LanguageModelToolInvocationStreamOptions = { rawInput: context.rawInput, chatRequestId: context.chatRequestId, - chatSessionId: context.chatSessionId, chatSessionResource: context.chatSessionResource, chatInteractionId: context.chatInteractionId }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 6aedfb31bfd6f..5caa093f45d0f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2008,7 +2008,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection.clear(); - this.container.setAttribute('data-session-id', model.sessionId); this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined); // Pass input model reference to input part for state syncing diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index df7195252c14a..a824546fe7792 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -66,7 +66,9 @@ import { IChatEntitlementService } from '../../../../../services/chat/common/cha import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; interface IChatViewPaneState extends Partial { - /** @deprecated */ + /** + * @deprecated This is kept around to support old view states. However it should not be set on new states and `sessionResource` should be used instead. + */ sessionId?: string; sessionResource?: URI; @@ -695,7 +697,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { await this.updateWidgetLockState(model.sessionResource); // Update widget lock state based on session type // remember as model to restore in view state - this.viewState.sessionId = model.sessionId; this.viewState.sessionResource = model.sessionResource; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index e8df0b6017ef0..b28dbabe10c20 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -866,7 +866,7 @@ export class ChatService extends Disposable implements IChatService { agent: agentPart?.agent ?? defaultAgent, agentSlashCommandPart, commandPart, - sessionId: model.sessionId, + sessionResource: model.sessionResource, location: model.initialLocation, options, enableCommandDetection diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts index 6cd2769fbec09..2a0f2cf7cc8af 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts @@ -13,6 +13,7 @@ import { ChatAgentVoteDirection, ChatCopyKind, IChatSendRequestOptions, IChatUse import { isImageVariableEntry } from '../attachments/chatVariableEntries.js'; import { ChatAgentLocation } from '../constants.js'; import { ILanguageModelsService } from '../languageModels.js'; +import { chatSessionResourceToId } from '../model/chatUri.js'; type ChatVoteEvent = { direction: 'up' | 'down'; @@ -263,7 +264,7 @@ export class ChatRequestTelemetry { agent: IChatAgentData; agentSlashCommandPart: ChatRequestAgentSubcommandPart | undefined; commandPart: ChatRequestSlashCommandPart | undefined; - sessionId: string; + sessionResource: URI; location: ChatAgentLocation; options: IChatSendRequestOptions | undefined; enableCommandDetection: boolean; @@ -294,7 +295,7 @@ export class ChatRequestTelemetry { agent: detectedAgent?.id ?? this.opts.agent.id, agentExtensionId: detectedAgent?.extensionId.value ?? this.opts.agent.extensionId.value, slashCommand: this.opts.agentSlashCommandPart ? this.opts.agentSlashCommandPart.command.name : this.opts.commandPart?.slashCommand.command, - chatSessionId: this.opts.sessionId, + chatSessionId: chatSessionResourceToId(this.opts.sessionResource), enableCommandDetection: this.opts.enableCommandDetection, isParticipantDetected: !!detectedAgent, location: this.opts.location, diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 3316f148d3186..d14a93696e08e 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -349,8 +349,6 @@ export interface IToolInvocationStreamContext { toolCallId: string; rawInput: unknown; chatRequestId?: string; - /** @deprecated Use {@link chatSessionResource} instead */ - chatSessionId?: string; chatSessionResource?: URI; chatInteractionId?: string; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 603f8a6fcb8a0..b23503fa2328f 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -1020,8 +1020,6 @@ declare module 'vscode' { readonly rawInput?: unknown; readonly chatRequestId?: string; - /** @deprecated Use {@link chatSessionResource} instead */ - readonly chatSessionId?: string; readonly chatSessionResource?: Uri; readonly chatInteractionId?: string; } From 790026d0e1c167f9b09a977ddedcbb0d193e3658 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 11:37:17 +0100 Subject: [PATCH 07/36] sessions - retire `chat.agentsControl.clickBehavior: focus` (#295975) --- .../configuration/browser/configuration.contribution.ts | 1 - .../workbench/contrib/chat/browser/actions/chatActions.ts | 8 -------- .../workbench/contrib/chat/browser/chat.contribution.ts | 1 - 3 files changed, 10 deletions(-) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index db053fcef2266..9a5d00df68597 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -8,7 +8,6 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; Registry.as(Extensions.Configuration).registerDefaultConfigurations([{ overrides: { - 'chat.agentsControl.clickBehavior': 'focus', 'chat.agentsControl.enabled': true, 'chat.agent.maxRequests': 1000, 'chat.restoreLastPanelSession': true, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 7ac9212392892..88e50c6a49834 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -605,14 +605,6 @@ export function registerChatActions() { const chatVisible = viewsService.isViewVisible(ChatViewId); const clickBehavior = configurationService.getValue(ChatConfiguration.AgentsControlClickBehavior); switch (clickBehavior) { - case AgentsControlClickBehavior.Focus: - if (chatLocation === ViewContainerLocation.AuxiliaryBar) { - layoutService.setAuxiliaryBarMaximized(true); - } else { - this.updatePartVisibility(layoutService, chatLocation, true); - } - (await widgetService.revealWidget())?.focusInput(); - break; case AgentsControlClickBehavior.Cycle: if (chatVisible) { if ( diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 53be09ee9a2ae..b5427fb7021ce 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -208,7 +208,6 @@ configurationRegistry.registerConfiguration({ enumDescriptions: [ nls.localize('chat.agentsControl.clickBehavior.default', "Clicking chat icon toggles chat visibility."), nls.localize('chat.agentsControl.clickBehavior.cycle', "Clicking chat icon cycles through: show chat, maximize chat, hide chat. This requires chat to be contained in the secondary sidebar."), - nls.localize('chat.agentsControl.clickBehavior.focus', "Clicking chat icon focuses the chat view and maximizes it if located in the secondary sidebar.") ], markdownDescription: nls.localize('chat.agentsControl.clickBehavior', "Controls the behavior when clicking on the chat icon in the command center."), default: product.quality !== 'stable' ? AgentsControlClickBehavior.Cycle : AgentsControlClickBehavior.Default, From 045f45a19cf71e37e5fcc686a5135ba52c3872df Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 12:36:50 +0100 Subject: [PATCH 08/36] sessions - fix part layout assumptions (#295988) --- src/vs/sessions/browser/parts/auxiliaryBarPart.ts | 2 +- src/vs/sessions/browser/parts/sidebarPart.ts | 2 +- .../contrib/sessions/browser/sessionsViewPane.ts | 10 +++------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/vs/sessions/browser/parts/auxiliaryBarPart.ts b/src/vs/sessions/browser/parts/auxiliaryBarPart.ts index abb205600d572..d4e677a621515 100644 --- a/src/vs/sessions/browser/parts/auxiliaryBarPart.ts +++ b/src/vs/sessions/browser/parts/auxiliaryBarPart.ts @@ -105,7 +105,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { { hasTitle: true, trailingSeparator: false, - borderWidth: () => 0, + borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0, }, AuxiliaryBarPart.activeViewSettingsKey, ActiveAuxiliaryContext.bindTo(contextKeyService), diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index b689ed528f40f..d632124f3defb 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -99,7 +99,7 @@ export class SidebarPart extends AbstractPaneCompositePart { ) { super( Parts.SIDEBAR_PART, - { hasTitle: true, trailingSeparator: false, borderWidth: () => 0 }, + { hasTitle: true, trailingSeparator: false, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 }, SidebarPart.activeViewletSettingsKey, ActiveViewletContext.bindTo(contextKeyService), SidebarFocusContext.bindTo(contextKeyService), diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 8f6af610ad5c2..a243503c2f5c7 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -73,7 +73,6 @@ const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed'; export class AgenticSessionsViewPane extends ViewPane { private viewPaneContainer: HTMLElement | undefined; - private newSessionButtonContainer: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; sessionsControl: AgentSessionsControl | undefined; private aiCustomizationContainer: HTMLElement | undefined; @@ -156,7 +155,7 @@ export class AgenticSessionsViewPane extends ViewPane { const sessionsContent = DOM.append(sessionsSection, $('.agent-sessions-content')); // New Session Button - const newSessionButtonContainer = this.newSessionButtonContainer = DOM.append(sessionsContent, $('.agent-sessions-new-button-container')); + const newSessionButtonContainer = DOM.append(sessionsContent, $('.agent-sessions-new-button-container')); const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true })); newSessionButton.label = localize('newSession', "New Session"); this._register(newSessionButton.onDidClick(() => this.activeSessionService.openNewSession())); @@ -402,14 +401,11 @@ export class AgenticSessionsViewPane extends ViewPane { protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width); - if (!this.sessionsControl || !this.newSessionButtonContainer) { + if (!this.sessionsControl || !this.sessionsControlContainer) { return; } - const buttonHeight = this.newSessionButtonContainer.offsetHeight; - const customizationHeight = this.aiCustomizationContainer?.offsetHeight || 0; - const availableSessionsHeight = height - buttonHeight - customizationHeight; - this.sessionsControl.layout(availableSessionsHeight, width); + this.sessionsControl.layout(this.sessionsControlContainer.offsetHeight, width); } override focus(): void { From 33b466b265d66d9292af24d75946f976c9da06ca Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 18 Feb 2026 12:39:25 +0100 Subject: [PATCH 09/36] feat: add inline chat input functionality and styling improvements --- src/vs/platform/actions/common/actions.ts | 1 + .../browser/inlineChat.contribution.ts | 1 + .../inlineChat/browser/inlineChatActions.ts | 27 ++- .../browser/inlineChatController.ts | 7 +- .../browser/inlineChatOverlayWidget.ts | 209 +++++++++--------- .../inlineChat/browser/media/inlineChat.css | 8 +- .../media/inlineChatEditorAffordance.css | 7 +- .../browser/media/inlineChatOverlayWidget.css | 35 ++- .../contrib/inlineChat/common/inlineChat.ts | 1 + 9 files changed, 183 insertions(+), 113 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c3d2db6d98236..9b720b3d0cb55 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -285,6 +285,7 @@ export class MenuId { static readonly ChatEditorInlineExecute = new MenuId('ChatEditorInputExecute'); static readonly ChatEditorInlineInputSide = new MenuId('ChatEditorInputSide'); static readonly InlineChatEditorAffordance = new MenuId('InlineChatEditorAffordance'); + static readonly InlineChatInput = new MenuId('InlineChatInput'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorContent = new MenuId('MultiDiffEditorContent'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index c3bb43bc90b68..8db06f955979d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -103,6 +103,7 @@ MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.FocusInlineChat); +registerAction2(InlineChatActions.SubmitInlineChatInputAction); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index f7dc57db29273..69d0589726692 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -330,3 +330,28 @@ export class UndoAndCloseSessionAction2 extends KeepOrUndoSessionAction { }); } } + +export class SubmitInlineChatInputAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat.submitInput', + title: localize2('submitInput', "Send"), + icon: Codicon.send, + precondition: CTX_INLINE_CHAT_INPUT_HAS_TEXT, + menu: [{ + id: MenuId.InlineChatInput, + group: '0_main', + order: 1, + }] + }); + } + + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: unknown[]): void { + const value = ctrl.inputWidget.value; + if (value) { + ctrl.inputWidget.hide(); + ctrl.run({ message: value, autoSend: true }); + } + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 1790933cbb888..79335bf93cd9a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -118,6 +118,7 @@ export class InlineChatController implements IEditorContribution { private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; private readonly _gutterIndicator: InlineChatAffordance; + private readonly _inputWidget: InlineChatInputWidget; private readonly _currentSession: IObservable; @@ -129,6 +130,10 @@ export class InlineChatController implements IEditorContribution { return Boolean(this._currentSession.get()); } + get inputWidget(): InlineChatInputWidget { + return this._inputWidget; + } + constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @@ -152,7 +157,7 @@ export class InlineChatController implements IEditorContribution { const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); - const overlayWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); + const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 0284206c2bf80..1789bec7a6e4a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -5,10 +5,8 @@ import './media/inlineChatOverlayWidget.css'; import * as dom from '../../../../base/browser/dom.js'; +import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; -import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { IAction, Separator } from '../../../../base/common/actions.js'; -import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -23,20 +21,18 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/b import { IModelService } from '../../../../editor/common/services/model.js'; import { localize } from '../../../../nls.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ChatEditingAcceptRejectActionViewItem } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; -import { ACTION_START } from '../common/inlineChat.js'; +import { ACTION_START, CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; import { StickyScrollController } from '../../../../editor/contrib/stickyScroll/browser/stickyScrollController.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; -import { InlineChatRunOptions } from './inlineChatController.js'; import { IInlineChatSession2 } from './inlineChatSessionService.js'; -import { Position } from '../../../../editor/common/core/position.js'; import { CancelChatActionId } from '../../chat/browser/actions/chatExecuteActions.js'; import { assertType } from '../../../../base/common/types.js'; @@ -46,8 +42,9 @@ import { assertType } from '../../../../base/common/types.js'; export class InlineChatInputWidget extends Disposable { private readonly _domNode: HTMLElement; + private readonly _container: HTMLElement; private readonly _inputContainer: HTMLElement; - private readonly _actionBar: ActionBar; + private readonly _toolbarContainer: HTMLElement; private readonly _input: IActiveCodeEditor; private readonly _position = observableValue(this, null); readonly position: IObservable = this._position; @@ -55,7 +52,7 @@ export class InlineChatInputWidget extends Disposable { private readonly _showStore = this._store.add(new DisposableStore()); private readonly _stickyScrollHeight: IObservable; - private _inlineStartAction: IAction | undefined; + private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number }>; private _anchorLineNumber: number = 0; private _anchorLeft: number = 0; private _anchorAbove: boolean = false; @@ -64,8 +61,8 @@ export class InlineChatInputWidget extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IMenuService private readonly _menuService: IMenuService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICommandService private readonly _commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, @@ -75,17 +72,19 @@ export class InlineChatInputWidget extends Disposable { // Create container this._domNode = dom.$('.inline-chat-gutter-menu'); + // Create inner container (background + focus border) + this._container = dom.append(this._domNode, dom.$('.inline-chat-gutter-container')); + // Create input editor container - this._inputContainer = dom.append(this._domNode, dom.$('.input')); - this._inputContainer.style.width = '200px'; - this._inputContainer.style.height = '26px'; - this._inputContainer.style.display = 'flex'; - this._inputContainer.style.alignItems = 'center'; - this._inputContainer.style.justifyContent = 'center'; + this._inputContainer = dom.append(this._container, dom.$('.input')); + + // Create toolbar container + this._toolbarContainer = dom.append(this._container, dom.$('.toolbar')); // Create editor options const options = getSimpleEditorOptions(configurationService); - options.wordWrap = 'on'; + options.wordWrap = 'off'; + options.wrappingStrategy = 'advanced'; options.lineNumbers = 'off'; options.glyphMargin = false; options.lineDecorationsWidth = 0; @@ -94,6 +93,10 @@ export class InlineChatInputWidget extends Disposable { options.minimap = { enabled: false }; options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; options.renderLineHighlight = 'none'; + options.fontFamily = DEFAULT_FONT_FAMILY; + options.fontSize = 12; + options.lineHeight = 18; + options.cursorWidth = 1; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, @@ -107,10 +110,79 @@ export class InlineChatInputWidget extends Disposable { const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); this._input.setModel(model); + // Create toolbar + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MenuId.InlineChatInput, { + telemetrySource: 'inlineChatInput.toolbar', + hiddenItemStrategy: HiddenItemStrategy.NoHide, + toolbarOptions: { + primaryGroup: () => true, + }, + menuOptions: { shouldForwardArgs: true }, + })); + // Initialize sticky scroll height observable const stickyScrollController = StickyScrollController.get(this._editorObs.editor); this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + // Track toolbar width changes + const toolbarWidth = observableValue(this, 0); + const resizeObserver = new dom.DisposableResizeObserver(() => { + toolbarWidth.set(dom.getTotalWidth(toolbar.getElement()), undefined); + }); + this._store.add(resizeObserver); + this._store.add(resizeObserver.observe(toolbar.getElement())); + + // Compute min and max widget width based on editor content width + const maxWidgetWidth = derived(r => { + const layoutInfo = this._editorObs.layoutInfo.read(r); + return Math.max(0, Math.round(layoutInfo.contentWidth * 0.70)); + }); + const minWidgetWidth = derived(r => { + const layoutInfo = this._editorObs.layoutInfo.read(r); + return Math.max(0, Math.round(layoutInfo.contentWidth * 0.33)); + }); + + const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); + const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); + + this._layoutData = derived(r => { + + const totalWidth = contentWidth.read(r) + toolbarWidth.read(r); + const minWidth = minWidgetWidth.read(r); + const maxWidth = maxWidgetWidth.read(r); + const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' + ? maxWidth + : Math.max(minWidth, Math.min(totalWidth, maxWidth)); + + const lineHeight = this._input.getOption(EditorOption.lineHeight); + const clampedHeight = Math.min(contentHeight.read(r), (3 * lineHeight)); + + if (totalWidth > clampedWidth) { + // enable word wrap + this._input.updateOptions({ wordWrap: 'on', }); + } + + return { + toolbarWidth: toolbarWidth.read(r), + totalWidth: clampedWidth, + height: clampedHeight + }; + }); + + // Update container width and editor layout when width changes + this._store.add(autorun(r => { + const { toolbarWidth, totalWidth, height } = this._layoutData.read(r); + + const inputWidth = totalWidth - toolbarWidth; + this._container.style.width = `${totalWidth}px`; + this._inputContainer.style.width = `${inputWidth}px`; + this._input.layout({ width: inputWidth, height }); + })); + + // Toggle focus class on the container + this._store.add(this._input.onDidFocusEditorText(() => this._container.classList.add('focused'))); + this._store.add(this._input.onDidBlurEditorText(() => this._container.classList.remove('focused'))); + // Update placeholder based on selection state this._store.add(autorun(r => { const selection = this._editorObs.cursorSelection.read(r); @@ -122,71 +194,38 @@ export class InlineChatInputWidget extends Disposable { this._input.updateOptions({ placeholder: this._keybindingService.appendKeybinding(placeholderText, ACTION_START) }); })); - // Listen to content size changes and resize the input editor (max 3 lines) - this._store.add(this._input.onDidContentSizeChange(e => { - if (e.contentHeightChanged) { - this._updateInputHeight(e.contentHeight); - } + + // Track input text for context key and adjust width based on content + const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService); + this._store.add(this._input.onDidChangeModelContent(() => { + inputHasText.set(this._input.getModel().getValue().trim().length > 0); })); + this._store.add(toDisposable(() => inputHasText.reset())); - // Handle Enter key to submit and ArrowDown to focus action bar + // Handle Enter key to submit this._store.add(this._input.onKeyDown(e => { if (e.keyCode === KeyCode.Enter && !e.shiftKey) { - const value = this._input.getModel().getValue() ?? ''; - // TODO@jrieken this isn't nice - if (this._inlineStartAction && value) { - e.preventDefault(); - e.stopPropagation(); - this._actionBar.actionRunner.run( - this._inlineStartAction, - { message: value, autoSend: true } satisfies InlineChatRunOptions - ); - } + e.preventDefault(); + e.stopPropagation(); + this._commandService.executeCommand('inlineChat.submitInput'); } else if (e.keyCode === KeyCode.Escape) { // Hide overlay if input is empty const value = this._input.getModel().getValue() ?? ''; if (!value) { e.preventDefault(); e.stopPropagation(); - this._hide(); - } - } else if (e.keyCode === KeyCode.DownArrow) { - // Focus first action bar item when at the end of the input - const inputModel = this._input.getModel(); - const position = this._input.getPosition(); - const lastLineNumber = inputModel.getLineCount(); - const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); - if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { - e.preventDefault(); - e.stopPropagation(); - this._actionBar.focus(); + this.hide(); } } })); - // Create vertical action bar - this._actionBar = this._store.add(new ActionBar(this._domNode, { - orientation: ActionsOrientation.VERTICAL, - preventLoopNavigation: true, - })); - - // Handle ArrowUp on first action bar item to focus input editor - this._store.add(dom.addDisposableListener(this._actionBar.domNode, 'keydown', e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.UpArrow) && this._actionBar.isFocused(this._actionBar.viewItems.findIndex(item => item.action.id !== Separator.ID))) { - event.preventDefault(); - event.stopPropagation(); - this._input.focus(); - } - }, true)); - // Track focus - hide when focus leaves const focusTracker = this._store.add(dom.trackFocus(this._domNode)); - this._store.add(focusTracker.onDidBlur(() => this._hide())); + this._store.add(focusTracker.onDidBlur(() => this.hide())); + } - // Handle action bar cancel (Escape key) - this._store.add(this._actionBar.onDidCancel(() => this._hide())); - this._store.add(this._actionBar.onWillRun(() => this._hide())); + get value(): string { + return this._input.getModel().getValue(); } /** @@ -199,11 +238,8 @@ export class InlineChatInputWidget extends Disposable { this._showStore.clear(); // Clear input state + this._input.updateOptions({ wordWrap: 'off' }); this._input.getModel().setValue(''); - this._updateInputHeight(this._input.getContentHeight()); - - // Refresh actions from menu - this._refreshActions(); // Store anchor info for scroll updates this._anchorLineNumber = lineNumber; @@ -234,7 +270,7 @@ export class InlineChatInputWidget extends Disposable { ); const hasContent = !!this._input.getModel().getValue(); if (!isLineVisible && !hasContent) { - this._hide(); + this.hide(); } else { this._updatePosition(); } @@ -277,7 +313,7 @@ export class InlineChatInputWidget extends Disposable { /** * Hide the widget (removes from editor but does not dispose). */ - private _hide(): void { + hide(): void { // Focus editor if focus is still within the editor's DOM const editorDomNode = this._editorObs.editor.getDomNode(); if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) { @@ -286,35 +322,6 @@ export class InlineChatInputWidget extends Disposable { this._position.set(null, undefined); this._showStore.clear(); } - - private _refreshActions(): void { - // Clear existing actions - this._actionBar.clear(); - this._inlineStartAction = undefined; - - // Get fresh actions from menu - const actions = getFlatActionBarActions(this._menuService.getMenuActions(MenuId.ChatEditorInlineGutter, this._contextKeyService, { shouldForwardArgs: true })); - - // Set actions with keybindings (skip ACTION_START since we have the input editor) - for (const action of actions) { - if (action.id === ACTION_START) { - this._inlineStartAction = action; - continue; - } - const keybinding = this._keybindingService.lookupKeybinding(action.id)?.getLabel(); - this._actionBar.push(action, { icon: false, label: true, keybinding }); - } - } - - private _updateInputHeight(contentHeight: number): void { - const lineHeight = this._input.getOption(EditorOption.lineHeight); - const maxHeight = 3 * lineHeight; - const clampedHeight = Math.min(contentHeight, maxHeight); - const containerPadding = 8; - - this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; - this._input.layout({ width: 200, height: clampedHeight }); - } } /** diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 2215f0f84ddd0..73344bbf55fb9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -324,17 +324,15 @@ /* Gutter menu overlay widget */ .inline-chat-gutter-menu { - background: var(--vscode-menu-background); + background: var(--vscode-panel-background); border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); - border-radius: 4px; + border-radius: 5px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); - padding: 4px 0; - min-width: 160px; z-index: 10000; } .inline-chat-gutter-menu .input { - padding: 0 8px; + padding: 0 6px; } .inline-chat-gutter-menu .monaco-action-bar.vertical .action-item { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index 1cd7c1d50cbe4..b13f086fd951d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ .inline-chat-content-widget { - background-color: var(--vscode-editor-background); - padding: 2px; - border-radius: 8px; + background-color: var(--vscode-panel-background); + padding: 0 1px; + border-radius: 5px; display: flex; align-items: center; box-shadow: 0 4px 8px var(--vscode-widget-shadow); @@ -14,6 +14,7 @@ min-width: var(--vscode-inline-chat-affordance-height); min-height: var(--vscode-inline-chat-affordance-height); line-height: var(--vscode-inline-chat-affordance-height); + border: 1px solid var(--vscode-input-border, transparent); } .inline-chat-content-widget .action-label.codicon.codicon-light-bulb, diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 1af3ff339a139..f88c3626fad9c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -7,8 +7,39 @@ transition: top 100ms; } -.inline-chat-gutter-menu .input .monaco-editor-background { - background-color: var(--vscode-menu-background); +.inline-chat-gutter-container { + box-sizing: border-box; + display: flex; + align-items: center; + margin: 2px; + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 5px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + overflow: hidden; +} + +.inline-chat-gutter-container.focused { + border-color: var(--vscode-focusBorder); +} + +.inline-chat-gutter-container > .input { + flex: 1; + min-width: 0; +} + +.inline-chat-gutter-container > .input .monaco-editor-background { + background-color: var(--vscode-input-background); +} + +.inline-chat-gutter-container > .toolbar { + display: flex; + align-items: center; + padding: 0 4px; +} + +.inline-chat-gutter-container > .toolbar .monaco-action-bar .actions-container { + gap: 2px; } .inline-chat-session-overlay-widget { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index e8866c447f313..99591004f8210 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -108,6 +108,7 @@ export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFoc export const CTX_INLINE_CHAT_EDITING = new RawContextKey('inlineChatEditing', true, localize('inlineChatEditing', "Whether the user is currently editing or generating code in the inline chat")); export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); +export const CTX_INLINE_CHAT_INPUT_HAS_TEXT = new RawContextKey('inlineChatInputHasText', false, localize('inlineChatInputHasText', "Whether the inline chat input widget has text")); export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); From 16c3f4409c7361b0c8fa101a6a6266686f1b1542 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 18 Feb 2026 12:52:42 +0100 Subject: [PATCH 10/36] sessions window fixes --- src/vs/sessions/browser/menus.ts | 1 + src/vs/sessions/browser/parts/titlebarPart.ts | 35 ++++- src/vs/sessions/browser/workbench.ts | 41 +----- .../contrib/chat/browser/chat.contribution.ts | 8 +- .../electron-browser/parts/titlebarPart.ts | 135 ++++++++++++++++++ .../sessions/electron-browser/titleService.ts | 10 ++ src/vs/sessions/sessions.common.main.ts | 6 +- src/vs/sessions/sessions.desktop.main.ts | 2 + src/vs/workbench/browser/contextkeys.ts | 11 +- 9 files changed, 194 insertions(+), 55 deletions(-) create mode 100644 src/vs/sessions/electron-browser/parts/titlebarPart.ts create mode 100644 src/vs/sessions/electron-browser/titleService.ts diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index 8d0fdbe0d1d9f..a6f273f3aeb88 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -23,4 +23,5 @@ export const Menus = { AuxiliaryBarTitle: new MenuId('SessionsAuxiliaryBarTitle'), AuxiliaryBarTitleLeft: new MenuId('SessionsAuxiliaryBarTitleLeft'), SidebarFooter: new MenuId('SessionsSidebarFooter'), + SidebarCustomizations: new MenuId('SessionsSidebarCustomizations'), } as const; diff --git a/src/vs/sessions/browser/parts/titlebarPart.ts b/src/vs/sessions/browser/parts/titlebarPart.ts index b2231dd47b821..f8839f0db5b71 100644 --- a/src/vs/sessions/browser/parts/titlebarPart.ts +++ b/src/vs/sessions/browser/parts/titlebarPart.ts @@ -203,7 +203,7 @@ export class TitlebarPart extends Part implements ITitlebarPart { })); // Right toolbar (driven by Menus.TitleBarRight - includes account submenu) - const rightToolbarContainer = append(this.rightContent, $('div.action-toolbar-container')); + const rightToolbarContainer = prepend(this.rightContent, $('div.action-toolbar-container')); this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRight, { contextMenu: Menus.TitleBarContext, telemetrySource: 'titlePart.right', @@ -258,8 +258,15 @@ export class TitlebarPart extends Part implements ITitlebarPart { private lastLayoutDimension: Dimension | undefined; + get hasZoomableElements(): boolean { + return true; // sessions titlebar always has command center and toolbar actions + } + get preventZoom(): boolean { - return getZoomFactor(getWindow(this.element)) < 1; + // Prevent zooming behavior if any of the following conditions are met: + // 1. Shrinking below the window control size (zoom < 1) + // 2. No custom items are present in the title bar + return getZoomFactor(getWindow(this.element)) < 1 || !this.hasZoomableElements; } override layout(width: number, height: number): void { @@ -342,6 +349,7 @@ export class AuxiliaryTitlebarPart extends TitlebarPart implements IAuxiliaryTit constructor( readonly container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, + private readonly mainTitlebar: TitlebarPart, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @@ -354,6 +362,15 @@ export class AuxiliaryTitlebarPart extends TitlebarPart implements IAuxiliaryTit const id = AuxiliaryTitlebarPart.COUNTER++; super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService); } + + override get preventZoom(): boolean { + // Prevent zooming behavior if any of the following conditions are met: + // 1. Shrinking below the window control size (zoom < 1) + // 2. No custom items are present in the main title bar + // The auxiliary title bar never contains any zoomable items itself, + // but we want to match the behavior of the main title bar. + return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements; + } } /** @@ -366,17 +383,21 @@ export class TitleService extends MultiWindowParts implements ITit readonly mainPart: TitlebarPart; constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService ) { super('workbench.agentSessionsTitleService', themeService, storageService); - this.mainPart = this._register(this.instantiationService.createInstance(MainTitlebarPart)); + this.mainPart = this._register(this.createMainTitlebarPart()); this.onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange; this._register(this.registerPart(this.mainPart)); } + protected createMainTitlebarPart(): TitlebarPart { + return this.instantiationService.createInstance(MainTitlebarPart); + } + //#region Auxiliary Titlebar Parts createAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): IAuxiliaryTitlebarPart { @@ -386,7 +407,7 @@ export class TitleService extends MultiWindowParts implements ITit const disposables = new DisposableStore(); - const titlebarPart = instantiationService.createInstance(AuxiliaryTitlebarPart, titlebarPartContainer, editorGroupsContainer); + const titlebarPart = this.doCreateAuxiliaryTitlebarPart(titlebarPartContainer, editorGroupsContainer, instantiationService); disposables.add(this.registerPart(titlebarPart)); disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`)); @@ -397,6 +418,10 @@ export class TitleService extends MultiWindowParts implements ITit return titlebarPart; } + protected doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): TitlebarPart & IAuxiliaryTitlebarPart { + return instantiationService.createInstance(AuxiliaryTitlebarPart, container, editorGroupsContainer, this.mainPart); + } + //#endregion //#region Service Implementation diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 25c88e45dda44..d9b4e673d1ac7 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -1059,12 +1059,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { } this.partVisibility.sidebar = !hidden; - - // Adjust CSS - for hiding, defer adding the class until animation - // completes so the part stays visible during the exit animation. - if (!hidden) { - this.mainContainer.classList.remove(LayoutClasses.SIDEBAR_HIDDEN); - } + this.mainContainer.classList.toggle(LayoutClasses.SIDEBAR_HIDDEN, hidden); // Propagate to grid this.workbenchGrid.setViewVisible( @@ -1087,12 +1082,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { } this.partVisibility.auxiliaryBar = !hidden; - - // Adjust CSS - for hiding, defer adding the class until animation - // completes so the part stays visible during the exit animation. - if (!hidden) { - this.mainContainer.classList.remove(LayoutClasses.AUXILIARYBAR_HIDDEN); - } + this.mainContainer.classList.toggle(LayoutClasses.AUXILIARYBAR_HIDDEN, hidden); // Propagate to grid this.workbenchGrid.setViewVisible( @@ -1115,15 +1105,8 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { } this.partVisibility.editor = !hidden; - - // Adjust CSS for main container - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN); - this.mainContainer.classList.remove(LayoutClasses.EDITOR_MODAL_VISIBLE); - } else { - this.mainContainer.classList.remove(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN); - this.mainContainer.classList.add(LayoutClasses.EDITOR_MODAL_VISIBLE); - } + this.mainContainer.classList.toggle(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN, hidden); + this.mainContainer.classList.toggle(LayoutClasses.EDITOR_MODAL_VISIBLE, !hidden); // Show/hide modal if (hidden) { @@ -1144,13 +1127,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { } this.partVisibility.panel = !hidden; - - // Adjust CSS - for hiding, defer adding the class until animation - // completes because `.nopanel .part.panel { display: none !important }` - // would instantly hide the panel content mid-animation. - if (!hidden) { - this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); - } + this.mainContainer.classList.toggle(LayoutClasses.PANEL_HIDDEN, hidden); // Propagate to grid this.workbenchGrid.setViewVisible( @@ -1173,13 +1150,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { } this.partVisibility.chatBar = !hidden; - - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.CHATBAR_HIDDEN); - } else { - this.mainContainer.classList.remove(LayoutClasses.CHATBAR_HIDDEN); - } + this.mainContainer.classList.toggle(LayoutClasses.CHATBAR_HIDDEN, hidden); // Propagate to grid this.workbenchGrid.setViewVisible(this.chatBarPartView, !hidden); diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 5c783fc4c063d..b17a408300976 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -16,7 +16,9 @@ import { SyncDescriptor } from '../../../../platform/instantiation/common/descri import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js'; -import { ITerminalService, ITerminalGroupService } from '../../../../workbench/contrib/terminal/browser/terminal.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'; @@ -111,7 +113,7 @@ export class OpenSessionInTerminalAction extends Action2 { override async run(accessor: ServicesAccessor,): Promise { const terminalService = accessor.get(ITerminalService); - const terminalGroupService = accessor.get(ITerminalGroupService); + const viewsService = accessor.get(IViewsService); const sessionsManagementService = accessor.get(ISessionsManagementService); const activeSession = sessionsManagementService.activeSession.get(); @@ -124,7 +126,7 @@ export class OpenSessionInTerminalAction extends Action2 { terminalService.setActiveInstance(instance); } } - terminalGroupService.showPanel(true); + await viewsService.openView(TERMINAL_VIEW_ID, true); } } diff --git a/src/vs/sessions/electron-browser/parts/titlebarPart.ts b/src/vs/sessions/electron-browser/parts/titlebarPart.ts new file mode 100644 index 0000000000000..8c2c0597b633d --- /dev/null +++ b/src/vs/sessions/electron-browser/parts/titlebarPart.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomFactor } from '../../../base/browser/browser.js'; +import { getWindow, getWindowId } from '../../../base/browser/dom.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { INativeHostService } from '../../../platform/native/common/native.js'; +import { IStorageService } from '../../../platform/storage/common/storage.js'; +import { IThemeService } from '../../../platform/theme/common/themeService.js'; +import { useWindowControlsOverlay } from '../../../platform/window/common/window.js'; +import { IHostService } from '../../../workbench/services/host/browser/host.js'; +import { IWorkbenchLayoutService, Parts } from '../../../workbench/services/layout/browser/layoutService.js'; +import { IAuxiliaryTitlebarPart } from '../../../workbench/browser/parts/titlebar/titlebarPart.js'; +import { IEditorGroupsContainer } from '../../../workbench/services/editor/common/editorGroupsService.js'; +import { CodeWindow, mainWindow } from '../../../base/browser/window.js'; +import { TitlebarPart, TitleService } from '../../browser/parts/titlebarPart.js'; + +export class NativeTitlebarPart extends TitlebarPart { + + private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined; + private cachedWindowControlHeight: number | undefined; + + constructor( + id: string, + targetWindow: CodeWindow, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IContextKeyService contextKeyService: IContextKeyService, + @IHostService hostService: IHostService, + @INativeHostService private readonly nativeHostService: INativeHostService, + ) { + super(id, targetWindow, contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService); + } + + override updateStyles(): void { + super.updateStyles(); + + if (this.element) { + if (useWindowControlsOverlay(this.configurationService)) { + if ( + !this.cachedWindowControlStyles || + this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor || + this.cachedWindowControlStyles.fgColor !== this.element.style.color + ) { + this.nativeHostService.updateWindowControls({ + targetWindowId: getWindowId(getWindow(this.element)), + backgroundColor: this.element.style.backgroundColor, + foregroundColor: this.element.style.color + }); + } + } + } + } + + override layout(width: number, height: number): void { + super.layout(width, height); + + if (useWindowControlsOverlay(this.configurationService)) { + const newHeight = Math.round(height * getZoomFactor(getWindow(this.element))); + if (newHeight !== this.cachedWindowControlHeight) { + this.cachedWindowControlHeight = newHeight; + this.nativeHostService.updateWindowControls({ + targetWindowId: getWindowId(getWindow(this.element)), + height: newHeight + }); + } + } + } +} + +class MainNativeTitlebarPart extends NativeTitlebarPart { + + constructor( + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IContextKeyService contextKeyService: IContextKeyService, + @IHostService hostService: IHostService, + @INativeHostService nativeHostService: INativeHostService, + ) { + super(Parts.TITLEBAR_PART, mainWindow, contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService); + } +} + +class AuxiliaryNativeTitlebarPart extends NativeTitlebarPart implements IAuxiliaryTitlebarPart { + + private static COUNTER = 1; + + get height() { return this.minimumHeight; } + + constructor( + readonly container: HTMLElement, + editorGroupsContainer: IEditorGroupsContainer, + private readonly mainTitlebar: TitlebarPart, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IContextKeyService contextKeyService: IContextKeyService, + @IHostService hostService: IHostService, + @INativeHostService nativeHostService: INativeHostService, + ) { + const id = AuxiliaryNativeTitlebarPart.COUNTER++; + super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService); + } + + override get preventZoom(): boolean { + return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements; + } +} + +export class NativeTitleService extends TitleService { + + protected override createMainTitlebarPart(): MainNativeTitlebarPart { + return this.instantiationService.createInstance(MainNativeTitlebarPart); + } + + protected override doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): AuxiliaryNativeTitlebarPart { + return instantiationService.createInstance(AuxiliaryNativeTitlebarPart, container, editorGroupsContainer, this.mainPart); + } +} diff --git a/src/vs/sessions/electron-browser/titleService.ts b/src/vs/sessions/electron-browser/titleService.ts new file mode 100644 index 0000000000000..9ffc7a93650af --- /dev/null +++ b/src/vs/sessions/electron-browser/titleService.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InstantiationType, registerSingleton } from '../../platform/instantiation/common/extensions.js'; +import { ITitleService } from '../../workbench/services/title/browser/titleService.js'; +import { NativeTitleService } from './parts/titlebarPart.js'; + +registerSingleton(ITitleService, NativeTitleService, InstantiationType.Eager); diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index 7e8c8e57a474a..4119d7eaf1342 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -351,9 +351,9 @@ import '../workbench/contrib/surveys/browser/nps.contribution.js'; import '../workbench/contrib/surveys/browser/languageSurveys.contribution.js'; // Welcome -import '../workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; -import '../workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.js'; -import '../workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution.js'; +// import '../workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; +// import '../workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.js'; +// import '../workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution.js'; import '../workbench/contrib/welcomeViews/common/viewsWelcome.contribution.js'; import '../workbench/contrib/welcomeViews/common/newFile.contribution.js'; diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index f690a68b72e12..b81b0de984e9c 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -9,6 +9,7 @@ import './sessions.common.main.js'; //#region --- workbench (agentic desktop main) import './electron-browser/sessions.main.js'; +import './electron-browser/titleService.js'; import '../workbench/electron-browser/desktop.contribution.js'; //#endregion @@ -190,6 +191,7 @@ import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contri import './contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.js'; import './contrib/chat/browser/chat.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; +import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; import './contrib/configuration/browser/configuration.contribution.js'; diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 16c7e5e226710..678a225f1df49 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -16,7 +16,6 @@ import { getRemoteName } from '../../platform/remote/common/remoteHosts.js'; import { getVirtualWorkspaceScheme } from '../../platform/workspace/common/virtualWorkspace.js'; import { IWorkingCopyService } from '../services/workingCopy/common/workingCopyService.js'; import { isNative } from '../../base/common/platform.js'; -import { IPaneCompositePartService } from '../services/panecomposite/browser/panecomposite.js'; import { WebFileSystemAccess } from '../../platform/files/browser/webFileSystemAccess.js'; import { IProductService } from '../../platform/product/common/productService.js'; import { getTitleBarStyle } from '../../platform/window/common/window.js'; @@ -75,7 +74,6 @@ export class WorkbenchContextKeysHandler extends Disposable { @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IEditorService private readonly editorService: IEditorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, ) { super(); @@ -180,6 +178,7 @@ export class WorkbenchContextKeysHandler extends Disposable { // Sidebar this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService); + this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART)); // Title Bar this.titleAreaVisibleContext = TitleBarVisibleContext.bindTo(this.contextKeyService); @@ -245,14 +244,12 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.layoutService.onDidChangePanelAlignment(alignment => this.panelAlignmentContext.set(alignment))); - this._register(this.paneCompositeService.onDidPaneCompositeClose(() => this.updateSideBarContextKeys())); - this._register(this.paneCompositeService.onDidPaneCompositeOpen(() => this.updateSideBarContextKeys())); - this._register(this.layoutService.onDidChangePartVisibility(() => { this.mainEditorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART, mainWindow)); this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); this.auxiliaryBarVisibleContext.set(this.layoutService.isVisible(Parts.AUXILIARYBAR_PART)); + this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART)); this.updateTitleBarContextKeys(); })); @@ -326,10 +323,6 @@ export class WorkbenchContextKeysHandler extends Disposable { } } - private updateSideBarContextKeys(): void { - this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART)); - } - private updateTitleBarContextKeys(): void { this.titleAreaVisibleContext.set(this.layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow)); this.titleBarStyleContext.set(getTitleBarStyle(this.configurationService)); From 39924a5ec0117769dac134df13452f0a44f09c44 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 18 Feb 2026 12:57:55 +0100 Subject: [PATCH 11/36] remove contrib --- src/vs/sessions/sessions.desktop.main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index b81b0de984e9c..b9a4d26199cf9 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -191,7 +191,6 @@ import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contri import './contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.js'; import './contrib/chat/browser/chat.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; -import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; import './contrib/configuration/browser/configuration.contribution.js'; From 1ea241d6ed1d55e558b8fc2a65d965c8e74edd28 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 13:03:54 +0100 Subject: [PATCH 12/36] sessions - retire `chat.agentsControl.clickBehavior: focus` (#295980) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- src/vs/workbench/contrib/chat/common/constants.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b5427fb7021ce..cd8b77b0a0c85 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -204,7 +204,7 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.AgentsControlClickBehavior]: { type: 'string', - enum: [AgentsControlClickBehavior.Default, AgentsControlClickBehavior.Cycle, AgentsControlClickBehavior.Focus], + enum: [AgentsControlClickBehavior.Default, AgentsControlClickBehavior.Cycle], enumDescriptions: [ nls.localize('chat.agentsControl.clickBehavior.default', "Clicking chat icon toggles chat visibility."), nls.localize('chat.agentsControl.clickBehavior.cycle', "Clicking chat icon cycles through: show chat, maximize chat, hide chat. This requires chat to be contained in the secondary sidebar."), diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 64a1f9b5ba830..8066635dc7477 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -92,7 +92,6 @@ export enum CollapsedToolsDisplayMode { export enum AgentsControlClickBehavior { Default = 'default', Cycle = 'cycle', - Focus = 'focus', } export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook' | 'editing-session'; From 2ef3c126b7417fa722e494fa1e62772986ee7a75 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 13:04:11 +0100 Subject: [PATCH 13/36] sessions - use activeSessionService for opening from picker (#295984) --- .../sessions/browser/sessionsTitleBarWidget.ts | 4 +++- .../agentSessions/agentSessionsActions.ts | 2 +- .../agentSessions/agentSessionsPicker.ts | 17 ++++++++++++++--- .../viewPane/chatViewTitleControl.ts | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index faba73697edfb..526fdb358d98e 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -344,7 +344,9 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { } private _showSessionsPicker(): void { - const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined); + const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined, { + overrideSessionOpen: (session, openOptions) => this.activeSessionService.openSession(session.resource, openOptions) + }); picker.pickAgentSession(); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 56f51542f992b..160fde288088d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -149,7 +149,7 @@ export class PickAgentSessionAction extends Action2 { async run(accessor: ServicesAccessor): Promise { const instantiationService = accessor.get(IInstantiationService); - const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined); + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined, undefined); await agentSessionsPicker.pickAgentSession(); } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts index 27fe44366cb72..056f9ecd07f7c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.ts @@ -11,7 +11,7 @@ import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; -import { openSession } from './agentSessionsOpener.js'; +import { ISessionOpenOptions, openSession } from './agentSessionsOpener.js'; import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js'; import { IAgentSessionsService } from './agentSessionsService.js'; import { AgentSessionsSorter, groupAgentSessionsByDate, sessionDateFromNow } from './agentSessionsViewer.js'; @@ -62,12 +62,17 @@ export function getSessionButtons(session: IAgentSession): IQuickInputButton[] { return buttons; } +export interface IAgentSessionsPickerOptions { + overrideSessionOpen?(session: IAgentSession, openOptions?: ISessionOpenOptions): Promise; +} + export class AgentSessionsPicker { private readonly sorter = new AgentSessionsSorter(); constructor( private readonly anchor: HTMLElement | undefined, + private readonly options: IAgentSessionsPickerOptions | undefined, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -87,13 +92,19 @@ export class AgentSessionsPicker { disposables.add(picker.onDidAccept(e => { const pick = picker.selectedItems[0]; if (pick) { - this.instantiationService.invokeFunction(openSession, pick.session, { + const openOptions: ISessionOpenOptions = { sideBySide: e.inBackground, editorOptions: { preserveFocus: e.inBackground, pinned: e.inBackground } - }); + }; + + if (this.options?.overrideSessionOpen) { + this.options.overrideSessionOpen(pick.session, openOptions); + } else { + this.instantiationService.invokeFunction(openSession, pick.session, openOptions); + } } if (!e.inBackground) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index 0cbc75d1883eb..b60a4f3b64f39 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -78,7 +78,7 @@ export class ChatViewTitleControl extends Disposable { async run(accessor: ServicesAccessor): Promise { const instantiationService = accessor.get(IInstantiationService); - const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element); + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element, undefined); await agentSessionsPicker.pickAgentSession(); } })); From 0c3635fc05fd151a909aac4ac5ab7372380d0b51 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 18 Feb 2026 13:07:07 +0100 Subject: [PATCH 14/36] feat: enhance inline chat affordance with command tracking and UI improvements --- .../components/gutterIndicatorMenu.ts | 7 ++++--- .../components/gutterIndicatorView.ts | 10 +++++++++- .../browser/inlineChat.contribution.ts | 3 ++- .../inlineChat/browser/inlineChatAffordance.ts | 16 ++++++++-------- .../browser/inlineChatEditorAffordance.ts | 9 ++++++--- .../browser/inlineChatGutterAffordance.ts | 8 +++++++- .../browser/inlineChatOverlayWidget.ts | 5 ++--- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 4f18305361308..afecdbf91d575 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -36,7 +36,7 @@ export class GutterIndicatorMenuContent { constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _data: InlineSuggestionGutterMenuData, - private readonly _close: (focusEditor: boolean) => void, + private readonly _close: (focusEditor: boolean, commandId?: string) => void, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ICommandService private readonly _commandService: ICommandService, @@ -59,8 +59,9 @@ export class GutterIndicatorMenuContent { isActive: activeElement.map(v => v === options.id), onHoverChange: v => activeElement.set(v ? options.id : undefined, undefined), onAction: () => { - this._close(true); - return this._commandService.executeCommand(typeof options.commandId === 'string' ? options.commandId : options.commandId.get(), ...(options.commandArgs ?? [])); + const commandId = typeof options.commandId === 'string' ? options.commandId : options.commandId.get(); + this._close(true, commandId); + return this._commandService.executeCommand(commandId, ...(options.commandArgs ?? [])); }, }; }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index a2850bbc30f19..27a5b2edaea29 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -36,6 +36,7 @@ import { InlineSuggestAlternativeAction } from '../../../model/InlineSuggestAlte import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ThemeIcon } from '../../../../../../../base/common/themables.js'; import { IUserInteractionService } from '../../../../../../../platform/userInteraction/browser/userInteractionService.js'; +import { Event, Emitter } from '../../../../../../../base/common/event.js'; /** * Customization options for the gutter indicator appearance and behavior. @@ -99,6 +100,10 @@ const CODICON_SIZE_PX = 16; const CODICON_PADDING_PX = 2; export class InlineEditsGutterIndicator extends Disposable { + + private readonly _onDidCloseWithCommand = this._register(new Emitter()); + readonly onDidCloseWithCommand: Event = this._onDidCloseWithCommand.event; + constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _data: IObservable, @@ -474,10 +479,13 @@ export class InlineEditsGutterIndicator extends Disposable { GutterIndicatorMenuContent, this._editorObs, data.gutterMenuData, - (focusEditor) => { + (focusEditor, commandId) => { if (focusEditor) { this._editorObs.editor.focus(); } + if (commandId) { + this._onDidCloseWithCommand.fire(commandId); + } h?.dispose(); }, ).toDisposableLiveElement()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 8db06f955979d..1fb41b8d8fd89 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -93,7 +93,8 @@ MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { order: 1, command: { id: ACTION_START, - title: localize('editCode', "Edit Code"), + title: localize('editCode', "Edit Code..."), + shortTitle: localize('editCodeShort', "Edit Code"), icon: Codicon.sparkle, }, when: EditorContextKeys.hasNonEmptySelection, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 67dd59dcf9507..c221808470c29 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -23,15 +23,18 @@ import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { generateUuid } from '../../../../base/common/uuid.js'; +import { Event } from '../../../../base/common/event.js'; type InlineChatAffordanceEvent = { mode: string; id: string; + commandId: string; }; type InlineChatAffordanceClassification = { mode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The affordance mode: gutter or editor.' }; id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'UUID to correlate shown and selected events.' }; + commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The command that was executed.' }; owner: 'jrieken'; comment: 'Tracks when the inline chat affordance is shown or selected.'; }; @@ -83,7 +86,7 @@ export class InlineChatAffordance extends Disposable { affordanceId = generateUuid(); const mode = affordance.read(undefined); if (mode === 'gutter' || mode === 'editor') { - telemetryService.publicLog2('inlineChatAffordance/shown', { mode, id: affordanceId }); + telemetryService.publicLog2('inlineChatAffordance/shown', { mode, id: affordanceId, commandId: '' }); } selectionData.set(value, undefined); })); @@ -106,7 +109,7 @@ export class InlineChatAffordance extends Disposable { } })); - this._store.add(this.#instantiationService.createInstance( + const gutterAffordance = this._store.add(this.#instantiationService.createInstance( InlineChatGutterAffordance, editorObs, derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), @@ -119,9 +122,10 @@ export class InlineChatAffordance extends Disposable { derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined) ); this._store.add(editorAffordance); - this._store.add(editorAffordance.onDidRunAction(() => { + + this._store.add(Event.any(editorAffordance.onDidRunAction, gutterAffordance.onDidRunAction)(commandId => { if (affordanceId) { - telemetryService.publicLog2('inlineChatAffordance/selected', { mode: 'editor', id: affordanceId }); + telemetryService.publicLog2('inlineChatAffordance/selected', { mode: affordance.get(), id: affordanceId, commandId }); } })); @@ -140,10 +144,6 @@ export class InlineChatAffordance extends Disposable { return; } - if (affordanceId) { - telemetryService.publicLog2('inlineChatAffordance/selected', { mode: 'gutter', id: affordanceId }); - } - // Reveal the line in case it's outside the viewport (e.g., when triggered from sticky scroll) this.#editor.revealLineInCenterIfOutsideViewport(data.lineNumber, ScrollType.Immediate); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 0aacdb125727d..519ae158e7f68 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -147,8 +147,8 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi private _position: IContentWidgetPosition | null = null; private _isVisible = false; - private readonly _onDidRunAction = this._store.add(new Emitter()); - readonly onDidRunAction: Event = this._onDidRunAction.event; + private readonly _onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this._onDidRunAction.event; readonly allowEditorOverflow = false; readonly suppressMouseDown = false; @@ -179,7 +179,10 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi return undefined; } })); - this._store.add(toolbar.actionRunner.onDidRun(() => this._onDidRunAction.fire())); + this._store.add(toolbar.actionRunner.onDidRun((e) => { + this._onDidRunAction.fire(e.action.id); + this._hide(); + })); this._store.add(autorun(r => { const sel = selection.read(r); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index aa6033e319f48..19d67a29e9503 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { autorun, constObservable, derived, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; @@ -25,6 +26,9 @@ import { IUserInteractionService } from '../../../../platform/userInteraction/br export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { + private readonly _onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this._onDidRunAction.event; + constructor( private readonly _myEditorObs: ObservableCodeEditor, selection: IObservable, @@ -40,7 +44,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { ) { const menu = menuService.createMenu(MenuId.InlineChatEditorAffordance, contextKeyService); - const menuObs = observableFromEvent(menu.onDidChange, () => menu.getActions({ renderShortTitle: true })); + const menuObs = observableFromEvent(menu.onDidChange, () => menu.getActions({ renderShortTitle: false })); const codeActionController = CodeActionController.get(_myEditorObs.editor); const lightBulbObs = codeActionController?.lightBulbState; @@ -108,6 +112,8 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const element = _hover.read(r); this._hoverVisible.set(!!element, undefined); })); + + this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); } private _doShowHover(): void { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 1789bec7a6e4a..252277b9702b9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -26,7 +26,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ChatEditingAcceptRejectActionViewItem } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; -import { ACTION_START, CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; import { StickyScrollController } from '../../../../editor/contrib/stickyScroll/browser/stickyScrollController.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -60,7 +60,6 @@ export class InlineChatInputWidget extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ICommandService private readonly _commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, @@ -191,7 +190,7 @@ export class InlineChatInputWidget extends Disposable { ? localize('placeholderWithSelection', "Modify selected code") : localize('placeholderNoSelection', "Generate code"); - this._input.updateOptions({ placeholder: this._keybindingService.appendKeybinding(placeholderText, ACTION_START) }); + this._input.updateOptions({ placeholder: placeholderText }); })); From cbbb61782ab3eab6972db21f9cbaa94e8088110f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 18 Feb 2026 13:22:50 +0100 Subject: [PATCH 15/36] different structure for curated models (#295994) use different structure for curated models --- .../contrib/chat/common/languageModels.ts | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 4f71f64b7fec7..e452196a385e0 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -18,7 +18,7 @@ import { equals } from '../../../../base/common/objects.js'; import Severity from '../../../../base/common/severity.js'; import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { isString } from '../../../../base/common/types.js'; +import { isObject, isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; @@ -512,17 +512,15 @@ interface IRawCuratedModel { readonly id: string; readonly isNew?: boolean; readonly minVSCodeVersion?: string; - readonly paidOnly?: boolean; } interface IChatControlResponse { readonly version: number; readonly restrictedChatParticipants: { [name: string]: string[] }; - readonly curatedModels?: (string | IRawCuratedModel)[]; -} - -function normalizeCuratedModels(models: (string | IRawCuratedModel)[]): IRawCuratedModel[] { - return models.map(m => typeof m === 'string' ? { id: m } : m); + readonly curatedModels?: { + readonly free?: IRawCuratedModel[]; + readonly paid?: IRawCuratedModel[]; + }; } export class LanguageModelsService implements ILanguageModelsService { @@ -1425,19 +1423,26 @@ export class LanguageModelsService implements ILanguageModelsService { return this._curatedModels; } - private _setCuratedModels(models: IRawCuratedModel[]): void { + private _setCuratedModels(free: IRawCuratedModel[], paid: IRawCuratedModel[]): void { const toPublic = (m: IRawCuratedModel): ICuratedModel => ({ id: m.id, isNew: m.isNew, minVSCodeVersion: m.minVSCodeVersion }); - this._curatedModels = { - free: models.filter(m => !m.paidOnly).map(toPublic), - paid: models.filter(m => m.paidOnly).map(toPublic), - }; + this._curatedModels = { free: [], paid: [] }; const newIds = new Set(); - for (const model of models) { + + for (const model of free) { + this._curatedModels.free.push(toPublic(model)); if (model.isNew) { newIds.add(model.id); } } + + for (const model of paid) { + this._curatedModels.paid.push(toPublic(model)); + if (model.isNew) { + newIds.add(model.id); + } + } + this._newModelIds = newIds; this._onDidChangeNewModelIds.fire(); } @@ -1495,9 +1500,9 @@ export class LanguageModelsService implements ILanguageModelsService { // Restore curated models from storage const rawCurated = this._storageService.get(CHAT_CURATED_MODELS_STORAGE_KEY, StorageScope.APPLICATION); try { - const curated = JSON.parse(rawCurated ?? '[]'); - if (Array.isArray(curated)) { - this._setCuratedModels(normalizeCuratedModels(curated)); + const curated = JSON.parse(rawCurated ?? '{}'); + if (isObject(curated) && Array.isArray(curated.free) && Array.isArray(curated.paid)) { + this._setCuratedModels(curated.free, curated.paid); } } catch (err) { this._storageService.remove(CHAT_CURATED_MODELS_STORAGE_KEY, StorageScope.APPLICATION); @@ -1536,8 +1541,8 @@ export class LanguageModelsService implements ILanguageModelsService { this._storageService.store(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE); // Update curated models - if (result.curatedModels && Array.isArray(result.curatedModels)) { - this._setCuratedModels(normalizeCuratedModels(result.curatedModels)); + if (result.curatedModels) { + this._setCuratedModels(result.curatedModels?.free ?? [], result.curatedModels?.paid ?? []); this._storageService.store(CHAT_CURATED_MODELS_STORAGE_KEY, JSON.stringify(result.curatedModels), StorageScope.APPLICATION, StorageTarget.MACHINE); } } From 0a40ab4cce417f1013fc8bed5346be38d1c7434b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 13:23:09 +0100 Subject: [PATCH 16/36] sessions - show count for all groups (fix #291606) (#295992) --- .../agentSessions/agentSessionsViewer.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index bcc94cad564c3..daabea59fff01 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -758,13 +758,19 @@ export function groupAgentSessionsByDate(sessions: IAgentSession[]): Map ({ + section, + label: localize('agentSessions.sectionWithCount', "{0} ({1})", AgentSessionSectionLabels[section], sessions.length), + sessions + }); + return new Map([ - [AgentSessionSection.InProgress, { section: AgentSessionSection.InProgress, label: AgentSessionSectionLabels[AgentSessionSection.InProgress], sessions: inProgressSessions }], - [AgentSessionSection.Today, { section: AgentSessionSection.Today, label: AgentSessionSectionLabels[AgentSessionSection.Today], sessions: todaySessions }], - [AgentSessionSection.Yesterday, { section: AgentSessionSection.Yesterday, label: AgentSessionSectionLabels[AgentSessionSection.Yesterday], sessions: yesterdaySessions }], - [AgentSessionSection.Week, { section: AgentSessionSection.Week, label: AgentSessionSectionLabels[AgentSessionSection.Week], sessions: weekSessions }], - [AgentSessionSection.Older, { section: AgentSessionSection.Older, label: AgentSessionSectionLabels[AgentSessionSection.Older], sessions: olderSessions }], - [AgentSessionSection.Archived, { section: AgentSessionSection.Archived, label: localize('agentSessions.archivedSectionWithCount', "Archived ({0})", archivedSessions.length), sessions: archivedSessions }], + [AgentSessionSection.InProgress, sectionWithCount(AgentSessionSection.InProgress, inProgressSessions)], + [AgentSessionSection.Today, sectionWithCount(AgentSessionSection.Today, todaySessions)], + [AgentSessionSection.Yesterday, sectionWithCount(AgentSessionSection.Yesterday, yesterdaySessions)], + [AgentSessionSection.Week, sectionWithCount(AgentSessionSection.Week, weekSessions)], + [AgentSessionSection.Older, sectionWithCount(AgentSessionSection.Older, olderSessions)], + [AgentSessionSection.Archived, sectionWithCount(AgentSessionSection.Archived, archivedSessions)], ]); } From 919612309dfda5ce144c9cb814dc9fc7493c3d0c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:28:24 +0100 Subject: [PATCH 17/36] Fix quick dispose/reregister of tree view (#295996) --- .../workbench/api/common/extHostTreeViews.ts | 9 +++++-- .../api/test/browser/extHostTreeViews.test.ts | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 6734cb807f7bb..fce5e9d47db5a 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -151,7 +151,13 @@ export class ExtHostTreeViews extends Disposable implements ExtHostTreeViewsShap dispose: async () => { // Wait for the registration promise to finish before doing the dispose. await registerPromise; - this._treeViews.delete(viewId); + // Only notify the main thread if this view was not replaced by a new registration. + // When an extension disposes a view and immediately re-registers it, the new + // registration may have already updated _treeViews before this async dispose runs. + if (this._treeViews.get(viewId) === treeView) { + this._treeViews.delete(viewId); + this._proxy.$disposeTree(viewId); + } treeView.dispose(); } }; @@ -1108,6 +1114,5 @@ class ExtHostTreeView extends Disposable { this._refreshCancellationSource.dispose(); this._clearAll(); - this._proxy.$disposeTree(this._viewId); } } diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 88d9928ac790e..751a4ff73f9f7 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -499,6 +499,30 @@ suite('ExtHostTreeView', function () { }); }); + test('dispose and re-register tree view', async () => { + const disposeTreeSpy = sinon.spy(target, '$disposeTree'); + const registerSpy = sinon.spy(target, '$registerTreeViewDataProvider'); + + // Create, dispose, and re-register a tree view with the same id + const treeView1 = testObject.createTreeView('reRegisterTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription); + treeView1.dispose(); + const treeView2 = testObject.createTreeView('reRegisterTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription); + + // Let all pending microtasks (the async dispose) settle + await new Promise(r => setTimeout(r, 0)); + + // The new view should work — $getChildren should return results, not reject + const elements = await testObject.$getChildren('reRegisterTreeProvider'); + assert.deepStrictEqual(unBatchChildren(elements)?.map(e => e.handle), ['0/0:a', '0/0:b']); + + // $registerTreeViewDataProvider should have been called twice (once per createTreeView) + assert.strictEqual(registerSpy.callCount, 2); + // $disposeTree should NOT have been called — the old async dispose should detect it was replaced + assert.strictEqual(disposeTreeSpy.callCount, 0); + + treeView2.dispose(); + }); + test('reveal will throw an error if getParent is not implemented', () => { const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription); return treeView.reveal({ key: 'a' }) From 37553522f364dafd475bb956656c15e0e6e6e38a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 13:28:59 +0100 Subject: [PATCH 18/36] Support dismissing modal overlays by clicking outside (fix #295893) (#295995) --- src/vs/workbench/browser/parts/editor/modalEditorPart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index b58cb981051f4..bdbd911fe2884 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -78,8 +78,8 @@ export class ModalEditorPart { if (e.target === modalElement) { EventHelper.stop(e, true); - // Guide focus back into the modal when clicking outside modal - editorPartContainer.focus(); + // Close modal when clicking outside the dialog + editorPart.close(); } })); From 514eff1c9300d16f83e5dacfd87da2f7ee30fea9 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 18 Feb 2026 13:57:18 +0100 Subject: [PATCH 19/36] fix: trim input value and clear model on reset in InlineChatInputWidget --- .../contrib/inlineChat/browser/inlineChatOverlayWidget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 252277b9702b9..08b5bc9c2575e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -224,7 +224,7 @@ export class InlineChatInputWidget extends Disposable { } get value(): string { - return this._input.getModel().getValue(); + return this._input.getModel().getValue().trim(); } /** @@ -319,6 +319,7 @@ export class InlineChatInputWidget extends Disposable { this._editorObs.editor.focus(); } this._position.set(null, undefined); + this._input.getModel().setValue(''); this._showStore.clear(); } } From 2e5af5b088cb67f3d92a30013a5603c6006a3301 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 12:59:23 +0000 Subject: [PATCH 20/36] Add chat thinking shimmer color to 2026 Light theme --- extensions/theme-2026/themes/2026-light.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 95d65795f8b79..daa83e994e6be 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -258,6 +258,7 @@ "quickInputTitle.background": "#F0F0F3", "chat.requestBubbleBackground": "#EEF4FB", "chat.requestBubbleHoverBackground": "#E6EDFA", + "chat.thinkingShimmer": "#999999", "editorCommentsWidget.rangeBackground": "#EEF4FB", "editorCommentsWidget.rangeActiveBackground": "#E6EDFA", "charts.foreground": "#202020", From d86a93d846f9a3a1b4adac6e1428b938f90655bb Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 13:37:27 +0000 Subject: [PATCH 21/36] Update color theme for 2026 Light: adjust background colors for various UI elements --- extensions/theme-2026/themes/2026-light.json | 34 ++++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 95d65795f8b79..7a96ec0bd809f 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -10,9 +10,9 @@ "descriptionForeground": "#555555", "icon.foreground": "#666666", "focusBorder": "#0069CCFF", - "textBlockQuote.background": "#EDEDED", + "textBlockQuote.background": "#EAEAEA", "textBlockQuote.border": "#F2F3F4FF", - "textCodeBlock.background": "#EDEDED", + "textCodeBlock.background": "#EAEAEA", "textLink.foreground": "#0069CC", "textLink.activeForeground": "#0069CC", "textPreformat.foreground": "#666666", @@ -21,10 +21,10 @@ "button.foreground": "#FFFFFF", "button.hoverBackground": "#0063C1", "button.border": "#EEEEF1", - "button.secondaryBackground": "#EDEDED", + "button.secondaryBackground": "#EAEAEA", "button.secondaryForeground": "#202020", - "button.secondaryHoverBackground": "#EEEEEE", - "checkbox.background": "#EDEDED", + "button.secondaryHoverBackground": "#F2F3F4", + "checkbox.background": "#EAEAEA", "checkbox.border": "#D8D8D8", "checkbox.foreground": "#202020", "dropdown.background": "#FFFFFF", @@ -61,11 +61,11 @@ "badge.background": "#0069CC", "badge.foreground": "#FFFFFF", "progressBar.background": "#0069CC", - "list.activeSelectionBackground": "#0069CC44", + "list.activeSelectionBackground": "#0069CC1A", "list.activeSelectionForeground": "#202020", - "list.inactiveSelectionBackground": "#E0E0E0", + "list.inactiveSelectionBackground": "#DADADA99", "list.inactiveSelectionForeground": "#202020", - "list.hoverBackground": "#EEEEEE", + "list.hoverBackground": "#DADADA4f", "list.hoverForeground": "#202020", "list.dropBackground": "#0069CC15", "list.focusBackground": "#0069CC1A", @@ -96,7 +96,7 @@ "titleBar.inactiveBackground": "#FAFAFD", "titleBar.inactiveForeground": "#666666", "titleBar.border": "#F2F3F4FF", - "menubar.selectionBackground": "#EDEDED", + "menubar.selectionBackground": "#EAEAEA", "menubar.selectionForeground": "#202020", "menu.background": "#FAFAFD", "menu.foreground": "#202020", @@ -121,10 +121,10 @@ "editor.wordHighlightStrongBackground": "#0069CC26", "editor.findMatchBackground": "#0069CC40", "editor.findMatchHighlightBackground": "#0069CC1A", - "editor.findRangeHighlightBackground": "#EDEDED", - "editor.hoverHighlightBackground": "#EDEDED", - "editor.lineHighlightBackground": "#EDEDED40", - "editor.rangeHighlightBackground": "#EDEDED", + "editor.findRangeHighlightBackground": "#EAEAEA", + "editor.hoverHighlightBackground": "#EAEAEA", + "editor.lineHighlightBackground": "#EAEAEA40", + "editor.rangeHighlightBackground": "#EAEAEA", "editorLink.activeForeground": "#0069CC", "editorWhitespace.foreground": "#66666640", "editorIndentGuide.background": "#F7F7F740", @@ -181,7 +181,7 @@ "statusBar.noFolderBackground": "#F0F0F3", "statusBar.noFolderForeground": "#666666", "statusBarItem.activeBackground": "#EEEEEE", - "statusBarItem.hoverBackground": "#EEEEEE", + "statusBarItem.hoverBackground": "#DADADA4f", "statusBarItem.focusBorder": "#0069CCFF", "statusBarItem.prominentBackground": "#0069CCDD", "statusBarItem.prominentForeground": "#FFFFFF", @@ -195,7 +195,7 @@ "tab.lastPinnedBorder": "#F2F3F4FF", "tab.activeBorder": "#FAFAFD", "tab.activeBorderTop": "#000000", - "tab.hoverBackground": "#EEEEEE", + "tab.hoverBackground": "#DADADA4f", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", "tab.unfocusedActiveForeground": "#666666", @@ -210,10 +210,10 @@ "breadcrumbPicker.background": "#F0F0F3", "notificationCenter.border": "#F2F3F4FF", "notificationCenterHeader.foreground": "#202020", - "notificationCenterHeader.background": "#F0F0F3", + "notificationCenterHeader.background": "#FAFAFD", "notificationToast.border": "#F2F3F4FF", "notifications.foreground": "#202020", - "notifications.background": "#F0F0F3", + "notifications.background": "#FAFAFD", "notifications.border": "#F2F3F4FF", "notificationLink.foreground": "#0069CC", "notificationsWarningIcon.foreground": "#B69500", From 873fa1c29f3439626a8d743562ec08dd4fa45560 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 13:44:45 +0000 Subject: [PATCH 22/36] Add foreground color for italic markup in theme files --- extensions/theme-defaults/themes/dark_vs.json | 3 ++- extensions/theme-defaults/themes/hc_light.json | 3 ++- extensions/theme-defaults/themes/light_vs.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index b66165d69e107..d1812b61ac067 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -156,7 +156,8 @@ { "scope": "markup.italic", "settings": { - "fontStyle": "italic" + "fontStyle": "italic", + "foreground": "#C586C0" } }, { diff --git a/extensions/theme-defaults/themes/hc_light.json b/extensions/theme-defaults/themes/hc_light.json index e59494f9cfb79..572cbe871ddfa 100644 --- a/extensions/theme-defaults/themes/hc_light.json +++ b/extensions/theme-defaults/themes/hc_light.json @@ -122,7 +122,8 @@ { "scope": "markup.italic", "settings": { - "fontStyle": "italic" + "fontStyle": "italic", + "foreground": "#800080" } }, { diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index e4cc701f82cca..3fdbbead3d0d6 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -159,7 +159,8 @@ { "scope": "markup.italic", "settings": { - "fontStyle": "italic" + "fontStyle": "italic", + "foreground": "#800080" } }, { From a83651d29197da15462f75f0c88b514063c85216 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 14:07:35 +0000 Subject: [PATCH 23/36] Update color values for foreground elements in 2026 Dark and Light themes --- extensions/theme-2026/themes/2026-dark.json | 32 +++++++++--------- extensions/theme-2026/themes/2026-light.json | 35 ++++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4f25b3268fc3e..17cb3439dddae 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -7,8 +7,8 @@ "foreground": "#bfbfbf", "disabledForeground": "#666666", "errorForeground": "#f48771", - "descriptionForeground": "#888888", - "icon.foreground": "#888888", + "descriptionForeground": "#8C8C8C", + "icon.foreground": "#8C8C8C", "focusBorder": "#3994BCB3", "textBlockQuote.background": "#242526", "textBlockQuote.border": "#2A2B2CFF", @@ -16,7 +16,7 @@ "textLink.foreground": "#48A0C7", "textLink.activeForeground": "#53A5CA", "textPreformat.background": "#262626", - "textPreformat.foreground": "#888888", + "textPreformat.foreground": "#8C8C8C", "textSeparator.foreground": "#2a2a2aFF", "button.background": "#3994BCF2", "button.foreground": "#FFFFFF", @@ -69,7 +69,7 @@ "list.warningForeground": "#e5ba7d", "activityBar.background": "#191A1B", "activityBar.foreground": "#bfbfbf", - "activityBar.inactiveForeground": "#888888", + "activityBar.inactiveForeground": "#8C8C8C", "activityBar.border": "#2A2B2CFF", "activityBar.activeBorder": "#bfbfbf", "activityBar.activeFocusBorder": "#3994BCB3", @@ -86,7 +86,7 @@ "titleBar.activeBackground": "#191A1B", "titleBar.activeForeground": "#bfbfbf", "titleBar.inactiveBackground": "#191A1B", - "titleBar.inactiveForeground": "#888888", + "titleBar.inactiveForeground": "#8C8C8C", "titleBar.border": "#2A2B2CFF", "menubar.selectionBackground": "#242526", "menubar.selectionForeground": "#bfbfbf", @@ -120,11 +120,11 @@ "editor.lineHighlightBackground": "#242526", "editor.rangeHighlightBackground": "#242526", "editorLink.activeForeground": "#3a94bc", - "editorWhitespace.foreground": "#8888884D", + "editorWhitespace.foreground": "#8C8C8C4D", "editorIndentGuide.background": "#8384854D", "editorIndentGuide.activeBackground": "#838485", "editorRuler.foreground": "#848484", - "editorCodeLens.foreground": "#888888", + "editorCodeLens.foreground": "#8C8C8C", "editorBracketMatch.background": "#3994BC55", "editorBracketMatch.border": "#2A2B2CFF", "editorWidget.background": "#202122", @@ -143,12 +143,12 @@ "peekViewEditor.matchHighlightBackground": "#3994BC33", "peekViewResult.background": "#191A1B", "peekViewResult.fileForeground": "#bfbfbf", - "peekViewResult.lineForeground": "#888888", + "peekViewResult.lineForeground": "#8C8C8C", "peekViewResult.matchHighlightBackground": "#3994BC33", "peekViewResult.selectionBackground": "#3994BC26", "peekViewResult.selectionForeground": "#bfbfbf", "peekViewTitle.background": "#242526", - "peekViewTitleDescription.foreground": "#888888", + "peekViewTitleDescription.foreground": "#8C8C8C", "peekViewTitleLabel.foreground": "#bfbfbf", "editorGutter.background": "#121314", "editorGutter.addedBackground": "#72C892", @@ -166,15 +166,15 @@ "panel.border": "#2A2B2CFF", "panelTitle.activeBorder": "#3994BC", "panelTitle.activeForeground": "#bfbfbf", - "panelTitle.inactiveForeground": "#888888", + "panelTitle.inactiveForeground": "#8C8C8C", "statusBar.background": "#191A1B", - "statusBar.foreground": "#888888", + "statusBar.foreground": "#8C8C8C", "statusBar.border": "#2A2B2CFF", "statusBar.focusBorder": "#3994BCB3", "statusBar.debuggingBackground": "#3994BC", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#191A1B", - "statusBar.noFolderForeground": "#888888", + "statusBar.noFolderForeground": "#8C8C8C", "statusBarItem.activeBackground": "#4B4C4D", "statusBarItem.hoverBackground": "#262728", "statusBarItem.focusBorder": "#3994BCB3", @@ -184,7 +184,7 @@ "tab.activeBackground": "#121314", "tab.activeForeground": "#bfbfbf", "tab.inactiveBackground": "#191A1B", - "tab.inactiveForeground": "#888888", + "tab.inactiveForeground": "#8C8C8C", "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorder": "#121314", @@ -192,12 +192,12 @@ "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", "tab.unfocusedActiveBackground": "#121314", - "tab.unfocusedActiveForeground": "#888888", + "tab.unfocusedActiveForeground": "#8C8C8C", "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", "editorGroupHeader.tabsBackground": "#191A1B", "editorGroupHeader.tabsBorder": "#2A2B2CFF", - "breadcrumb.foreground": "#888888", + "breadcrumb.foreground": "#8C8C8C", "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bfbfbf", "breadcrumb.activeSelectionForeground": "#bfbfbf", @@ -612,7 +612,7 @@ "markup.fenced_code" ], "settings": { - "foreground": "#888888" + "foreground": "#8C8C8C" } }, { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index daa83e994e6be..4f5bd4039f4d4 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -7,15 +7,16 @@ "foreground": "#202020", "disabledForeground": "#BBBBBB", "errorForeground": "#ad0707", - "descriptionForeground": "#555555", - "icon.foreground": "#666666", + "descriptionForeground": "#585858", + "icon.foreground": "#585858", "focusBorder": "#0069CCFF", "textBlockQuote.background": "#EDEDED", "textBlockQuote.border": "#F2F3F4FF", "textCodeBlock.background": "#EDEDED", "textLink.foreground": "#0069CC", "textLink.activeForeground": "#0069CC", - "textPreformat.foreground": "#666666", + "textPreformat.foreground": "#585858", + "textPreformat.background": "#EAEAEA", "textSeparator.foreground": "#EEEEEEFF", "button.background": "#0069CC", "button.foreground": "#FFFFFF", @@ -77,7 +78,7 @@ "list.warningForeground": "#667309", "activityBar.background": "#FAFAFD", "activityBar.foreground": "#202020", - "activityBar.inactiveForeground": "#666666", + "activityBar.inactiveForeground": "#585858", "activityBar.border": "#F2F3F4FF", "activityBar.activeBorder": "#000000", "activityBar.activeFocusBorder": "#0069CCFF", @@ -94,7 +95,7 @@ "titleBar.activeBackground": "#FAFAFD", "titleBar.activeForeground": "#424242", "titleBar.inactiveBackground": "#FAFAFD", - "titleBar.inactiveForeground": "#666666", + "titleBar.inactiveForeground": "#585858", "titleBar.border": "#F2F3F4FF", "menubar.selectionBackground": "#EDEDED", "menubar.selectionForeground": "#202020", @@ -111,7 +112,7 @@ "commandCenter.border": "#D8D8D8", "editor.background": "#FFFFFF", "editor.foreground": "#202020", - "editorLineNumber.foreground": "#666666", + "editorLineNumber.foreground": "#585858", "editorLineNumber.activeForeground": "#202020", "editorCursor.foreground": "#202020", "editor.selectionBackground": "#0069CC1A", @@ -126,11 +127,11 @@ "editor.lineHighlightBackground": "#EDEDED40", "editor.rangeHighlightBackground": "#EDEDED", "editorLink.activeForeground": "#0069CC", - "editorWhitespace.foreground": "#66666640", + "editorWhitespace.foreground": "#58585840", "editorIndentGuide.background": "#F7F7F740", "editorIndentGuide.activeBackground": "#EEEEEE", "editorRuler.foreground": "#F7F7F7", - "editorCodeLens.foreground": "#666666", + "editorCodeLens.foreground": "#585858", "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F2F3F4FF", "editorWidget.background": "#F0F0F3", @@ -148,12 +149,12 @@ "peekViewEditor.matchHighlightBackground": "#0069CC33", "peekViewResult.background": "#F0F0F3", "peekViewResult.fileForeground": "#202020", - "peekViewResult.lineForeground": "#666666", + "peekViewResult.lineForeground": "#585858", "peekViewResult.matchHighlightBackground": "#0069CC33", "peekViewResult.selectionBackground": "#0069CC26", "peekViewResult.selectionForeground": "#202020", "peekViewTitle.background": "#F0F0F3", - "peekViewTitleDescription.foreground": "#666666", + "peekViewTitleDescription.foreground": "#585858", "peekViewTitleLabel.foreground": "#202020", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", @@ -171,15 +172,15 @@ "panel.border": "#F2F3F4FF", "panelTitle.activeBorder": "#000000", "panelTitle.activeForeground": "#202020", - "panelTitle.inactiveForeground": "#666666", + "panelTitle.inactiveForeground": "#585858", "statusBar.background": "#FAFAFD", - "statusBar.foreground": "#666666", + "statusBar.foreground": "#585858", "statusBar.border": "#F2F3F4FF", "statusBar.focusBorder": "#0069CCFF", "statusBar.debuggingBackground": "#0069CC", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#F0F0F3", - "statusBar.noFolderForeground": "#666666", + "statusBar.noFolderForeground": "#585858", "statusBarItem.activeBackground": "#EEEEEE", "statusBarItem.hoverBackground": "#EEEEEE", "statusBarItem.focusBorder": "#0069CCFF", @@ -190,7 +191,7 @@ "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", "tab.inactiveBackground": "#FAFAFD", - "tab.inactiveForeground": "#666666", + "tab.inactiveForeground": "#585858", "tab.border": "#F2F3F4FF", "tab.lastPinnedBorder": "#F2F3F4FF", "tab.activeBorder": "#FAFAFD", @@ -198,12 +199,12 @@ "tab.hoverBackground": "#EEEEEE", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", - "tab.unfocusedActiveForeground": "#666666", + "tab.unfocusedActiveForeground": "#585858", "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", "editorGroupHeader.tabsBorder": "#F2F3F4FF", - "breadcrumb.foreground": "#666666", + "breadcrumb.foreground": "#585858", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", @@ -618,7 +619,7 @@ "markup.fenced_code" ], "settings": { - "foreground": "#666666" + "foreground": "#585858" } }, { From e1956282ce41e50a03100af1161be4b5babbf7df Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 14:14:13 +0000 Subject: [PATCH 24/36] Update italic markup colors in theme files for consistency across themes --- .../test/colorize-results/test_md.json | 42 +++++++++---------- .../test/colorize-results/test_rst.json | 14 +++---- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_md.json b/extensions/vscode-colorize-tests/test/colorize-results/test_md.json index 648feab845afb..7fd99b59bc03e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_md.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_md.json @@ -1809,42 +1809,42 @@ "c": "_", "t": "text.html.markdown meta.paragraph.markdown markup.italic.markdown punctuation.definition.italic.markdown", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_plus": "markup.italic: #C586C0", + "light_plus": "markup.italic: #800080", + "dark_vs": "markup.italic: #C586C0", + "light_vs": "markup.italic: #800080", "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_modern": "markup.italic: #C586C0", + "hc_light": "markup.italic: #800080", + "light_modern": "markup.italic: #800080" } }, { "c": "italics", "t": "text.html.markdown meta.paragraph.markdown markup.italic.markdown", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_plus": "markup.italic: #C586C0", + "light_plus": "markup.italic: #800080", + "dark_vs": "markup.italic: #C586C0", + "light_vs": "markup.italic: #800080", "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_modern": "markup.italic: #C586C0", + "hc_light": "markup.italic: #800080", + "light_modern": "markup.italic: #800080" } }, { "c": "_", "t": "text.html.markdown meta.paragraph.markdown markup.italic.markdown punctuation.definition.italic.markdown", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_plus": "markup.italic: #C586C0", + "light_plus": "markup.italic: #800080", + "dark_vs": "markup.italic: #C586C0", + "light_vs": "markup.italic: #800080", "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_modern": "markup.italic: #C586C0", + "hc_light": "markup.italic: #800080", + "light_modern": "markup.italic: #800080" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json b/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json index aa8468c4d8a7c..a2838df578b62 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json @@ -3,14 +3,14 @@ "c": "*italics*", "t": "source.rst markup.italic", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_plus": "markup.italic: #C586C0", + "light_plus": "markup.italic: #800080", + "dark_vs": "markup.italic: #C586C0", + "light_vs": "markup.italic: #800080", "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_modern": "markup.italic: #C586C0", + "hc_light": "markup.italic: #800080", + "light_modern": "markup.italic: #800080" } }, { From ffe44095bc85bdcd855d6c6aa535884f55cc4ee8 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 14:22:50 +0000 Subject: [PATCH 25/36] Update foreground color values in 2026 Light theme for consistency --- extensions/theme-2026/themes/2026-light.json | 34 ++++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 4f5bd4039f4d4..4834e1d0e3228 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -7,15 +7,15 @@ "foreground": "#202020", "disabledForeground": "#BBBBBB", "errorForeground": "#ad0707", - "descriptionForeground": "#585858", - "icon.foreground": "#585858", + "descriptionForeground": "#5A5A5A", + "icon.foreground": "#5A5A5A", "focusBorder": "#0069CCFF", "textBlockQuote.background": "#EDEDED", "textBlockQuote.border": "#F2F3F4FF", "textCodeBlock.background": "#EDEDED", "textLink.foreground": "#0069CC", "textLink.activeForeground": "#0069CC", - "textPreformat.foreground": "#585858", + "textPreformat.foreground": "#5A5A5A", "textPreformat.background": "#EAEAEA", "textSeparator.foreground": "#EEEEEEFF", "button.background": "#0069CC", @@ -78,7 +78,7 @@ "list.warningForeground": "#667309", "activityBar.background": "#FAFAFD", "activityBar.foreground": "#202020", - "activityBar.inactiveForeground": "#585858", + "activityBar.inactiveForeground": "#5A5A5A", "activityBar.border": "#F2F3F4FF", "activityBar.activeBorder": "#000000", "activityBar.activeFocusBorder": "#0069CCFF", @@ -95,7 +95,7 @@ "titleBar.activeBackground": "#FAFAFD", "titleBar.activeForeground": "#424242", "titleBar.inactiveBackground": "#FAFAFD", - "titleBar.inactiveForeground": "#585858", + "titleBar.inactiveForeground": "#5A5A5A", "titleBar.border": "#F2F3F4FF", "menubar.selectionBackground": "#EDEDED", "menubar.selectionForeground": "#202020", @@ -112,7 +112,7 @@ "commandCenter.border": "#D8D8D8", "editor.background": "#FFFFFF", "editor.foreground": "#202020", - "editorLineNumber.foreground": "#585858", + "editorLineNumber.foreground": "#5A5A5A", "editorLineNumber.activeForeground": "#202020", "editorCursor.foreground": "#202020", "editor.selectionBackground": "#0069CC1A", @@ -127,11 +127,11 @@ "editor.lineHighlightBackground": "#EDEDED40", "editor.rangeHighlightBackground": "#EDEDED", "editorLink.activeForeground": "#0069CC", - "editorWhitespace.foreground": "#58585840", + "editorWhitespace.foreground": "#5A5A5A40", "editorIndentGuide.background": "#F7F7F740", "editorIndentGuide.activeBackground": "#EEEEEE", "editorRuler.foreground": "#F7F7F7", - "editorCodeLens.foreground": "#585858", + "editorCodeLens.foreground": "#5A5A5A", "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F2F3F4FF", "editorWidget.background": "#F0F0F3", @@ -149,12 +149,12 @@ "peekViewEditor.matchHighlightBackground": "#0069CC33", "peekViewResult.background": "#F0F0F3", "peekViewResult.fileForeground": "#202020", - "peekViewResult.lineForeground": "#585858", + "peekViewResult.lineForeground": "#5A5A5A", "peekViewResult.matchHighlightBackground": "#0069CC33", "peekViewResult.selectionBackground": "#0069CC26", "peekViewResult.selectionForeground": "#202020", "peekViewTitle.background": "#F0F0F3", - "peekViewTitleDescription.foreground": "#585858", + "peekViewTitleDescription.foreground": "#5A5A5A", "peekViewTitleLabel.foreground": "#202020", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", @@ -172,15 +172,15 @@ "panel.border": "#F2F3F4FF", "panelTitle.activeBorder": "#000000", "panelTitle.activeForeground": "#202020", - "panelTitle.inactiveForeground": "#585858", + "panelTitle.inactiveForeground": "#5A5A5A", "statusBar.background": "#FAFAFD", - "statusBar.foreground": "#585858", + "statusBar.foreground": "#5A5A5A", "statusBar.border": "#F2F3F4FF", "statusBar.focusBorder": "#0069CCFF", "statusBar.debuggingBackground": "#0069CC", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#F0F0F3", - "statusBar.noFolderForeground": "#585858", + "statusBar.noFolderForeground": "#5A5A5A", "statusBarItem.activeBackground": "#EEEEEE", "statusBarItem.hoverBackground": "#EEEEEE", "statusBarItem.focusBorder": "#0069CCFF", @@ -191,7 +191,7 @@ "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", "tab.inactiveBackground": "#FAFAFD", - "tab.inactiveForeground": "#585858", + "tab.inactiveForeground": "#5A5A5A", "tab.border": "#F2F3F4FF", "tab.lastPinnedBorder": "#F2F3F4FF", "tab.activeBorder": "#FAFAFD", @@ -199,12 +199,12 @@ "tab.hoverBackground": "#EEEEEE", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", - "tab.unfocusedActiveForeground": "#585858", + "tab.unfocusedActiveForeground": "#5A5A5A", "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", "editorGroupHeader.tabsBorder": "#F2F3F4FF", - "breadcrumb.foreground": "#585858", + "breadcrumb.foreground": "#5A5A5A", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", @@ -619,7 +619,7 @@ "markup.fenced_code" ], "settings": { - "foreground": "#585858" + "foreground": "#5A5A5A" } }, { From 92f02ddd49c022ca46bdfe453702f7eebbb21ce2 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 14:26:37 +0000 Subject: [PATCH 26/36] Update foreground color values for consistency in 2026 Light theme --- extensions/theme-2026/themes/2026-light.json | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 4834e1d0e3228..8f07311d78a38 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -7,16 +7,16 @@ "foreground": "#202020", "disabledForeground": "#BBBBBB", "errorForeground": "#ad0707", - "descriptionForeground": "#5A5A5A", - "icon.foreground": "#5A5A5A", + "descriptionForeground": "#606060", + "icon.foreground": "#606060", "focusBorder": "#0069CCFF", "textBlockQuote.background": "#EDEDED", "textBlockQuote.border": "#F2F3F4FF", "textCodeBlock.background": "#EDEDED", "textLink.foreground": "#0069CC", "textLink.activeForeground": "#0069CC", - "textPreformat.foreground": "#5A5A5A", - "textPreformat.background": "#EAEAEA", + "textPreformat.foreground": "#606060", + "textPreformat.background": "#ECECEC", "textSeparator.foreground": "#EEEEEEFF", "button.background": "#0069CC", "button.foreground": "#FFFFFF", @@ -78,7 +78,7 @@ "list.warningForeground": "#667309", "activityBar.background": "#FAFAFD", "activityBar.foreground": "#202020", - "activityBar.inactiveForeground": "#5A5A5A", + "activityBar.inactiveForeground": "#606060", "activityBar.border": "#F2F3F4FF", "activityBar.activeBorder": "#000000", "activityBar.activeFocusBorder": "#0069CCFF", @@ -95,7 +95,7 @@ "titleBar.activeBackground": "#FAFAFD", "titleBar.activeForeground": "#424242", "titleBar.inactiveBackground": "#FAFAFD", - "titleBar.inactiveForeground": "#5A5A5A", + "titleBar.inactiveForeground": "#606060", "titleBar.border": "#F2F3F4FF", "menubar.selectionBackground": "#EDEDED", "menubar.selectionForeground": "#202020", @@ -112,7 +112,7 @@ "commandCenter.border": "#D8D8D8", "editor.background": "#FFFFFF", "editor.foreground": "#202020", - "editorLineNumber.foreground": "#5A5A5A", + "editorLineNumber.foreground": "#606060", "editorLineNumber.activeForeground": "#202020", "editorCursor.foreground": "#202020", "editor.selectionBackground": "#0069CC1A", @@ -127,11 +127,11 @@ "editor.lineHighlightBackground": "#EDEDED40", "editor.rangeHighlightBackground": "#EDEDED", "editorLink.activeForeground": "#0069CC", - "editorWhitespace.foreground": "#5A5A5A40", + "editorWhitespace.foreground": "#60606040", "editorIndentGuide.background": "#F7F7F740", "editorIndentGuide.activeBackground": "#EEEEEE", "editorRuler.foreground": "#F7F7F7", - "editorCodeLens.foreground": "#5A5A5A", + "editorCodeLens.foreground": "#606060", "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F2F3F4FF", "editorWidget.background": "#F0F0F3", @@ -149,12 +149,12 @@ "peekViewEditor.matchHighlightBackground": "#0069CC33", "peekViewResult.background": "#F0F0F3", "peekViewResult.fileForeground": "#202020", - "peekViewResult.lineForeground": "#5A5A5A", + "peekViewResult.lineForeground": "#606060", "peekViewResult.matchHighlightBackground": "#0069CC33", "peekViewResult.selectionBackground": "#0069CC26", "peekViewResult.selectionForeground": "#202020", "peekViewTitle.background": "#F0F0F3", - "peekViewTitleDescription.foreground": "#5A5A5A", + "peekViewTitleDescription.foreground": "#606060", "peekViewTitleLabel.foreground": "#202020", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", @@ -172,15 +172,15 @@ "panel.border": "#F2F3F4FF", "panelTitle.activeBorder": "#000000", "panelTitle.activeForeground": "#202020", - "panelTitle.inactiveForeground": "#5A5A5A", + "panelTitle.inactiveForeground": "#606060", "statusBar.background": "#FAFAFD", - "statusBar.foreground": "#5A5A5A", + "statusBar.foreground": "#606060", "statusBar.border": "#F2F3F4FF", "statusBar.focusBorder": "#0069CCFF", "statusBar.debuggingBackground": "#0069CC", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#F0F0F3", - "statusBar.noFolderForeground": "#5A5A5A", + "statusBar.noFolderForeground": "#606060", "statusBarItem.activeBackground": "#EEEEEE", "statusBarItem.hoverBackground": "#EEEEEE", "statusBarItem.focusBorder": "#0069CCFF", @@ -191,7 +191,7 @@ "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", "tab.inactiveBackground": "#FAFAFD", - "tab.inactiveForeground": "#5A5A5A", + "tab.inactiveForeground": "#606060", "tab.border": "#F2F3F4FF", "tab.lastPinnedBorder": "#F2F3F4FF", "tab.activeBorder": "#FAFAFD", @@ -199,12 +199,12 @@ "tab.hoverBackground": "#EEEEEE", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", - "tab.unfocusedActiveForeground": "#5A5A5A", + "tab.unfocusedActiveForeground": "#606060", "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", "editorGroupHeader.tabsBorder": "#F2F3F4FF", - "breadcrumb.foreground": "#5A5A5A", + "breadcrumb.foreground": "#606060", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", @@ -619,7 +619,7 @@ "markup.fenced_code" ], "settings": { - "foreground": "#5A5A5A" + "foreground": "#606060" } }, { From cfe24398f333ecf662bea723f8292901a8ae9305 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 18 Feb 2026 14:32:51 +0000 Subject: [PATCH 27/36] Increase max-width of extension editor to 95% for improved layout --- .../contrib/extensions/browser/media/extensionEditor.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 2e1cece4685df..8daf6dc94270d 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -9,7 +9,7 @@ display: flex; flex-direction: column; margin: 0px auto; - max-width: 90%; + max-width: 95%; } .extension-editor .clickable { From bec3414838649d24fbbf61b52b39844be5f9ceb8 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 18 Feb 2026 15:37:20 +0100 Subject: [PATCH 28/36] refactor: rename ChatEditorInlineGutter to ChatEditorInlineMenu and update related actions --- src/vs/platform/actions/common/actions.ts | 2 +- .../browser/actions/chatContextActions.ts | 10 +++ .../inlineChat/browser/inlineChatActions.ts | 4 -- .../browser/inlineChatOverlayWidget.ts | 59 ++++++++++++++--- .../inlineChat/browser/media/inlineChat.css | 40 ------------ .../media/inlineChatEditorAffordance.css | 2 +- .../browser/media/inlineChatOverlayWidget.css | 63 ++++++++++++++++--- .../actions/common/menusExtensionPoint.ts | 2 +- 8 files changed, 118 insertions(+), 64 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 9b720b3d0cb55..86afa862d5def 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -281,7 +281,7 @@ export class MenuId { static readonly ChatToolOutputResourceContext = new MenuId('ChatToolOutputResourceContext'); static readonly ChatMultiDiffContext = new MenuId('ChatMultiDiffContext'); static readonly ChatConfirmationMenu = new MenuId('ChatConfirmationMenu'); - static readonly ChatEditorInlineGutter = new MenuId('ChatEditorInlineGutter'); + static readonly ChatEditorInlineMenu = new MenuId('ChatEditorInlineGutter'); static readonly ChatEditorInlineExecute = new MenuId('ChatEditorInputExecute'); static readonly ChatEditorInlineInputSide = new MenuId('ChatEditorInputSide'); static readonly InlineChatEditorAffordance = new MenuId('InlineChatEditorAffordance'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index ccb4de93996ac..5542c4cad88be 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -178,6 +178,11 @@ class AttachFileToChatAction extends AttachResourceAction { group: '0_chat', order: 3, when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate()) + }, { + id: MenuId.ChatEditorInlineMenu, + group: '0_chat', + order: 3, + when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate()) }] }); } @@ -313,6 +318,11 @@ class AttachSelectionToChatAction extends Action2 { group: '0_chat', order: 2, when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection) + }, { + id: MenuId.ChatEditorInlineMenu, + group: '0_chat', + order: 2, + when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection) }] }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 69d0589726692..1b1a639631153 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -75,10 +75,6 @@ export class StartSessionAction extends Action2 { id: MenuId.ChatTitleBarMenu, group: 'a_open', order: 3, - }, { - id: MenuId.ChatEditorInlineGutter, - group: '1_chat', - order: 1, }] }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 08b5bc9c2575e..4c2c7a41d6919 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -6,7 +6,10 @@ import './media/inlineChatOverlayWidget.css'; import * as dom from '../../../../base/browser/dom.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { BaseActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -21,7 +24,8 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/b import { IModelService } from '../../../../editor/common/services/model.js'; import { localize } from '../../../../nls.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -62,6 +66,7 @@ export class InlineChatInputWidget extends Disposable { private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ICommandService private readonly _commandService: ICommandService, + @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, @@ -80,6 +85,22 @@ export class InlineChatInputWidget extends Disposable { // Create toolbar container this._toolbarContainer = dom.append(this._container, dom.$('.toolbar')); + // Create vertical actions bar below the input container + const actionsContainer = dom.append(this._domNode, dom.$('.inline-chat-gutter-actions')); + const actionBar = this._store.add(new ActionBar(actionsContainer, { + orientation: ActionsOrientation.VERTICAL, + preventLoopNavigation: true, + })); + const actionsMenu = this._store.add(this._menuService.createMenu(MenuId.ChatEditorInlineMenu, this._contextKeyService)); + const updateActions = () => { + const actions = getFlatActionBarActions(actionsMenu.getActions({ shouldForwardArgs: true })); + actionBar.clear(); + actionBar.push(actions); + dom.setVisibility(actions.length > 0, actionsContainer); + }; + this._store.add(actionsMenu.onDidChange(updateActions)); + updateActions(); + // Create editor options const options = getSimpleEditorOptions(configurationService); options.wordWrap = 'off'; @@ -93,9 +114,10 @@ export class InlineChatInputWidget extends Disposable { options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; options.renderLineHighlight = 'none'; options.fontFamily = DEFAULT_FONT_FAMILY; - options.fontSize = 12; - options.lineHeight = 18; + options.fontSize = 13; + options.lineHeight = 20; options.cursorWidth = 1; + options.padding = { top: 2, bottom: 2 }; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, @@ -146,7 +168,7 @@ export class InlineChatInputWidget extends Disposable { this._layoutData = derived(r => { - const totalWidth = contentWidth.read(r) + toolbarWidth.read(r); + const totalWidth = contentWidth.read(r) + 6 + toolbarWidth.read(r); const minWidth = minWidgetWidth.read(r); const maxWidth = maxWidgetWidth.read(r); const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' @@ -172,7 +194,7 @@ export class InlineChatInputWidget extends Disposable { this._store.add(autorun(r => { const { toolbarWidth, totalWidth, height } = this._layoutData.read(r); - const inputWidth = totalWidth - toolbarWidth; + const inputWidth = totalWidth - toolbarWidth - 6; this._container.style.width = `${totalWidth}px`; this._inputContainer.style.width = `${inputWidth}px`; this._input.layout({ width: inputWidth, height }); @@ -187,8 +209,8 @@ export class InlineChatInputWidget extends Disposable { const selection = this._editorObs.cursorSelection.read(r); const hasSelection = selection && !selection.isEmpty(); const placeholderText = hasSelection - ? localize('placeholderWithSelection', "Modify selected code") - : localize('placeholderNoSelection', "Generate code"); + ? localize('placeholderWithSelection', "Describe how to change this") + : localize('placeholderNoSelection', "Describe what to generate"); this._input.updateOptions({ placeholder: placeholderText }); })); @@ -201,7 +223,7 @@ export class InlineChatInputWidget extends Disposable { })); this._store.add(toDisposable(() => inputHasText.reset())); - // Handle Enter key to submit + // Handle Enter key to submit, ArrowDown to move to actions this._store.add(this._input.onKeyDown(e => { if (e.keyCode === KeyCode.Enter && !e.shiftKey) { e.preventDefault(); @@ -215,9 +237,30 @@ export class InlineChatInputWidget extends Disposable { e.stopPropagation(); this.hide(); } + } else if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { + const model = this._input.getModel(); + const position = this._input.getPosition(); + if (position && position.lineNumber === model.getLineCount()) { + e.preventDefault(); + e.stopPropagation(); + actionBar.focus(0); + } } })); + // ArrowUp on first action bar item moves focus back to input editor + this._store.add(dom.addDisposableListener(actionBar.domNode, 'keydown', (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.UpArrow) { + const firstItem = actionBar.viewItems[0] as BaseActionViewItem | undefined; + if (firstItem?.element && dom.isAncestorOfActiveElement(firstItem.element)) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); + } + } + }, true)); + // Track focus - hide when focus leaves const focusTracker = this._store.add(dom.trackFocus(this._domNode)); this._store.add(focusTracker.onDidBlur(() => this.hide())); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 73344bbf55fb9..da1e670cf8bc7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -321,43 +321,3 @@ .monaco-workbench .inline-chat .chat-attached-context { padding: 2px 0px; } - -/* Gutter menu overlay widget */ -.inline-chat-gutter-menu { - background: var(--vscode-panel-background); - border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); - border-radius: 5px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); - z-index: 10000; -} - -.inline-chat-gutter-menu .input { - padding: 0 6px; -} - -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item { - display: flex; - justify-content: space-between; - border-radius: 3px; - margin: 0 4px; -} - -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item .action-label { - font-size: 13px; - width: 100%; -} - -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover, -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within { - background-color: var(--vscode-list-activeSelectionBackground); - color: var(--vscode-list-activeSelectionForeground); - outline: 1px solid var(--vscode-menu-selectionBorder, transparent); - outline-offset: -1px; -} - -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover .action-label, -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within .action-label { - color: var(--vscode-list-activeSelectionForeground); - outline: 1px solid var(--vscode-menu-selectionBorder, transparent); - outline-offset: -1px; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index b13f086fd951d..b50a69f8752d0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -6,7 +6,7 @@ .inline-chat-content-widget { background-color: var(--vscode-panel-background); padding: 0 1px; - border-radius: 5px; + border-radius: 8px; display: flex; align-items: center; box-shadow: 0 4px 8px var(--vscode-widget-shadow); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index f88c3626fad9c..a44cc4bb7fb00 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -3,42 +3,87 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +/* Gutter menu overlay widget */ +.inline-chat-gutter-menu { + background: var(--vscode-panel-background); + border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); + border-radius: 8px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + z-index: 10000; +} + +.inline-chat-gutter-menu .input { + padding: 0 3px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item { + display: flex; + justify-content: space-between; + border-radius: 3px; + margin: 0 4px; +} + +.inline-chat-gutter-menu .inline-chat-gutter-actions { + padding-bottom: 2px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item .action-label { + font-size: 13px; + width: 100%; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover, +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; +} + +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover .action-label, +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within .action-label { + color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; +} + + .inline-chat-gutter-menu.clamped { transition: top 100ms; } -.inline-chat-gutter-container { +.inline-chat-gutter-menu .inline-chat-gutter-container { box-sizing: border-box; display: flex; align-items: center; - margin: 2px; + margin: 6px 4px 4px 4px; background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); - border-radius: 5px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + border-radius: 4px; overflow: hidden; } -.inline-chat-gutter-container.focused { +.inline-chat-gutter-menu .inline-chat-gutter-container.focused { border-color: var(--vscode-focusBorder); } -.inline-chat-gutter-container > .input { +.inline-chat-gutter-menu .inline-chat-gutter-container > .input { flex: 1; min-width: 0; } -.inline-chat-gutter-container > .input .monaco-editor-background { +.inline-chat-gutter-menu .inline-chat-gutter-container > .input .monaco-editor-background { background-color: var(--vscode-input-background); } -.inline-chat-gutter-container > .toolbar { +.inline-chat-gutter-menu .inline-chat-gutter-container > .toolbar { display: flex; align-items: center; padding: 0 4px; } -.inline-chat-gutter-container > .toolbar .monaco-action-bar .actions-container { +.inline-chat-gutter-menu .inline-chat-gutter-container > .toolbar .monaco-action-bar .actions-container { gap: 2px; } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index ad7aa09f9ea6b..7f5c8e64d149f 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -503,7 +503,7 @@ const apiMenus: IAPIMenu[] = [ }, { key: 'chat/editor/inlineGutter', - id: MenuId.ChatEditorInlineGutter, + id: MenuId.ChatEditorInlineMenu, description: localize('menus.chatEditorInlineGutter', "The inline gutter menu in the chat editor."), supportsSubmenus: false, proposed: 'contribChatEditorInlineGutterMenu', From 1415a3434382cb558caecb67a04127a6481fd7b9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 18 Feb 2026 15:45:13 +0100 Subject: [PATCH 29/36] improve welcome experience (#296014) --- .../chat/browser/media/chatWelcomePart.css | 138 +++++--- .../contrib/chat/browser/newChatViewPane.ts | 324 ++++++++++++++---- .../browser/sessionsManagementService.ts | 163 ++++++--- 3 files changed, 458 insertions(+), 167 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css index 6316384afb338..5d9ebc89a95ab 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css @@ -12,7 +12,7 @@ height: 100%; box-sizing: border-box; overflow-x: hidden; - transition: justify-content 0.4s ease; + padding-bottom: 10%; } .chat-full-welcome.revealed { @@ -29,32 +29,29 @@ overflow: visible; } -/* Mascot */ -.chat-full-welcome-mascot { - width: 80px; - height: 80px; +/* Watermark letterpress */ +.chat-full-welcome-letterpress { + width: 100%; + max-width: 200px; + aspect-ratio: 1/1; + background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-dark.svg'); background-size: contain; - background-repeat: no-repeat; background-position: center; + background-repeat: no-repeat; margin-top: 8px; - margin-bottom: 12px; - animation: chat-full-welcome-mascot-bounce 1s ease-in-out infinite; - transition: animation-duration 0.3s ease; - background-image: url('../../../../../workbench/browser/media/code-icon.svg'); + margin-bottom: 20px; } -.chat-full-welcome.revealed .chat-full-welcome-mascot { - animation-duration: 2s; +.vs .chat-full-welcome-letterpress { + background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-light.svg'); } -@keyframes chat-full-welcome-mascot-bounce { - 0%, 100% { - transform: translateY(0); - } +.hc-light .chat-full-welcome-letterpress { + background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-hcLight.svg'); +} - 50% { - transform: translateY(-6px); - } +.hc-black .chat-full-welcome-letterpress { + background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-hcDark.svg'); } /* Input slot */ @@ -71,18 +68,18 @@ animation: chat-full-welcome-fade-in 0.35s ease 0.15s both; } -/* Option group pickers container (below the input) */ +/* Option group pickers container (above the input) */ .chat-full-welcome-pickers-container { display: none; - justify-content: center; width: 100%; max-width: 800px; - margin: 12px; + margin: 0 0 24px 0; + padding: 0; box-sizing: border-box; } .chat-full-welcome.revealed .chat-full-welcome-pickers-container { - display: flex; + display: block; animation: chat-full-welcome-fade-in 0.35s ease 0.1s both; } @@ -103,6 +100,39 @@ margin-bottom: 0; } +/* Local mode picker (Workspace / Worktree) below input */ +.chat-full-welcome-local-mode { + width: 100%; + max-width: 800px; + margin-top: 8px; + box-sizing: border-box; + display: none; + flex-direction: row; + align-items: center; + min-height: 28px; +} + +.chat-full-welcome.revealed .chat-full-welcome-local-mode { + display: flex; +} + +.sessions-chat-local-mode-left { + display: flex; + align-items: center; + min-width: 0; +} + +.sessions-chat-local-mode-spacer { + flex: 1; +} + +.sessions-chat-local-mode-right { + display: flex; + align-items: center; + gap: 2px; + min-width: 0; +} + /* Ensure the input editor fits properly */ .chat-full-welcome-inputSlot .interactive-input-part { margin: 0; @@ -128,7 +158,7 @@ background-color: var(--vscode-input-background) !important; } -/* Pickers row - flat horizontal bar below the input */ +/* Pickers row - two equal halves */ .chat-full-welcome-pickers { display: flex; flex-direction: row; @@ -136,48 +166,68 @@ align-items: center; width: 100%; box-sizing: border-box; - padding: 0 2px; + padding: 0; } .chat-full-welcome-pickers:empty { display: none; } -/* Left group (target dropdown + left-side extension pickers) */ -.sessions-chat-pickers-left { +/* Left half: target switcher, right-justified */ +.sessions-chat-pickers-left-half { + flex: 1; display: flex; + justify-content: flex-end; align-items: center; - gap: 2px; min-width: 0; } -/* Spacer between left and right groups */ -.sessions-chat-pickers-spacer { +/* Right half: pickers, left-justified */ +.sessions-chat-pickers-right-half { flex: 1; -} - -/* Right group (repo/folder pickers) */ -.sessions-chat-extension-pickers-right { display: flex; + justify-content: flex-start; align-items: center; - gap: 2px; min-width: 0; } -.sessions-chat-extension-pickers-right:empty { +/* Separator between switcher and folder picker */ +.sessions-chat-pickers-left-separator { + width: 1px; + height: 22px; + background-color: var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); + margin: 0 12px; + flex-shrink: 0; display: none; } -/* Left extension pickers container */ -.sessions-chat-extension-pickers-left { - display: flex; - align-items: center; - gap: 2px; - min-width: 0; +/* Target switcher radio buttons - bigger and fancier */ +.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button { + font-size: 13px; + line-height: 1.4em; + padding: 4px 14px; } -.sessions-chat-extension-pickers-left:empty { - display: none; +.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button:focus { + outline: none; +} + +/* Folder label next to the picker */ +.sessions-chat-folder-label { + font-size: 13px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + margin-right: 6px; } /* Target dropdown button */ diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index dbfcb58ff7d9e..1ada74835c253 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -8,6 +8,7 @@ import './media/chatWelcomePart.css'; import * as dom from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Separator, toAction } from '../../../../base/common/actions.js'; +import { Radio } from '../../../../base/browser/ui/radio/radio.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; @@ -25,16 +26,13 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; -import { asCSSUrl } from '../../../../base/browser/cssValue.js'; -import { FileAccess } from '../../../../base/common/network.js'; import { localize } from '../../../../nls.js'; -import { AgentSessionProviders, getAgentSessionProviderIcon } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ChatSessionPosition, getResourceForNewChatSession } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.js'; @@ -50,8 +48,8 @@ import { WorkspaceFolderCountContext } from '../../../../workbench/common/contex import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; import { IWorkspacesService, isRecentFolder } from '../../../../platform/workspaces/common/workspaces.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js'; import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js'; import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; @@ -138,6 +136,7 @@ export interface INewChatSendRequestData { readonly query: string; readonly sendOptions: IChatSendRequestOptions; readonly selectedOptions: ReadonlyMap; + readonly folderUri?: URI; } /** @@ -174,6 +173,11 @@ class NewChatWidget extends Disposable { private _extensionPickersLeftContainer: HTMLElement | undefined; private _extensionPickersRightContainer: HTMLElement | undefined; private _inputSlot: HTMLElement | undefined; + private _localModeContainer: HTMLElement | undefined; + private _localModeDropdownContainer: HTMLElement | undefined; + private _localModePickersContainer: HTMLElement | undefined; + private _localMode: 'workspace' | 'worktree' = 'worktree'; + private _selectedFolderUri: URI | undefined; private readonly _pickerWidgets = new Map(); private readonly _pickerWidgetDisposables = this._register(new DisposableStore()); private readonly _optionEmitters = new Map>(); @@ -190,23 +194,29 @@ class NewChatWidget extends Disposable { @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @IHoverService _hoverService: IHoverService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this._targetConfig = this._register(new TargetConfig(options.targetConfig)); this._options = options; + // Restore last picked folder + const lastFolder = this.storageService.get('agentSessions.lastPickedFolder', StorageScope.PROFILE); + if (lastFolder) { + try { this._selectedFolderUri = URI.parse(lastFolder); } catch { /* ignore */ } + } + // When target changes, regenerate pending resource this._register(this._targetConfig.onDidChangeSelectedTarget(() => { this._generatePendingSessionResource(); - this._updateTargetDropdown(); + this._notifyFolderSelection(); this._renderExtensionPickers(true); + this._renderLocalModePicker(); })); this._register(this._targetConfig.onDidChangeAllowedTargets(() => { @@ -219,6 +229,7 @@ class NewChatWidget extends Disposable { // Listen for option group changes to re-render pickers this._register(this.chatSessionsService.onDidChangeOptionGroups(() => { + this._notifyFolderSelection(); this._renderExtensionPickers(); })); @@ -247,11 +258,12 @@ class NewChatWidget extends Disposable { const wrapper = dom.append(container, dom.$('.sessions-chat-widget')); const welcomeElement = dom.append(wrapper, dom.$('.chat-full-welcome')); - // Mascot + // Watermark letterpress const header = dom.append(welcomeElement, dom.$('.chat-full-welcome-header')); - const quality = this.productService.quality ?? 'stable'; - const mascot = dom.append(header, dom.$('.chat-full-welcome-mascot')); - mascot.style.backgroundImage = asCSSUrl(FileAccess.asBrowserUri(`vs/sessions/contrib/chat/browser/media/code-icon-agent-sessions-${quality}.svg`)); + dom.append(header, dom.$('.chat-full-welcome-letterpress')); + + // Option group pickers (above the input) + this._pickersContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-pickers-container')); // Input slot this._inputSlot = dom.append(welcomeElement, dom.$('.chat-full-welcome-inputSlot')); @@ -262,8 +274,11 @@ class NewChatWidget extends Disposable { this._createToolbar(inputArea); this._inputSlot.appendChild(inputArea); - // Option group pickers (below the input) - this._pickersContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-pickers-container')); + // Local mode picker (below the input, shown when Local is selected) + this._localModeContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-local-mode')); + this._localModeDropdownContainer = dom.append(this._localModeContainer, dom.$('.sessions-chat-local-mode-left')); + dom.append(this._localModeContainer, dom.$('.sessions-chat-local-mode-spacer')); + this._localModePickersContainer = dom.append(this._localModeContainer, dom.$('.sessions-chat-local-mode-right')); // Render target buttons & extension pickers this._renderOptionGroupPickers(); @@ -274,12 +289,23 @@ class NewChatWidget extends Disposable { // Generate pending resource for option changes this._generatePendingSessionResource(); + // Render local mode picker + this._renderLocalModePicker(); + // Reveal welcomeElement.classList.add('revealed'); } - private _generatePendingSessionResource(): void { + private _getEffectiveTarget(): AgentSessionProviders | undefined { const target = this._targetConfig.selectedTarget.get(); + if (target === AgentSessionProviders.Local && this._localMode === 'worktree') { + return AgentSessionProviders.Background; + } + return target; + } + + private _generatePendingSessionResource(): void { + const target = this._getEffectiveTarget(); if (!target || target === AgentSessionProviders.Local) { this._pendingSessionResource = undefined; return; @@ -420,17 +446,15 @@ class NewChatWidget extends Disposable { const pickersRow = dom.append(this._pickersContainer, dom.$('.chat-full-welcome-pickers')); - // Left group: target dropdown (agent/worktree picker) - const leftGroup = dom.append(pickersRow, dom.$('.sessions-chat-pickers-left')); - this._targetDropdownContainer = dom.append(leftGroup, dom.$('.sessions-chat-dropdown-wrapper')); + // Left half: target switcher (right-justified within its half) + const leftHalf = dom.append(pickersRow, dom.$('.sessions-chat-pickers-left-half')); + this._targetDropdownContainer = dom.append(leftHalf, dom.$('.sessions-chat-dropdown-wrapper')); this._renderTargetDropdown(this._targetDropdownContainer); - // Spacer - dom.append(pickersRow, dom.$('.sessions-chat-pickers-spacer')); - - // Right group: all extension pickers (folder first, then others) - this._extensionPickersLeftContainer = undefined; - this._extensionPickersRightContainer = dom.append(pickersRow, dom.$('.sessions-chat-extension-pickers-right')); + // Right half: separator + pickers (left-justified within its half) + const rightHalf = dom.append(pickersRow, dom.$('.sessions-chat-pickers-right-half')); + this._extensionPickersLeftContainer = dom.append(rightHalf, dom.$('.sessions-chat-pickers-left-separator')); + this._extensionPickersRightContainer = dom.append(rightHalf, dom.$('.sessions-chat-extension-pickers-right')); this._renderExtensionPickers(); } @@ -441,44 +465,120 @@ class NewChatWidget extends Disposable { return; } - const activeType = this._targetConfig.selectedTarget.get() ?? AgentSessionProviders.Background; - const icon = getAgentSessionProviderIcon(activeType); - const name = getAgentSessionProviderName(activeType); + const activeType = this._targetConfig.selectedTarget.get() ?? AgentSessionProviders.Local; + const targets = [AgentSessionProviders.Local, AgentSessionProviders.Cloud].filter(t => allowed.has(t)); + const activeIndex = targets.indexOf(activeType); + + const radio = new Radio({ + items: targets.map(target => ({ + text: getAgentSessionProviderName(target), + isActive: target === activeType, + })), + }); + this._welcomeContentDisposables.add(radio); + container.appendChild(radio.domNode); + + if (activeIndex >= 0) { + radio.setActiveItem(activeIndex); + } + + this._welcomeContentDisposables.add(radio.onDidSelect(index => { + this._targetConfig.setSelectedTarget(targets[index]); + })); + } + + // --- Local mode picker (Workspace / Worktree) --- + + private readonly _localModeDisposables = this._register(new DisposableStore()); + + private _renderLocalModePicker(): void { + if (!this._localModeContainer || !this._localModeDropdownContainer || !this._localModePickersContainer) { + return; + } + + this._localModeDisposables.clear(); + dom.clearNode(this._localModeDropdownContainer); + dom.clearNode(this._localModePickersContainer); + + const selectedTarget = this._targetConfig.selectedTarget.get(); + if (selectedTarget !== AgentSessionProviders.Local) { + this._localModeContainer.style.visibility = 'hidden'; + return; + } + + this._localModeContainer.style.visibility = ''; - const button = dom.append(container, dom.$('.sessions-chat-dropdown-button')); + // Dropdown button for Workspace / Worktree + const modeLabel = this._localMode === 'workspace' + ? localize('localMode.workspace', "Workspace") + : localize('localMode.worktree', "Worktree"); + const modeIcon = this._localMode === 'workspace' ? Codicon.folder : Codicon.worktree; + + const button = dom.append(this._localModeDropdownContainer, dom.$('.sessions-chat-dropdown-button')); button.tabIndex = 0; button.role = 'button'; button.ariaHasPopup = 'true'; - dom.append(button, renderIcon(icon)); - dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, name)); + dom.append(button, renderIcon(modeIcon)); + dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, modeLabel)); dom.append(button, renderIcon(Codicon.chevronDown)); - this._welcomeContentDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, () => { - const currentAllowed = this._targetConfig.allowedTargets.get(); - const currentActive = this._targetConfig.selectedTarget.get(); - const actions = [...currentAllowed] - .map(sessionType => { - const label = getAgentSessionProviderName(sessionType); - return toAction({ - id: `target.${sessionType}`, - label, - checked: sessionType === currentActive, - run: () => this._targetConfig.setSelectedTarget(sessionType), - }); - }); + this._localModeDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, () => { + const actions = [ + toAction({ + id: 'localMode.workspace', + label: localize('localMode.workspace', "Workspace"), + checked: this._localMode === 'workspace', + run: () => this._setLocalMode('workspace'), + }), + toAction({ + id: 'localMode.worktree', + label: localize('localMode.worktree', "Worktree"), + checked: this._localMode === 'worktree', + run: () => this._setLocalMode('worktree'), + }), + ]; this.contextMenuService.showContextMenu({ getAnchor: () => button, getActions: () => actions, }); })); + + // Render pickers in the right side + this._renderLocalModePickers(); + } + + private _setLocalMode(mode: 'workspace' | 'worktree'): void { + if (this._localMode !== mode) { + this._localMode = mode; + this._generatePendingSessionResource(); + this._notifyFolderSelection(); + this._renderLocalModePicker(); + } } - private _updateTargetDropdown(): void { - if (!this._targetDropdownContainer) { + private _notifyFolderSelection(): void { + if (!this._pendingSessionResource) { return; } - dom.clearNode(this._targetDropdownContainer); - this._renderTargetDropdown(this._targetDropdownContainer); + const folderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + if (folderUri) { + this.chatSessionsService.notifySessionOptionsChange( + this._pendingSessionResource, + [{ optionId: 'repository', value: folderUri.fsPath }] + ).catch((err) => this.logService.error('Failed to notify extension of folder selection:', err)); + } + } + + private _renderLocalModePickers(): void { + if (!this._localModePickersContainer) { + return; + } + dom.clearNode(this._localModePickersContainer); + + if (this._localMode === 'worktree') { + // Worktree mode: render extension pickers for Background provider + this._renderExtensionPickersInContainer(this._localModePickersContainer, AgentSessionProviders.Background); + } } // --- Welcome: Extension option pickers --- @@ -488,16 +588,17 @@ class NewChatWidget extends Disposable { return; } - const activeSessionType = this._targetConfig.selectedTarget.get(); + const activeSessionType = this._getEffectiveTarget(); if (!activeSessionType) { this._clearExtensionPickers(); return; } - // For Local target, render a workspace folder picker instead of extension pickers - if (activeSessionType === AgentSessionProviders.Local) { + // For Local target, show folder picker in top row and handle bottom row + if (this._targetConfig.selectedTarget.get() === AgentSessionProviders.Local) { this._clearExtensionPickers(); - this._renderLocalFolderPicker(); + this._renderLocalFolderPickerInTopRow(); + this._renderLocalModePicker(); return; } @@ -552,6 +653,11 @@ class NewChatWidget extends Disposable { this._clearExtensionPickers(); + // Show the separator between target switcher and extension pickers + if (this._extensionPickersLeftContainer) { + this._extensionPickersLeftContainer.style.display = 'block'; + } + for (const optionGroup of visibleGroups) { const initialItem = this._getDefaultOptionForGroup(optionGroup); const initialState = { group: optionGroup, item: initialItem }; @@ -602,35 +708,43 @@ class NewChatWidget extends Disposable { } } - private _renderLocalFolderPicker(): void { + private _renderLocalFolderPickerInTopRow(): void { if (!this._extensionPickersRightContainer) { return; } - const folders = this.workspaceContextService.getWorkspace().folders; - const currentFolder = folders[0]; - const folderName = currentFolder ? basename(currentFolder.uri) : localize('noFolder', "No Folder"); + // Show the separator + if (this._extensionPickersLeftContainer) { + this._extensionPickersLeftContainer.style.display = 'block'; + } + + this._renderLocalFolderPickerInContainer(this._extensionPickersRightContainer, this._pickerWidgetDisposables); + } + + private _renderLocalFolderPickerInContainer(container: HTMLElement, disposables: DisposableStore): void { + const currentFolderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + const folderName = currentFolderUri ? basename(currentFolderUri) : localize('pickFolder', "Pick Folder"); - const slot = dom.append(this._extensionPickersRightContainer, dom.$('.sessions-chat-picker-slot')); + const slot = dom.append(container, dom.$('.sessions-chat-picker-slot')); const button = dom.append(slot, dom.$('.sessions-chat-dropdown-button')); button.tabIndex = 0; button.role = 'button'; button.ariaHasPopup = 'true'; - dom.append(button, renderIcon(Codicon.folder)); dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, folderName)); dom.append(button, renderIcon(Codicon.chevronDown)); const switchFolder = async (folderUri: URI) => { - const foldersToDelete = this.workspaceContextService.getWorkspace().folders.map(f => f.uri); - await this.workspaceEditingService.updateFolders(0, foldersToDelete.length, [{ uri: folderUri }]); + this._selectedFolderUri = folderUri; + this.storageService.store('agentSessions.lastPickedFolder', folderUri.toString(), StorageScope.PROFILE, StorageTarget.MACHINE); + this._notifyFolderSelection(); this._renderExtensionPickers(true); }; - this._pickerWidgetDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, async () => { + disposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, async () => { const recentlyOpened = await this.workspacesService.getRecentlyOpened(); const recentFolders = recentlyOpened.workspaces .filter(isRecentFolder) - .filter(r => !currentFolder || !isEqual(r.folderUri, currentFolder.uri)) + .filter(r => !currentFolderUri || !isEqual(r.folderUri, currentFolderUri)) .slice(0, 10); const actions = recentFolders.map(recent => toAction({ @@ -663,6 +777,73 @@ class NewChatWidget extends Disposable { })); } + private _renderExtensionPickersInContainer(container: HTMLElement, sessionType: AgentSessionProviders): void { + const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(sessionType); + if (!optionGroups || optionGroups.length === 0) { + return; + } + + const visibleGroups: IChatSessionProviderOptionGroup[] = []; + for (const group of optionGroups) { + if (isModelOptionGroup(group)) { + continue; + } + if (group.id === 'repository') { + continue; + } + const hasItems = group.items.length > 0 || (group.commands || []).length > 0 || !!group.searchable; + const passesWhenClause = this._evaluateOptionGroupVisibility(group); + if (hasItems && passesWhenClause) { + visibleGroups.push(group); + } + } + + for (const optionGroup of visibleGroups) { + const initialItem = this._getDefaultOptionForGroup(optionGroup); + const initialState = { group: optionGroup, item: initialItem }; + + if (initialItem) { + this._updateOptionContextKey(optionGroup.id, initialItem.id); + } + + const emitter = this._getOrCreateOptionEmitter(optionGroup.id); + const itemDelegate: IChatSessionPickerDelegate = { + getCurrentOption: () => this._selectedOptions.get(optionGroup.id) ?? this._getDefaultOptionForGroup(optionGroup), + onDidChangeOption: emitter.event, + setOption: (option: IChatSessionProviderOptionItem) => { + this._selectedOptions.set(optionGroup.id, option); + this._updateOptionContextKey(optionGroup.id, option.id); + emitter.fire(option); + + if (this._pendingSessionResource) { + this.chatSessionsService.notifySessionOptionsChange( + this._pendingSessionResource, + [{ optionId: optionGroup.id, value: option }] + ).catch((err) => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); + } + + this._renderLocalModePickers(); + }, + getOptionGroup: () => { + const groups = this.chatSessionsService.getOptionGroupsForSessionType(sessionType); + return groups?.find((g: { id: string }) => g.id === optionGroup.id); + }, + getSessionResource: () => this._pendingSessionResource, + }; + + const action = toAction({ id: optionGroup.id, label: optionGroup.name, run: () => { } }); + const widget = this.instantiationService.createInstance( + optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, + action, initialState, itemDelegate + ); + + this._localModeDisposables.add(widget); + + const slot = dom.append(container, dom.$('.sessions-chat-picker-slot')); + widget.render(slot); + } + } + private _evaluateOptionGroupVisibility(optionGroup: { id: string; when?: string }): boolean { if (!optionGroup.when) { return true; @@ -676,7 +857,7 @@ class NewChatWidget extends Disposable { } private _syncOptionsFromSession(sessionResource: URI): void { - const activeSessionType = this._targetConfig.selectedTarget.get(); + const activeSessionType = this._getEffectiveTarget(); if (!activeSessionType) { return; } @@ -738,7 +919,7 @@ class NewChatWidget extends Disposable { this._pickerWidgets.clear(); this._optionEmitters.clear(); if (this._extensionPickersLeftContainer) { - dom.clearNode(this._extensionPickersLeftContainer); + this._extensionPickersLeftContainer.style.display = 'none'; } if (this._extensionPickersRightContainer) { dom.clearNode(this._extensionPickersRightContainer); @@ -753,7 +934,7 @@ class NewChatWidget extends Disposable { return; } - const target = this._targetConfig.selectedTarget.get(); + const target = this._getEffectiveTarget(); if (!target) { this.logService.warn('ChatWelcomeWidget: No target selected, cannot create session'); return; @@ -780,12 +961,15 @@ class NewChatWidget extends Disposable { agentIdSilent: contribution?.type, }; + const folderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + this._options.onSendRequest?.({ resource, target, query, sendOptions, selectedOptions: new Map(this._selectedOptions), + folderUri, }); } @@ -847,11 +1031,11 @@ export class NewChatViewPane extends ViewPane { { targetConfig: { allowedTargets: this.computeAllowedTargets(), - defaultTarget: AgentSessionProviders.Background, + defaultTarget: AgentSessionProviders.Local, }, onSendRequest: (data) => { - this.activeSessionService.openSessionAndSend( - data.resource, data.query, data.sendOptions, data.selectedOptions + this.activeSessionService.sendRequestForNewSession( + data.resource, data.query, data.sendOptions, data.selectedOptions, data.folderUri ).catch(e => this.logService.error('NewChatViewPane: Failed to open session and send request', e)); }, } satisfies INewChatWidgetOptions, @@ -866,11 +1050,7 @@ export class NewChatViewPane extends ViewPane { } private computeAllowedTargets(): AgentSessionProviders[] { - const targets: AgentSessionProviders[] = []; - if (this.workspaceContextService.getWorkspace().folders.length === 1) { - targets.push(AgentSessionProviders.Local); - } - targets.push(AgentSessionProviders.Background, AgentSessionProviders.Cloud); + const targets: AgentSessionProviders[] = [AgentSessionProviders.Local, AgentSessionProviders.Cloud]; return targets; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 8686c11d381b2..5ebd3496cb380 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -12,12 +12,17 @@ import { IContextKey, IContextKeyService, RawContextKey } from '../../../../plat import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ISessionOpenOptions, openSession as openSessionDefault } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.js'; -import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; +import { ChatViewId, ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; +import { ChatViewPane } from '../../../../workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.js'; import { IChatSessionItem, IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/constants.js'; import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { LocalChatSessionUri } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; +import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -62,17 +67,17 @@ export interface ISessionsManagementService { */ openSession(sessionResource: URI, openOptions?: ISessionOpenOptions): Promise; - /** - * Open a new session, apply options, and send the initial request. - * This is the main entry point for the new-chat welcome widget. - */ - openSessionAndSend(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap): Promise; - /** * Switch to the new-session view. * No-op if the current session is already a new session. */ openNewSession(): void; + + /** + * Open a new session, apply options, and send the initial request. + * This is the main entry point for the new-chat welcome widget. + */ + sendRequestForNewSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap, folderUri?: URI): Promise; } export const ISessionsManagementService = createDecorator('sessionsManagementService'); @@ -96,6 +101,9 @@ export class SessionsManagementService extends Disposable implements ISessionsMa @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, + @IViewsService private readonly viewsService: IViewsService, ) { super(); @@ -107,9 +115,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.lastSelectedSession = this.loadLastSelectedSession(); // Save on shutdown - this._register(this.storageService.onWillSaveState(() => { - this.saveLastSelectedSession(); - })); + this._register(this.storageService.onWillSaveState(() => this.saveLastSelectedSession())); // Update active session when session options change this._register(this.chatSessionsService.onDidChangeSessionOptions(sessionResource => { @@ -124,9 +130,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa })); // Update active session when the agent sessions model changes (e.g., metadata updates with worktree/repository info) - this._register(this.agentSessionsService.model.onDidChangeSessions(() => { - this.refreshActiveSessionFromModel(); - })); + this._register(this.agentSessionsService.model.onDidChangeSessions(() => this.refreshActiveSessionFromModel())); } private refreshActiveSessionFromModel(): void { @@ -208,44 +212,106 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } async openSession(sessionResource: URI, openOptions?: ISessionOpenOptions): Promise { - const session = this.agentSessionsService.model.getSession(sessionResource); - if (session) { - this.isNewChatSessionContext.set(false); - this.setActiveSession(session); - await this.instantiationService.invokeFunction(openSessionDefault, session, openOptions); + this.isNewChatSessionContext.set(false); + + const existingSession = this.agentSessionsService.model.getSession(sessionResource); + if (existingSession) { + await this.openExistingSession(existingSession, openOptions); + } else if (LocalChatSessionUri.isLocalSession(sessionResource)) { + await this.openLocalSession(); } else { - // For new sessions, load via the chat service first so the model - // is ready before the ChatViewPane renders it. - const modelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); - // Switch view only after the model is loaded so the ChatViewPane - // has content immediately when it becomes visible. - this.isNewChatSessionContext.set(false); - const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); - if (!chatWidget?.viewModel) { - this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`); - modelRef?.dispose(); - return; + await this.openNewRemoteSession(sessionResource); + } + } + + /** + * Open an existing agent session - set it as active and reveal it. + */ + private async openExistingSession(session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { + this.setActiveSession(session); + await this.instantiationService.invokeFunction(openSessionDefault, session, openOptions); + } + + /** + * Open a fresh local chat session - show the ChatViewPane and clear the widget. + */ + private async openLocalSession(): Promise { + const view = await this.viewsService.openView(ChatViewId) as ChatViewPane | undefined; + if (view) { + await view.widget.clear(); + if (view.widget.viewModel) { + const folder = this.workspaceContextService.getWorkspace().folders[0]; + const activeSessionItem: IActiveSessionItem = { + resource: view.widget.viewModel.sessionResource, + label: view.widget.viewModel.model.title || '', + timing: view.widget.viewModel.model.timing, + repository: folder?.uri, + worktree: undefined + }; + this._activeSession.set(activeSessionItem, undefined); } - const repository = this.getRepositoryFromSessionOption(sessionResource); - const activeSessionItem: IActiveSessionItem = { - resource: sessionResource, - label: chatWidget.viewModel.model.title || '', - timing: chatWidget.viewModel.model.timing, - repository, - worktree: undefined - }; - this.logService.info(`[ActiveSessionService] Active session changed (new): ${sessionResource.toString()}, repository: ${repository?.toString() ?? 'none'}`); - this._activeSession.set(activeSessionItem, undefined); } } - async openSessionAndSend(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap): Promise { - // 1. Open the session in ChatViewPane - this transitions views, - // loads the model, and connects it to the ChatWidget so - // tool invocations work. + /** + * Open a new remote session - load the model first, then show it in the ChatViewPane. + */ + private async openNewRemoteSession(sessionResource: URI): Promise { + const modelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); + if (!chatWidget?.viewModel) { + this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`); + modelRef?.dispose(); + return; + } + const repository = this.getRepositoryFromSessionOption(sessionResource); + const activeSessionItem: IActiveSessionItem = { + resource: sessionResource, + label: chatWidget.viewModel.model.title || '', + timing: chatWidget.viewModel.model.timing, + repository, + worktree: undefined + }; + this.logService.info(`[ActiveSessionService] Active session changed (new): ${sessionResource.toString()}, repository: ${repository?.toString() ?? 'none'}`); + this._activeSession.set(activeSessionItem, undefined); + } + + async sendRequestForNewSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap, folderUri?: URI): Promise { + if (LocalChatSessionUri.isLocalSession(sessionResource)) { + await this.sendLocalSession(sessionResource, query, folderUri); + } else { + await this.sendCustomSession(sessionResource, query, sendOptions, selectedOptions); + } + } + + /** + * Local sessions run directly through the ChatWidget. + * Set the workspace folder, open a fresh chat view, and submit via acceptInput. + */ + private async sendLocalSession(sessionResource: URI, query: string, folderUri?: URI): Promise { + if (folderUri) { + await this.workspaceEditingService.updateFolders(0, this.workspaceContextService.getWorkspace().folders.length, [{ uri: folderUri }]); + } + + await this.openSession(sessionResource); + + const widget = this.chatWidgetService.lastFocusedWidget; + if (widget) { + widget.setInput(query); + widget.acceptInput(query); + } + } + + /** + * Custom sessions (worktree, cloud, etc.) go through the chat service. + * Apply selected options, send the request, then wait for the extension + * to create an agent session so it appears in the sidebar. + */ + private async sendCustomSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap): Promise { + // 1. Open the session - loads the model and shows the ChatViewPane await this.openSession(sessionResource); - // 2. Apply selected options to the contributed session + // 2. Apply selected options (repository, branch, etc.) to the contributed session if (selectedOptions && selectedOptions.size > 0) { const modelRef = this.chatService.getActiveSessionReference(sessionResource); if (modelRef) { @@ -264,22 +330,17 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } - // 3. Snapshot existing session resources so we can detect the new one + // 3. Send the request const existingResources = new Set( this.agentSessionsService.model.sessions.map(s => s.resource.toString()) ); - - // 4. Send the request through the chat service - the model is now - // connected to the ChatWidget, so tools and rendering work. const result = await this.chatService.sendRequest(sessionResource, query, sendOptions); if (result.kind === 'rejected') { this.logService.error(`[ActiveSessionService] sendRequest rejected: ${result.reason}`); return; } - // 5. After send, the extension creates an agent session. Wait for it - // and set it as the active session so the titlebar and sidebar - // reflect the new session. + // 4. Wait for the extension to create an agent session, then set it as active let newSession = this.agentSessionsService.model.sessions.find( s => !existingResources.has(s.resource.toString()) ); From 490292e2482967a71c34df6e06c3c49ff0eb95e2 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 18 Feb 2026 15:47:17 +0100 Subject: [PATCH 30/36] adding inline completions in the chat input (#295605) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 4e246bc55abfe..cfe1d784a8ba5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -126,6 +126,7 @@ import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js' import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; import { Target } from '../../../common/promptSyntax/service/promptsService.js'; +import { InlineCompletionsController } from '../../../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; const $ = dom.$; @@ -2073,7 +2074,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditorElement = dom.append(editorContainer, $(chatInputEditorContainerSelector)); const editorOptions = getSimpleCodeEditorWidgetOptions(); - editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, DropIntoEditorController.ID, CopyPasteController.ID, LinkDetector.ID])); + editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, DropIntoEditorController.ID, CopyPasteController.ID, LinkDetector.ID, InlineCompletionsController.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); SuggestController.get(this._inputEditor)?.forceRenderingAbove(); @@ -2287,7 +2288,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { - inputModel = this.modelService.createModel('', null, this.inputUri, true); + inputModel = this.modelService.createModel('', null, this.inputUri, false); } this.textModelResolverService.createModelReference(this.inputUri).then(ref => { From eb1ab474363dc8911315896f1ea4587eac5a34c1 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 18 Feb 2026 15:58:01 +0100 Subject: [PATCH 31/36] feat: update inline chat affordance with new edit command and toolbar styling --- .../browser/inlineChat.contribution.ts | 4 ++-- .../browser/inlineChatOverlayWidget.ts | 18 ++++++++++++------ .../browser/media/inlineChatOverlayWidget.css | 5 +++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 1fb41b8d8fd89..ce4653f5cfbea 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -93,8 +93,8 @@ MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { order: 1, command: { id: ACTION_START, - title: localize('editCode', "Edit Code..."), - shortTitle: localize('editCodeShort', "Edit Code"), + title: localize('editCode', "Ask for Edits"), + shortTitle: localize('editCodeShort', "Ask for Edits"), icon: Codicon.sparkle, }, when: EditorContextKeys.hasNonEmptySelection, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 4c2c7a41d6919..17dd508687a59 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -56,7 +56,7 @@ export class InlineChatInputWidget extends Disposable { private readonly _showStore = this._store.add(new DisposableStore()); private readonly _stickyScrollHeight: IObservable; - private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number }>; + private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; private _anchorLineNumber: number = 0; private _anchorLeft: number = 0; private _anchorAbove: boolean = false; @@ -111,7 +111,7 @@ export class InlineChatInputWidget extends Disposable { options.lineNumbersMinChars = 0; options.folding = false; options.minimap = { enabled: false }; - options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; + options.scrollbar = { vertical: 'hidden', horizontal: 'hidden', alwaysConsumeMouseWheel: true }; options.renderLineHighlight = 'none'; options.fontFamily = DEFAULT_FONT_FAMILY; options.fontSize = 13; @@ -167,8 +167,8 @@ export class InlineChatInputWidget extends Disposable { const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); this._layoutData = derived(r => { - - const totalWidth = contentWidth.read(r) + 6 + toolbarWidth.read(r); + const editorPad = 6; + const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); const minWidth = minWidgetWidth.read(r); const maxWidth = maxWidgetWidth.read(r); const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' @@ -184,6 +184,7 @@ export class InlineChatInputWidget extends Disposable { } return { + editorPad, toolbarWidth: toolbarWidth.read(r), totalWidth: clampedWidth, height: clampedHeight @@ -192,9 +193,9 @@ export class InlineChatInputWidget extends Disposable { // Update container width and editor layout when width changes this._store.add(autorun(r => { - const { toolbarWidth, totalWidth, height } = this._layoutData.read(r); + const { editorPad, toolbarWidth, totalWidth, height } = this._layoutData.read(r); - const inputWidth = totalWidth - toolbarWidth - 6; + const inputWidth = totalWidth - toolbarWidth - editorPad; this._container.style.width = `${totalWidth}px`; this._inputContainer.style.width = `${inputWidth}px`; this._input.layout({ width: inputWidth, height }); @@ -204,6 +205,11 @@ export class InlineChatInputWidget extends Disposable { this._store.add(this._input.onDidFocusEditorText(() => this._container.classList.add('focused'))); this._store.add(this._input.onDidBlurEditorText(() => this._container.classList.remove('focused'))); + // Toggle scroll decoration on the toolbar + this._store.add(this._input.onDidScrollChange(e => { + this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); + })); + // Update placeholder based on selection state this._store.add(autorun(r => { const selection = this._editorObs.cursorSelection.read(r); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index a44cc4bb7fb00..294867b3056ea 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -80,9 +80,14 @@ .inline-chat-gutter-menu .inline-chat-gutter-container > .toolbar { display: flex; align-items: center; + align-self: stretch; padding: 0 4px; } +.inline-chat-gutter-menu .inline-chat-gutter-container > .toolbar.fake-scroll-decoration { + box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px inset; +} + .inline-chat-gutter-menu .inline-chat-gutter-container > .toolbar .monaco-action-bar .actions-container { gap: 2px; } From 16600d2086985612255438c9fa0756129b298361 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 18 Feb 2026 16:01:43 +0100 Subject: [PATCH 32/36] feat: hide inline chat affordance when the editor context menu is shown --- .../contrib/inlineChat/browser/inlineChatAffordance.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index c221808470c29..ea0933b7e448c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -109,6 +109,11 @@ export class InlineChatAffordance extends Disposable { } })); + // Hide when the editor context menu shows + this._store.add(this.#editor.onContextMenu(() => { + selectionData.set(undefined, undefined); + })); + const gutterAffordance = this._store.add(this.#instantiationService.createInstance( InlineChatGutterAffordance, editorObs, From 7ae95509fff1b1a5324fdcd3bf239eb9a1c779fd Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:11:05 +0100 Subject: [PATCH 33/36] Sessions - fix dropdown spacing (#296018) --- .../sessions/contrib/chat/browser/media/chatWelcomePart.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css index 5d9ebc89a95ab..75d90b5f97d74 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css @@ -235,7 +235,7 @@ display: flex; align-items: center; height: 16px; - padding: 3px 0 3px 6px; + padding: 3px 3px 3px 6px; cursor: pointer; font-size: 13px; color: var(--vscode-descriptionForeground); @@ -281,7 +281,7 @@ display: flex; align-items: center; height: 16px; - padding: 3px 0 3px 6px; + padding: 3px 3px 3px 6px; background-color: transparent; border: none; color: var(--vscode-descriptionForeground); From 8b04bf2b4e93d4dc9b1d3a92159776208d8d9912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 18 Feb 2026 16:42:50 +0100 Subject: [PATCH 34/36] bump distro (#296023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Moreno <22350+joaomoreno@users.noreply.github.com> --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b29648bcd2c7a..8735f91d979c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "3dc22e559f766d4c609a675d88819589e9876d9c", + "distro": "85914d5a600261a53306190177be48aa8f0cdfb4", "author": { "name": "Microsoft Corporation" }, @@ -244,4 +244,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file From ac44844ca4e1eb523fdf46290c918e5f17e5ffbb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Feb 2026 16:50:27 +0100 Subject: [PATCH 35/36] sessions - clear focus when creating new session (#296009) --- src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index a243503c2f5c7..ab3ba0b5a0809 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -194,6 +194,8 @@ export class AgenticSessionsViewPane extends ViewPane { if (!sessionsControl.reveal(activeSession.resource)) { sessionsControl.clearFocus(); } + } else { + sessionsControl.clearFocus(); // clear selection when a new session is created } })); From a4228a77315913d1dae69a680bee584109d2f89d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 18 Feb 2026 17:20:33 +0100 Subject: [PATCH 36/36] use `sessionResource` for edit source creation (#296032) re https://github.com/microsoft/vscode/issues/274403 --- .../browser/chatEditing/chatEditingTextModelChangeService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts index acbbd2c650f5e..07b5ef8421f84 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts @@ -34,6 +34,7 @@ import { IChatResponseModel } from '../../common/model/chatModel.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js'; import { pendingRewriteMinimap } from './chatEditingModifiedFileEntry.js'; +import { chatSessionResourceToId } from '../../common/model/chatUri.js'; type affectedLines = { linesAdded: number; linesRemoved: number; lineCount: number; hasRemainingEdits: boolean }; type acceptedOrRejectedLines = affectedLines & { state: 'accepted' | 'rejected' }; @@ -260,7 +261,7 @@ export class ChatEditingTextModelChangeService extends Disposable { return EditSources.unknown({ name: 'editSessionUndoRedo' }); } - const sessionId = responseModel.session.sessionId; + const sessionId = chatSessionResourceToId(responseModel.session.sessionResource); const request = responseModel.session.getRequests().at(-1); const languageId = this.modifiedModel.getLanguageId(); const agent = responseModel.agent;