From 6d53d3e19212860b894928edb532a07dd2ef3b6c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 10:02:48 +0000 Subject: [PATCH 01/14] Update toolbar hover background colors in 2026 dark and light themes --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index ce19af608424c..cd8cce33b4dfd 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -60,7 +60,7 @@ "list.hoverBackground": "#262728", "list.hoverForeground": "#bfbfbf", "list.dropBackground": "#3994BC1A", - "toolbar.hoverBackground": "#262728", + "toolbar.hoverBackground": "#FFFFFF18", "list.focusBackground": "#3994BC26", "list.focusForeground": "#bfbfbf", "list.focusOutline": "#3994BCB3", @@ -192,7 +192,7 @@ "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorderTop": "#3994BC", - "tab.hoverBackground": "#262728", + "tab.hoverBackground": "#121314", "tab.hoverForeground": "#bfbfbf", "tab.unfocusedActiveBackground": "#121314", "tab.unfocusedActiveForeground": "#8C8C8C", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index a03d296a0c65f..eb22e1c1c49d6 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -188,7 +188,7 @@ "statusBarItem.prominentBackground": "#0069CCDD", "statusBarItem.prominentForeground": "#FFFFFF", "statusBarItem.prominentHoverBackground": "#0069CC", - "toolbar.hoverBackground": "#DADADA4f", + "toolbar.hoverBackground": "#00000010", "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", "tab.inactiveBackground": "#FAFAFD", @@ -196,7 +196,7 @@ "tab.border": "#F0F1F2FF", "tab.lastPinnedBorder": "#F0F1F2FF", "tab.activeBorderTop": "#000000", - "tab.hoverBackground": "#DADADA4f", + "tab.hoverBackground": "#FFFFFF", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", "tab.unfocusedActiveForeground": "#606060", From 80c418069fe1996cb5cadfa8285df54bfc4163bd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 11:27:46 +0100 Subject: [PATCH 02/14] hide inline chat affordance when editor loses focus (#299716) Fixes #299616 --- .../contrib/inlineChat/browser/inlineChatAffordance.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 961c1943e746a..a5f6a2ad8036c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -120,6 +120,13 @@ export class InlineChatAffordance extends Disposable { selectionData.set(undefined, undefined); })); + // Hide when the editor loses focus (e.g., switching tabs in notebooks) + this._store.add(autorun(r => { + if (!editorObs.isFocused.read(r)) { + selectionData.set(undefined, undefined); + } + })); + this._store.add(autorun(r => { const sel = selectionData.read(r); const mode = affordance.read(r); From c3c31a2ba5615dd48ba21e9cb10e71b17510d104 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 6 Mar 2026 11:30:50 +0100 Subject: [PATCH 03/14] fix claude pickers when in overflow menu --- .../chatSessions/chatSessionPickerActionItem.ts | 15 +++++++++++++++ .../searchableOptionPickerActionItem.ts | 4 +++- .../chat/browser/widget/input/chatInputPart.ts | 10 ++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts index a21e2483b8d28..241eeb90adfcb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts @@ -7,6 +7,7 @@ import './media/chatSessionPickerActionItem.css'; import { IAction } from '../../../../../base/common/actions.js'; import { Event } from '../../../../../base/common/event.js'; import * as dom from '../../../../../base/browser/dom.js'; +import { getActiveWindow } from '../../../../../base/browser/dom.js'; import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction, IActionWidgetDropdownOptions } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -19,6 +20,7 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { renderLabelWithIcons, renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../../nls.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IChatInputPickerOptions } from '../widget/input/chatInputPickerActionItem.js'; export interface IChatSessionPickerDelegate { @@ -41,6 +43,7 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI action: IAction, initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined }, protected readonly delegate: IChatSessionPickerDelegate, + protected readonly _pickerOptions: IChatInputPickerOptions | undefined, @IActionWidgetService actionWidgetService: IActionWidgetService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -61,6 +64,7 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI }, actionBarActionProvider: undefined, reporter: { id: group.id, name: `ChatSession:${group.name}`, includeOptions: false }, + getAnchor: () => this._getAnchorElement(), }; super(actionWithLabel, sessionPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); @@ -153,6 +157,17 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI }; } + /** + * Returns the anchor element for the dropdown. + * Falls back to the overflow anchor if this element is not in the DOM. + */ + private _getAnchorElement(): HTMLElement { + if (this.element && getActiveWindow().document.contains(this.element)) { + return this.element; + } + return this._pickerOptions?.getOverflowAnchor?.() ?? this.element!; + } + protected override renderLabel(element: HTMLElement): IDisposable | null { const domChildren = []; element.classList.add('chat-session-option-picker'); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts index 9d22269371fe2..fd19654bcefdf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts @@ -22,6 +22,7 @@ import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from './chatS import { ILogService } from '../../../../../platform/log/common/log.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IChatInputPickerOptions } from '../widget/input/chatInputPickerActionItem.js'; interface ISearchableOptionQuickPickItem extends IQuickPickItem { readonly optionItem: IChatSessionProviderOptionItem; @@ -43,6 +44,7 @@ export class SearchableOptionPickerActionItem extends ChatSessionPickerActionIte action: IAction, initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined }, delegate: IChatSessionPickerDelegate, + pickerOptions: IChatInputPickerOptions | undefined, @IActionWidgetService actionWidgetService: IActionWidgetService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -51,7 +53,7 @@ export class SearchableOptionPickerActionItem extends ChatSessionPickerActionIte @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(action, initialState, delegate, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService); + super(action, initialState, delegate, pickerOptions, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService); } protected override getDropdownActions(): IActionWidgetDropdownAction[] { 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 4f54ec7cfe316..e54e738615972 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -381,6 +381,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatSessionPickerWidgets: Map = new Map(); private chatSessionPickerContainer: HTMLElement | undefined; private _lastSessionPickerAction: MenuItemAction | undefined; + private _lastSessionPickerOptions: IChatInputPickerOptions | undefined; private readonly _waitForPersistedLanguageModel: MutableDisposable = this._register(new MutableDisposable()); private readonly _chatSessionOptionEmitters: Map> = new Map(); @@ -838,8 +839,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Create picker widgets for all option groups available for the current session type. */ - private createChatSessionPickerWidgets(action: MenuItemAction): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { + private createChatSessionPickerWidgets(action: MenuItemAction, pickerOptions?: IChatInputPickerOptions): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { this._lastSessionPickerAction = action; + this._lastSessionPickerOptions = pickerOptions; const result = this.computeVisibleOptionGroups(); if (!result) { @@ -890,7 +892,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } }; - const widget = this.instantiationService.createInstance(optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, action, initialState, itemDelegate); + const widget = this.instantiationService.createInstance(optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, action, initialState, itemDelegate, pickerOptions); this.chatSessionPickerWidgets.set(optionGroup.id, widget); widgets.push(widget); } @@ -1720,7 +1722,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge !Array.from(visibleGroupIds).every(id => currentWidgetGroupIds.has(id)); if (needsRecreation && this._lastSessionPickerAction && this.chatSessionPickerContainer) { - const widgets = this.createChatSessionPickerWidgets(this._lastSessionPickerAction); + const widgets = this.createChatSessionPickerWidgets(this._lastSessionPickerAction, this._lastSessionPickerOptions); dom.clearNode(this.chatSessionPickerContainer); for (const widget of widgets) { const container = dom.$('.action-item.chat-sessionPicker-item'); @@ -2269,7 +2271,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { // Create all pickers and return a container action view item - const widgets = this.createChatSessionPickerWidgets(action); + const widgets = this.createChatSessionPickerWidgets(action, pickerOptions); if (widgets.length === 0) { return new HiddenActionViewItem(action); } From 176d771e8e0fbda8ad5ec4b717f4dc744796657b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 11:34:44 +0100 Subject: [PATCH 04/14] run oss-tool, update distro (#299717) --- ThirdPartyNotices.txt | 2 +- cglicenses.json | 9 -- cli/ThirdPartyNotices.txt | 170 ++------------------------------------ package.json | 2 +- 4 files changed, 8 insertions(+), 175 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 896b59001d616..0a15b3ff5fc30 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -684,7 +684,7 @@ more details. --------------------------------------------------------- -go-syntax 0.8.5 - MIT +go-syntax 0.8.6 - MIT https://github.com/worlpaker/go-syntax MIT License diff --git a/cglicenses.json b/cglicenses.json index 37bba3145ba11..48d2c3b093c9c 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -306,11 +306,6 @@ "name": "russh-keys", "fullLicenseTextUri": "https://raw.githubusercontent.com/warp-tech/russh/1da80d0d599b6ee2d257c544c0d6af4f649c9029/LICENSE-2.0.txt" }, - { - // Reason: license is in a subdirectory in repo - "name": "dirs-next", - "fullLicenseTextUri": "https://raw.githubusercontent.com/xdg-rs/dirs/af4aa39daba0ac68e222962a5aca17360158b7cc/dirs/LICENSE-MIT" - }, { // Reason: license is in a subdirectory in repo "name": "openssl", @@ -361,10 +356,6 @@ "name": "toml_datetime", "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml_datetime/LICENSE-MIT" }, - { // License is MIT/Apache and tool doesn't look in subfolders - "name": "dirs-sys-next", - "fullLicenseTextUri": "https://raw.githubusercontent.com/xdg-rs/dirs/master/dirs-sys/LICENSE-MIT" - }, { // License is MIT/Apache and gitlab API doesn't find the project "name": "libredox", "fullLicenseTextUri": "https://gitlab.redox-os.org/redox-os/libredox/-/raw/master/LICENSE" diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 9edb0ae9d2330..6e21ddb37297a 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -1666,7 +1666,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (crates) [`aead`]: ./aead -[`async‑signature`]: ./signature/async [`cipher`]: ./cipher [`crypto‑common`]: ./crypto-common [`crypto`]: ./crypto @@ -1828,7 +1827,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (crates) [`aead`]: ./aead -[`async‑signature`]: ./signature/async [`cipher`]: ./cipher [`crypto‑common`]: ./crypto-common [`crypto`]: ./crypto @@ -13671,33 +13669,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a zbus 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -13705,33 +13677,7 @@ DEALINGS IN THE SOFTWARE. zbus_macros 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -13739,33 +13685,7 @@ DEALINGS IN THE SOFTWARE. zbus_names 2.6.1 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -14211,33 +14131,7 @@ DEALINGS IN THE SOFTWARE. zvariant 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -14245,33 +14139,7 @@ DEALINGS IN THE SOFTWARE. zvariant_derive 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -14279,31 +14147,5 @@ DEALINGS IN THE SOFTWARE. zvariant_utils 1.0.1 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- \ No newline at end of file diff --git a/package.json b/package.json index 680a4bcd8cf4e..d9603fc572835 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.111.0", - "distro": "e802965a9da346fb619bb708f64e54e927167133", + "distro": "cd72f8f27b485d65c99f5020caa895a5ac5692eb", "author": { "name": "Microsoft Corporation" }, From 78e13de8de0a5b7be13b346aaf457f373e8ac221 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 10:42:45 +0000 Subject: [PATCH 05/14] Add minimap slider colors to 2026 dark and light themes --- extensions/theme-2026/themes/2026-dark.json | 5 ++++- extensions/theme-2026/themes/2026-light.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index cd8cce33b4dfd..8eb011c24434c 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -268,7 +268,10 @@ "charts.orange": "#CD861A", "charts.green": "#86CF86", "charts.purple": "#AD80D7", - "inlineChat.border": "#00000000" + "inlineChat.border": "#00000000", + "minimapSlider.background": "#83848533", + "minimapSlider.hoverBackground": "#83848566", + "minimapSlider.activeBackground": "#83848599", }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index eb22e1c1c49d6..a0f14f5e5d34a 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -272,7 +272,10 @@ "charts.green": "#388A34", "charts.purple": "#652D90", "agentStatusIndicator.background": "#FFFFFF", - "inlineChat.border": "#00000000" + "inlineChat.border": "#00000000", + "minimapSlider.background": "#99999926", + "minimapSlider.hoverBackground": "#99999940", + "minimapSlider.activeBackground": "#99999955", }, "tokenColors": [ { From 5b38f1d5296b42ae51049d452362b673ff714dbe Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 6 Mar 2026 21:48:45 +1100 Subject: [PATCH 06/14] Update default model selection to prioritize 'copilot' vendor in ExtHostLanguageModels (#298903) * Update default model selection to prioritize 'copilot' vendor in ExtHostLanguageModels * Fix tests * Fix tests --- extensions/vscode-api-tests/package.json | 4 ++++ .../vscode-api-tests/src/singlefolder-tests/chat.test.ts | 2 +- src/vs/workbench/api/common/extHostLanguageModels.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e5c6ce5f767a9..e516742990204 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -71,6 +71,10 @@ { "vendor": "test-lm-vendor", "displayName": "Test LM Vendor" + }, + { + "vendor": "copilot", + "displayName": "Test Copilot LM Vendor" } ], "chatParticipants": [ diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index ff5b49d9b69a1..6ed6c91171878 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -17,7 +17,7 @@ suite('chat', () => { disposables = []; // Register a dummy default model which is required for a participant request to go through - disposables.push(lm.registerLanguageModelChatProvider('test-lm-vendor', { + disposables.push(lm.registerLanguageModelChatProvider('copilot', { async provideLanguageModelChatInformation(_options, _token) { return [{ id: 'test-lm', diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index b6bcbfdbb52fe..cc76961ab1583 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -364,7 +364,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } for (const [modelIdentifier, modelData] of this._localModels) { - if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat] && !modelData.metadata.targetChatSessionType) { + if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat] && modelData.metadata.vendor === 'copilot') { defaultModelId = modelIdentifier; break; } From 13c7b019e2349828e8c90439d2187653dc0a141e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 10:57:11 +0000 Subject: [PATCH 07/14] Update widget and menu border colors in 2026 light theme Co-authored-by: Copilot --- extensions/theme-2026/themes/2026-light.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index a0f14f5e5d34a..f963724d6b52d 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -50,7 +50,7 @@ "inputValidation.errorForeground": "#202020", "scrollbar.shadow": "#00000000", "widget.shadow": "#00000000", - "widget.border": "#EEEEF1", + "widget.border": "#E2E2E5", "editorStickyScroll.shadow": "#00000000", "editorStickyScrollHover.background": "#F0F0F3", "editorStickyScroll.border": "#F0F1F2FF", @@ -105,7 +105,7 @@ "menu.selectionBackground": "#0069CC1A", "menu.selectionForeground": "#202020", "menu.separatorBackground": "#EEEEF1", - "menu.border": "#F0F1F2FF", + "menu.border": "#E4E5E6FF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", "commandCenter.background": "#FAFAFD", @@ -136,15 +136,15 @@ "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F0F1F2FF", "editorWidget.background": "#FAFAFD", - "editorWidget.border": "#F0F1F2FF", + "editorWidget.border": "#E4E5E6FF", "editorWidget.foreground": "#202020", "editorSuggestWidget.background": "#FAFAFD", - "editorSuggestWidget.border": "#F0F1F2FF", + "editorSuggestWidget.border": "#E4E5E6FF", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", "editorHoverWidget.background": "#FAFAFD", - "editorHoverWidget.border": "#F0F1F2FF", + "editorHoverWidget.border": "#E4E5E6FF", "peekView.border": "#0069CC", "peekViewEditor.background": "#FAFAFD", "peekViewEditor.matchHighlightBackground": "#0069CC33", From a9a9436b2bca278d0c0c1912abe2f3a5e06085a6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 12:26:44 +0100 Subject: [PATCH 08/14] Bump version to 1.112.0 in package.json and package-lock.json (#299736) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eadb4002ba5f7..5fc1351c51f28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-oss-dev", - "version": "1.111.0", + "version": "1.112.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.111.0", + "version": "1.112.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d9603fc572835..3f5bcc61b9ce3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "1.111.0", + "version": "1.112.0", "distro": "cd72f8f27b485d65c99f5020caa895a5ac5692eb", "author": { "name": "Microsoft Corporation" From ae0eadc1ff3dd969913cb054e8c70fcd74625279 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 11:33:22 +0000 Subject: [PATCH 09/14] Update tab borders in 2026 dark and light themes --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8eb011c24434c..d204a5506b093 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -199,8 +199,8 @@ "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", "editorGroupHeader.tabsBackground": "#191A1B", - "tab.activeBorder": "#00000000", - "editorGroupHeader.tabsBorder": "#00000000", + "tab.activeBorder": "#121314", + "editorGroupHeader.tabsBorder": "#2A2B2CFF", "breadcrumb.foreground": "#8C8C8C", "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bfbfbf", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f963724d6b52d..6df7171b0d29a 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -203,8 +203,8 @@ "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", - "tab.activeBorder": "#00000000", - "editorGroupHeader.tabsBorder": "#00000000", + "tab.activeBorder": "#FFFFFF", + "editorGroupHeader.tabsBorder": "#F0F1F2FF", "breadcrumb.foreground": "#606060", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", From 66aa46c943596e0ebda6b36186b3b13cfd3b6c8b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 12:06:13 +0000 Subject: [PATCH 10/14] Add background color to resizable hover widget in hover.css Co-authored-by: Copilot --- src/vs/editor/contrib/hover/browser/hover.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index d9d64ffc21624..269ee853c7ec2 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -11,6 +11,7 @@ border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: var(--vscode-cornerRadius-large); box-sizing: content-box; + background-color: var(--vscode-editorHoverWidget-background); } .monaco-editor .monaco-resizable-hover > .monaco-hover { From db73eef8c4b6a5306719878ff9e1ee3fd100e821 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:22:27 -0800 Subject: [PATCH 11/14] fix quick chat input not showing label (#299750) * fix quick chat input not showing label * don't use workaround --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 6 +++--- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 -- 2 files changed, 3 insertions(+), 5 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 4f54ec7cfe316..9eb316cb94b85 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2021,7 +2021,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; this.secondaryToolbarContainer = elements.secondaryToolbar; - if (this.options.isSessionsWindow) { + if (this.options.isSessionsWindow || this.options.renderStyle === 'compact') { this.secondaryToolbarContainer.style.display = 'none'; } this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; @@ -2032,7 +2032,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; - if (this.options.isSessionsWindow) { + if (this.options.isSessionsWindow || this.options.renderStyle === 'compact') { toolbarsContainer.prepend(this.contextUsageWidgetContainer); } @@ -3147,7 +3147,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth(); const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * toolbarItemGap; const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * toolbarItemGap : 0; - const contextUsageWidth = 0;// dom.getTotalWidth(this.contextUsageWidgetContainer); + const contextUsageWidth = dom.getTotalWidth(this.contextUsageWidgetContainer); const inputToolbarsPadding = 12; // pdading between input toolbar/execute toolbar/contextUsage. return executeToolbarWidth + executeToolbarPadding + contextUsageWidth + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding + inputToolbarsPadding); }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index d99f33cca29cf..fbb9c530fbb39 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -818,8 +818,6 @@ have to be updated for changes to the rules above, or to support more deeply nes /* top padding is inside the editor widget */ width: 100%; position: relative; - /* Prevent contents from covering border corner */ - overflow: hidden; } /* Context usage widget container - positioned in the secondary toolbar below input */ From 81f2b5cd2fdf2c2ceb61899f79332db8551f2c35 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 16:14:04 +0100 Subject: [PATCH 12/14] chore - Refactor inline chat classes to use private class fields (#299778) * Refactor inline chat affordance classes to use private class fields * native privates for inline chat --- .../inlineChat/browser/inlineChatActions.ts | 11 +- .../browser/inlineChatController.ts | 299 +++++++++-------- .../browser/inlineChatEditorAffordance.ts | 112 ++++--- .../browser/inlineChatGutterAffordance.ts | 6 +- .../inlineChat/browser/inlineChatNotebook.ts | 6 +- .../browser/inlineChatOverlayWidget.ts | 303 +++++++++--------- .../browser/inlineChatSessionServiceImpl.ts | 87 ++--- .../inlineChat/browser/inlineChatWidget.ts | 178 +++++----- .../browser/inlineChatZoneWidget.ts | 75 +++-- .../test/browser/testWorkerService.ts | 24 +- 10 files changed, 595 insertions(+), 506 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index aef9aeefc52be..204ae20da8d54 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -91,11 +91,11 @@ export class StartSessionAction extends Action2 { logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize()); return; } - return this._runEditorCommand(editorAccessor, editor, ...args); + return this.#runEditorCommand(editorAccessor, editor, ...args); }); } - private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { + async #runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { const configServce = accessor.get(IConfigurationService); @@ -262,12 +262,15 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction { class KeepOrUndoSessionAction extends AbstractInlineChatAction { - constructor(private readonly _keep: boolean, desc: IAction2Options) { + readonly #keep: boolean; + + constructor(keep: boolean, desc: IAction2Options) { super(desc); + this.#keep = keep; } override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise { - if (this._keep) { + if (this.#keep) { await ctrl.acceptSession(); } else { await ctrl.rejectSession(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 76aa39da94237..869084cff1d1f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -112,63 +112,93 @@ export class InlineChatController implements IEditorContribution { * Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session. * When set, this takes priority over the inlineChat.defaultModel setting. */ - private static _userSelectedModel: string | undefined; + static #userSelectedModel: string | undefined; - private readonly _store = new DisposableStore(); - private readonly _isActiveController = observableValue(this, false); - private readonly _renderMode: IObservable<'zone' | 'hover'>; - private readonly _zone: Lazy; + readonly #store = new DisposableStore(); + readonly #isActiveController = observableValue(this, false); + readonly #renderMode: IObservable<'zone' | 'hover'>; + readonly #zone: Lazy; readonly inputOverlayWidget: InlineChatAffordance; - private readonly _inputWidget: InlineChatInputWidget; - - private readonly _currentSession: IObservable; + readonly #inputWidget: InlineChatInputWidget; + + readonly #currentSession: IObservable; + + readonly #editor: ICodeEditor; + readonly #instaService: IInstantiationService; + readonly #notebookEditorService: INotebookEditorService; + readonly #inlineChatSessionService: IInlineChatSessionService; + readonly #configurationService: IConfigurationService; + readonly #webContentExtractorService: ISharedWebContentExtractorService; + readonly #fileService: IFileService; + readonly #chatAttachmentResolveService: IChatAttachmentResolveService; + readonly #editorService: IEditorService; + readonly #markerDecorationsService: IMarkerDecorationsService; + readonly #languageModelService: ILanguageModelsService; + readonly #logService: ILogService; + readonly #chatEditingService: IChatEditingService; + readonly #chatService: IChatService; get widget(): EditorBasedInlineChatWidget { - return this._zone.value.widget; + return this.#zone.value.widget; } get isActive() { - return Boolean(this._currentSession.get()); + return Boolean(this.#currentSession.get()); } get inputWidget(): InlineChatInputWidget { - return this._inputWidget; + return this.#inputWidget; } constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + editor: ICodeEditor, + @IInstantiationService instaService: IInstantiationService, + @INotebookEditorService notebookEditorService: INotebookEditorService, + @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, - @IFileService private readonly _fileService: IFileService, - @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService, - @IEditorService private readonly _editorService: IEditorService, - @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, - @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, - @ILogService private readonly _logService: ILogService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IChatService private readonly _chatService: IChatService, + @IConfigurationService configurationService: IConfigurationService, + @ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService, + @IFileService fileService: IFileService, + @IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService, + @IEditorService editorService: IEditorService, + @IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService, + @ILanguageModelsService languageModelService: ILanguageModelsService, + @ILogService logService: ILogService, + @IChatEditingService chatEditingService: IChatEditingService, + @IChatService chatService: IChatService, ) { - const editorObs = observableCodeEditor(_editor); + this.#editor = editor; + this.#instaService = instaService; + this.#notebookEditorService = notebookEditorService; + this.#inlineChatSessionService = inlineChatSessionService; + this.#configurationService = configurationService; + this.#webContentExtractorService = webContentExtractorService; + this.#fileService = fileService; + this.#chatAttachmentResolveService = chatAttachmentResolveService; + this.#editorService = editorService; + this.#markerDecorationsService = markerDecorationsService; + this.#languageModelService = languageModelService; + this.#logService = logService; + this.#chatEditingService = chatEditingService; + this.#chatService = chatService; + + const editorObs = observableCodeEditor(editor); const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const ctxPendingConfirmation = CTX_INLINE_CHAT_PENDING_CONFIRMATION.bindTo(contextKeyService); - const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); - this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); + const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService); + this.#renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this.#configurationService); // Track whether the current editor's file is being edited by any chat editing session - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const model = editorObs.model.read(r); if (!model) { ctxFileBelongsToChat.set(false); return; } - const sessions = this._chatEditingService.editingSessionsObs.read(r); + const sessions = this.#chatEditingService.editingSessionsObs.read(r); let hasEdits = false; for (const session of sessions) { const entries = session.entries.read(r); @@ -185,25 +215,25 @@ export class InlineChatController implements IEditorContribution { ctxFileBelongsToChat.set(hasEdits); })); - const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); - const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); - this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); + const overlayWidget = this.#inputWidget = this.#store.add(this.#instaService.createInstance(InlineChatInputWidget, editorObs)); + const sessionOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); + this.inputOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatAffordance, this.#editor, overlayWidget)); - this._zone = new Lazy(() => { + this.#zone = new Lazy(() => { - assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); + assertType(this.#editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); const location: IChatWidgetLocationOptions = { location: ChatAgentLocation.EditorInline, resolveData: () => { - assertType(this._editor.hasModel()); - const wholeRange = this._editor.getSelection(); - const document = this._editor.getModel().uri; + assertType(this.#editor.hasModel()); + const wholeRange = this.#editor.getSelection(); + const document = this.#editor.getModel().uri; return { type: ChatAgentLocation.EditorInline, - id: getEditorId(this._editor, this._editor.getModel()), - selection: this._editor.getSelection(), + id: getEditorId(this.#editor, this.#editor.getModel()), + selection: this.#editor.getSelection(), document, wholeRange }; @@ -213,22 +243,22 @@ export class InlineChatController implements IEditorContribution { // inline chat in notebooks // check if this editor is part of a notebook editor // if so, update the location and use the notebook specific widget - const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor); + const notebookEditor = this.#notebookEditorService.getNotebookForPossibleCell(this.#editor); if (!!notebookEditor) { location.location = ChatAgentLocation.Notebook; if (notebookAgentConfig.get()) { location.resolveData = () => { - assertType(this._editor.hasModel()); + assertType(this.#editor.hasModel()); return { type: ChatAgentLocation.Notebook, - sessionInputUri: this._editor.getModel().uri, + sessionInputUri: this.#editor.getModel().uri, }; }; } } - const result = this._instaService.createInstance(InlineChatZoneWidget, + const result = this.#instaService.createInstance(InlineChatZoneWidget, location, { enableWorkingSet: 'implicit', @@ -248,33 +278,33 @@ export class InlineChatController implements IEditorContribution { }, defaultMode: ChatMode.Ask }, - { editor: this._editor, notebookEditor }, + { editor: this.#editor, notebookEditor }, () => Promise.resolve(), ); - this._store.add(result); + this.#store.add(result); result.domNode.classList.add('inline-chat-2'); return result; }); - const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); + const sessionsSignal = observableSignalFromEvent(this, this.#inlineChatSessionService.onDidChangeSessions); - this._currentSession = derived(r => { + this.#currentSession = derived(r => { sessionsSignal.read(r); const model = editorObs.model.read(r); - const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri); + const session = model && this.#inlineChatSessionService.getSessionByTextModel(model.uri); return session ?? undefined; }); let lastSession: IInlineChatSession2 | undefined = undefined; - this._store.add(autorun(r => { - const session = this._currentSession.read(r); + this.#store.add(autorun(r => { + const session = this.#currentSession.read(r); if (!session) { - this._isActiveController.set(false, undefined); + this.#isActiveController.set(false, undefined); if (lastSession && !lastSession.chatModel.hasRequests) { const state = lastSession.chatModel.inputModel.state.read(undefined); @@ -290,23 +320,24 @@ export class InlineChatController implements IEditorContribution { let foundOne = false; for (const editor of codeEditorService.listCodeEditors()) { - if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) { + const ctrl = InlineChatController.get(editor); + if (ctrl && ctrl.#isActiveController.read(undefined)) { foundOne = true; break; } } if (!foundOne && editorObs.isFocused.read(r)) { - this._isActiveController.set(true, undefined); + this.#isActiveController.set(true, undefined); } })); const visibleSessionObs = observableValue(this, undefined); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const model = editorObs.model.read(r); - const session = this._currentSession.read(r); - const isActive = this._isActiveController.read(r); + const session = this.#currentSession.read(r); + const isActive = this.#isActiveController.read(r); if (!session || !isActive || !model) { visibleSessionObs.set(undefined, undefined); @@ -322,38 +353,38 @@ export class InlineChatController implements IEditorContribution { }); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { // HIDE/SHOW const session = visibleSessionObs.read(r); - const renderMode = this._renderMode.read(r); + const renderMode = this.#renderMode.read(r); if (!session) { - this._zone.rawValue?.hide(); - this._zone.rawValue?.widget.chatWidget.setModel(undefined); - _editor.focus(); + this.#zone.rawValue?.hide(); + this.#zone.rawValue?.widget.chatWidget.setModel(undefined); + this.#editor.focus(); ctxInlineChatVisible.reset(); } else if (renderMode === 'hover') { // hover mode: set model but don't show zone, keep focus in editor - this._zone.value.widget.chatWidget.setModel(session.chatModel); - this._zone.rawValue?.hide(); + this.#zone.value.widget.chatWidget.setModel(session.chatModel); + this.#zone.rawValue?.hide(); ctxInlineChatVisible.set(true); } else { ctxInlineChatVisible.set(true); - this._zone.value.widget.chatWidget.setModel(session.chatModel); - if (!this._zone.value.position) { - this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); - this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug - this._zone.value.show(session.initialPosition); + this.#zone.value.widget.chatWidget.setModel(session.chatModel); + if (!this.#zone.value.position) { + this.#zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); + this.#zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug + this.#zone.value.show(session.initialPosition); } - this._zone.value.reveal(this._zone.value.position!); - this._zone.value.widget.focus(); + this.#zone.value.reveal(this.#zone.value.position!); + this.#zone.value.widget.focus(); } })); // Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); - const renderMode = this._renderMode.read(r); + const renderMode = this.#renderMode.read(r); if (!session || renderMode !== 'hover') { ctxPendingConfirmation.set(false); sessionOverlayWidget.hide(); @@ -375,7 +406,7 @@ export class InlineChatController implements IEditorContribution { } })); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); if (session) { const entries = session.editingSession.entries.read(r); @@ -393,7 +424,7 @@ export class InlineChatController implements IEditorContribution { for (const entry of otherEntries) { // OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend // that modifies other files - this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); + this.#editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); } } })); @@ -414,36 +445,36 @@ export class InlineChatController implements IEditorContribution { }); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const response = lastResponseObs.read(r); - this._zone.rawValue?.widget.updateInfo(''); + this.#zone.rawValue?.widget.updateInfo(''); if (!response?.isInProgress.read(r)) { if (response?.result?.errorDetails) { // ERROR case - this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); + this.#zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); alert(response.result.errorDetails.message); } // no response or not in progress - this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); - this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); + this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); + this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); } else { - this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); + this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); let placeholder = response.request?.message.text; const lastProgress = lastResponseProgressObs.read(r); if (lastProgress) { placeholder = renderAsPlaintext(lastProgress.content); } - this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); + this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); } })); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); if (!session) { return; @@ -456,25 +487,25 @@ export class InlineChatController implements IEditorContribution { })); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); const entry = session?.editingSession.readEntry(session.uri, r); // make sure there is an editor integration - const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor)); + const pane = this.#editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this.#editor || isNotebookWithCellEditor(candidate, this.#editor)); if (pane && entry) { entry?.getEditorIntegration(pane); } // make sure the ZONE isn't inbetween a diff and move above if so - if (entry?.diffInfo && this._zone.value.position) { - const { position } = this._zone.value; + if (entry?.diffInfo && this.#zone.value.position) { + const { position } = this.#zone.value; const diff = entry.diffInfo.read(r); for (const change of diff.changes) { if (change.modified.contains(position.lineNumber)) { - this._zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1)); + this.#zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1)); break; } } @@ -483,90 +514,90 @@ export class InlineChatController implements IEditorContribution { } dispose(): void { - this._store.dispose(); + this.#store.dispose(); } getWidgetPosition(): Position | undefined { - return this._zone.rawValue?.position; + return this.#zone.rawValue?.position; } focus() { - this._zone.rawValue?.widget.focus(); + this.#zone.rawValue?.widget.focus(); } async run(arg?: InlineChatRunOptions): Promise { - assertType(this._editor.hasModel()); - const uri = this._editor.getModel().uri; + assertType(this.#editor.hasModel()); + const uri = this.#editor.getModel().uri; - const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri); + const existingSession = this.#inlineChatSessionService.getSessionByTextModel(uri); if (existingSession) { await existingSession.editingSession.accept(); existingSession.dispose(); } - this._isActiveController.set(true, undefined); + this.#isActiveController.set(true, undefined); - const session = this._inlineChatSessionService.createSession(this._editor); + const session = this.#inlineChatSessionService.createSession(this.#editor); // Store for tracking model changes during this session const sessionStore = new DisposableStore(); try { - await this._applyModelDefaults(session, sessionStore); + await this.#applyModelDefaults(session, sessionStore); if (arg) { - arg.attachDiagnostics ??= this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone'; + arg.attachDiagnostics ??= this.#configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone'; } // ADD diagnostics (only when explicitly requested) if (arg?.attachDiagnostics) { const entries: IChatRequestVariableEntry[] = []; - for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) { - if (range.intersectRanges(this._editor.getSelection())) { + for (const [range, marker] of this.#markerDecorationsService.getLiveMarkers(uri)) { + if (range.intersectRanges(this.#editor.getSelection())) { const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter)); } } if (entries.length > 0) { - this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries); + this.#zone.value.widget.chatWidget.attachmentModel.addContext(...entries); const msg = entries.length > 1 ? localize('fixN', "Fix the attached problems") : localize('fix1', "Fix the attached problem"); - this._zone.value.widget.chatWidget.input.setValue(msg, true); + this.#zone.value.widget.chatWidget.input.setValue(msg, true); arg.message = msg; - this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + this.#zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } } // Check args if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { if (arg.initialRange) { - this._editor.revealRange(arg.initialRange); + this.#editor.revealRange(arg.initialRange); } if (arg.initialSelection) { - this._editor.setSelection(arg.initialSelection); + this.#editor.setSelection(arg.initialSelection); } if (arg.attachments) { await Promise.all(arg.attachments.map(async attachment => { - await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment); + await this.#zone.value.widget.chatWidget.attachmentModel.addFile(attachment); })); delete arg.attachments; } if (arg.modelSelector) { - const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); + const id = (await this.#languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); if (!id) { throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`); } - const model = this._languageModelService.lookupLanguageModel(id); + const model = this.#languageModelService.lookupLanguageModel(id); if (!model) { throw new Error(`Language model not loaded: ${id}.`); } - this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); + this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); } if (arg.message) { - this._zone.value.widget.chatWidget.setInput(arg.message); + this.#zone.value.widget.chatWidget.setInput(arg.message); if (arg.autoSend) { - await this._zone.value.widget.chatWidget.acceptInput(); + await this.#zone.value.widget.chatWidget.acceptInput(); } } } @@ -592,7 +623,7 @@ export class InlineChatController implements IEditorContribution { } async acceptSession() { - const session = this._currentSession.get(); + const session = this.#currentSession.get(); if (!session) { return; } @@ -601,23 +632,23 @@ export class InlineChatController implements IEditorContribution { } async rejectSession() { - const session = this._currentSession.get(); + const session = this.#currentSession.get(); if (!session) { return; } - this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); + this.#chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); await session.editingSession.reject(); session.dispose(); } - private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise { - const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get(); + async #selectVendorDefaultModel(session: IInlineChatSession2): Promise { + const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get(); if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { - const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); + const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); for (const identifier of ids) { - const candidate = this._languageModelService.lookupLanguageModel(identifier); + const candidate = this.#languageModelService.lookupLanguageModel(identifier); if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) { - this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); + this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); break; } } @@ -628,39 +659,39 @@ export class InlineChatController implements IEditorContribution { * Applies model defaults based on settings and tracks user model changes. * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ - private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { - const userSelectedModel = InlineChatController._userSelectedModel; - const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); + async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { + const userSelectedModel = InlineChatController.#userSelectedModel; + const defaultModelSetting = this.#configurationService.getValue(InlineChatConfigKeys.DefaultModel); let modelApplied = false; // 1. Try user's explicitly chosen model from a previous inline chat in the same session if (userSelectedModel) { - modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); + modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); if (!modelApplied) { // User's previously selected model is no longer available, clear it - InlineChatController._userSelectedModel = undefined; + InlineChatController.#userSelectedModel = undefined; } } // 2. Try inlineChat.defaultModel setting if (!modelApplied && defaultModelSetting) { - modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); + modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); if (!modelApplied) { - this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); + this.#logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); } } // 3. Fall back to vendor default if (!modelApplied) { - await this._selectVendorDefaultModel(session); + await this.#selectVendorDefaultModel(session); } // Track model changes - store user's explicit choice in the given sessions. // NOTE: This currently detects any model change, not just user-initiated ones. let initialModelId: string | undefined; sessionStore.add(autorun(r => { - const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); + const newModel = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); if (!newModel) { return; } @@ -670,25 +701,25 @@ export class InlineChatController implements IEditorContribution { } if (initialModelId !== newModel.identifier) { // User explicitly changed model, store their choice as qualified name - InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); + InlineChatController.#userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); initialModelId = newModel.identifier; } })); } async createImageAttachment(attachment: URI): Promise { - const value = this._currentSession.get(); + const value = this.#currentSession.get(); if (!value) { return undefined; } if (attachment.scheme === Schemas.file) { - if (await this._fileService.canHandleResource(attachment)) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); + if (await this.#fileService.canHandleResource(attachment)) { + return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); } } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); + const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None); if (extractedImages) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); + return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); } } return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index e7773395fce86..361441642d6d6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -33,12 +33,13 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j class QuickFixActionViewItem extends MenuEntryActionViewItem { - private readonly _lightBulbStore = this._store.add(new MutableDisposable()); - private _currentTitle: string | undefined; + readonly #lightBulbStore = this._store.add(new MutableDisposable()); + readonly #editor: ICodeEditor; + #currentTitle: string | undefined; constructor( action: MenuItemAction, - private readonly _editor: ICodeEditor, + editor: ICodeEditor, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -55,7 +56,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { elementGetter: () => HTMLElement | undefined = () => undefined; override async run(...args: unknown[]): Promise { - const controller = CodeActionController.get(_editor); + const controller = CodeActionController.get(editor); const info = controller?.lightBulbState.get(); const element = this.elementGetter(); if (controller && info && element) { @@ -67,26 +68,27 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { super(wrappedAction, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); + this.#editor = editor; wrappedAction.elementGetter = () => this.element; } override render(container: HTMLElement): void { super.render(container); - this._updateFromLightBulb(); + this.#updateFromLightBulb(); } protected override getTooltip(): string { - return this._currentTitle ?? super.getTooltip(); + return this.#currentTitle ?? super.getTooltip(); } - private _updateFromLightBulb(): void { - const controller = CodeActionController.get(this._editor); + #updateFromLightBulb(): void { + const controller = CodeActionController.get(this.#editor); if (!controller) { return; } const store = new DisposableStore(); - this._lightBulbStore.value = store; + this.#lightBulbStore.value = store; store.add(autorun(reader => { const info = controller.lightBulbState.read(reader); @@ -99,7 +101,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { } // Update tooltip - this._currentTitle = info?.title; + this.#currentTitle = info?.title; this.updateTooltip(); })); } @@ -107,7 +109,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { - private readonly _kbLabel: string | undefined; + readonly #kbLabel: string | undefined; constructor( action: MenuItemAction, @@ -121,14 +123,14 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); this.options.label = true; this.options.icon = false; - this._kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined; + this.#kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined; } protected override updateLabel(): void { if (this.label) { dom.reset(this.label, this.action.label, - ...(this._kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this._kbLabel)] : []) + ...(this.#kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this.#kbLabel)] : []) ); } } @@ -140,38 +142,42 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { */ export class InlineChatEditorAffordance extends Disposable implements IContentWidget { - private static _idPool = 0; + static #idPool = 0; - private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`; - private readonly _domNode: HTMLElement; - private _position: IContentWidgetPosition | null = null; - private _isVisible = false; + readonly #id = `inline-chat-content-widget-${InlineChatEditorAffordance.#idPool++}`; + readonly #domNode: HTMLElement; + #position: IContentWidgetPosition | null = null; + #isVisible = false; - private readonly _onDidRunAction = this._store.add(new Emitter()); - readonly onDidRunAction: Event = this._onDidRunAction.event; + readonly #onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this.#onDidRunAction.event; readonly allowEditorOverflow = true; readonly suppressMouseDown = false; + readonly #editor: ICodeEditor; + constructor( - private readonly _editor: ICodeEditor, + editor: ICodeEditor, selection: IObservable, @IInstantiationService instantiationService: IInstantiationService, ) { super(); + this.#editor = editor; + // Create the widget DOM - this._domNode = dom.$('.inline-chat-content-widget'); + this.#domNode = dom.$('.inline-chat-content-widget'); // Create toolbar with the inline chat start action - const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, { + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#domNode, MenuId.InlineChatEditorAffordance, { telemetrySource: 'inlineChatEditorAffordance', hiddenItemStrategy: HiddenItemStrategy.Ignore, menuOptions: { renderShortTitle: true }, toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true }, actionViewItemProvider: (action: IAction) => { if (action instanceof MenuItemAction && action.id === quickFixCommandId) { - return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); + return instantiationService.createInstance(QuickFixActionViewItem, action, this.#editor); } if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT || action.id === 'inlineChat.fixDiagnostics')) { return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action); @@ -180,37 +186,37 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi } })); this._store.add(toolbar.actionRunner.onDidRun((e) => { - this._onDidRunAction.fire(e.action.id); - this._hide(); + this.#onDidRunAction.fire(e.action.id); + this.#hide(); })); this._store.add(autorun(r => { const sel = selection.read(r); if (sel) { - this._show(sel); + this.#show(sel); } else { - this._hide(); + this.#hide(); } })); } - private _show(selection: Selection): void { + #show(selection: Selection): void { if (selection.isEmpty()) { - this._showAtLineStart(selection.getPosition().lineNumber); + this.#showAtLineStart(selection.getPosition().lineNumber); } else { - this._showAtSelection(selection); + this.#showAtSelection(selection); } - if (this._isVisible) { - this._editor.layoutContentWidget(this); + if (this.#isVisible) { + this.#editor.layoutContentWidget(this); } else { - this._editor.addContentWidget(this); - this._isVisible = true; + this.#editor.addContentWidget(this); + this.#isVisible = true; } } - private _showAtSelection(selection: Selection): void { + #showAtSelection(selection: Selection): void { const cursorPosition = selection.getPosition(); const direction = selection.getDirection(); @@ -218,20 +224,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; - this._position = { + this.#position = { position: cursorPosition, preference: [preference], }; } - private _showAtLineStart(lineNumber: number): void { - const model = this._editor.getModel(); + #showAtLineStart(lineNumber: number): void { + const model = this.#editor.getModel(); if (!model) { return; } const tabSize = model.getOptions().tabSize; - const fontInfo = this._editor.getOptions().get(EditorOption.fontInfo); + const fontInfo = this.#editor.getOptions().get(EditorOption.fontInfo); const lineContent = model.getLineContent(lineNumber); const indent = computeIndentLevel(lineContent, tabSize); const lineHasSpace = indent < 0 ? true : fontInfo.spaceWidth * indent > 22; @@ -254,43 +260,43 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi const effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1; - this._position = { + this.#position = { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: [ContentWidgetPositionPreference.EXACT], }; } - private _hide(): void { - if (this._isVisible) { - this._isVisible = false; - this._editor.removeContentWidget(this); + #hide(): void { + if (this.#isVisible) { + this.#isVisible = false; + this.#editor.removeContentWidget(this); } } getId(): string { - return this._id; + return this.#id; } getDomNode(): HTMLElement { - return this._domNode; + return this.#domNode; } getPosition(): IContentWidgetPosition | null { - return this._position; + return this.#position; } beforeRender(): IDimension | null { - const position = this._editor.getPosition(); - const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight); + const position = this.#editor.getPosition(); + const lineHeight = position ? this.#editor.getLineHeightForPosition(position) : this.#editor.getOption(EditorOption.lineHeight); - this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); + this.#domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); return null; } override dispose(): void { - if (this._isVisible) { - this._editor.removeContentWidget(this); + if (this.#isVisible) { + this.#editor.removeContentWidget(this); } super.dispose(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 3d82cec90ec04..03a7766904655 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -26,8 +26,8 @@ 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; + readonly #onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this.#onDidRunAction.event; constructor( myEditorObs: ObservableCodeEditor, @@ -108,6 +108,6 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { this._store.add(menu); - this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); + this._store.add(this.onDidCloseWithCommand(commandId => this.#onDidRunAction.fire(commandId))); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index 539e8197ee046..ca722843a32e6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -14,7 +14,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js export class InlineChatNotebookContribution { - private readonly _store = new DisposableStore(); + readonly #store = new DisposableStore(); constructor( @IInlineChatSessionService sessionService: IInlineChatSessionService, @@ -22,7 +22,7 @@ export class InlineChatNotebookContribution { @INotebookEditorService notebookEditorService: INotebookEditorService, ) { - this._store.add(sessionService.onWillStartSession(newSessionEditor => { + this.#store.add(sessionService.onWillStartSession(newSessionEditor => { const candidate = CellUri.parse(newSessionEditor.getModel().uri); if (!candidate) { return; @@ -51,6 +51,6 @@ export class InlineChatNotebookContribution { } dispose(): void { - this._store.dispose(); + this.#store.dispose(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 04a76d7327a9f..5112e2fe44381 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -44,51 +44,58 @@ 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 _toolbarContainer: HTMLElement; - private readonly _input: IActiveCodeEditor; - private readonly _position = observableValue(this, null); - readonly position: IObservable = this._position; - - private readonly _showStore = this._store.add(new DisposableStore()); - private readonly _stickyScrollHeight: IObservable; - private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; - private _anchorLineNumber: number = 0; - private _anchorLeft: number = 0; - private _anchorAbove: boolean = false; - + readonly #domNode: HTMLElement; + readonly #container: HTMLElement; + readonly #inputContainer: HTMLElement; + readonly #toolbarContainer: HTMLElement; + readonly #input: IActiveCodeEditor; + readonly #position = observableValue(this, null); + readonly position: IObservable = this.#position; + + readonly #showStore = this._store.add(new DisposableStore()); + readonly #stickyScrollHeight: IObservable; + readonly #layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; + #anchorLineNumber: number = 0; + #anchorLeft: number = 0; + #anchorAbove: boolean = false; + + readonly #editorObs: ObservableCodeEditor; + readonly #contextKeyService: IContextKeyService; + readonly #menuService: IMenuService; constructor( - private readonly _editorObs: ObservableCodeEditor, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IMenuService private readonly _menuService: IMenuService, + editorObs: ObservableCodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, ) { super(); + this.#editorObs = editorObs; + this.#contextKeyService = contextKeyService; + this.#menuService = menuService; + // Create container - this._domNode = dom.$('.inline-chat-gutter-menu'); + this.#domNode = dom.$('.inline-chat-gutter-menu'); // Create inner container (background + focus border) - this._container = dom.append(this._domNode, dom.$('.inline-chat-gutter-container')); + this.#container = dom.append(this.#domNode, dom.$('.inline-chat-gutter-container')); // Create input editor container - this._inputContainer = dom.append(this._container, dom.$('.input')); + this.#inputContainer = dom.append(this.#container, dom.$('.input')); // Create toolbar container - this._toolbarContainer = dom.append(this._container, dom.$('.toolbar')); + 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 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 actionsMenu = this._store.add(this.#menuService.createMenu(MenuId.ChatEditorInlineMenu, this.#contextKeyService)); const updateActions = () => { const actions = getFlatActionBarActions(actionsMenu.getActions({ shouldForwardArgs: true })); actionBar.clear(); @@ -123,13 +130,13 @@ export class InlineChatInputWidget extends Disposable { ]) }; - this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + this.#input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this.#inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); - this._input.setModel(model); + this.#input.setModel(model); // Create toolbar - const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MenuId.InlineChatInput, { + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#toolbarContainer, MenuId.InlineChatInput, { telemetrySource: 'inlineChatInput.toolbar', hiddenItemStrategy: HiddenItemStrategy.NoHide, toolbarOptions: { @@ -139,8 +146,8 @@ export class InlineChatInputWidget extends Disposable { })); // Initialize sticky scroll height observable - const stickyScrollController = StickyScrollController.get(this._editorObs.editor); - this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + 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); @@ -150,24 +157,24 @@ export class InlineChatInputWidget extends Disposable { this._store.add(resizeObserver); this._store.add(resizeObserver.observe(toolbar.getElement())); - const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); - const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); + const contentWidth = observableFromEvent(this, this.#input.onDidChangeModelContent, () => this.#input.getContentWidth()); + const contentHeight = observableFromEvent(this, this.#input.onDidContentSizeChange, () => this.#input.getContentHeight()); - this._layoutData = derived(r => { + this.#layoutData = derived(r => { const editorPad = 6; const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); const minWidth = 220; const maxWidth = 600; - const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' + const clampedWidth = this.#input.getOption(EditorOption.wordWrap) === 'on' ? maxWidth : Math.max(minWidth, Math.min(totalWidth, maxWidth)); - const lineHeight = this._input.getOption(EditorOption.lineHeight); + 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', }); + this.#input.updateOptions({ wordWrap: 'on', }); } return { @@ -180,42 +187,42 @@ export class InlineChatInputWidget extends Disposable { // Update container width and editor layout when width changes this._store.add(autorun(r => { - const { editorPad, toolbarWidth, totalWidth, height } = this._layoutData.read(r); + const { editorPad, toolbarWidth, totalWidth, height } = this.#layoutData.read(r); const inputWidth = totalWidth - toolbarWidth - editorPad; - this._container.style.width = `${totalWidth}px`; - this._inputContainer.style.width = `${inputWidth}px`; - this._input.layout({ width: inputWidth, height }); + 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'))); + 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); + this._store.add(this.#input.onDidScrollChange(e => { + this.#toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); })); // 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); + 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())); // Track focus state - const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService); - this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); - this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); + const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this.#contextKeyService); + this._store.add(this.#input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); + this._store.add(this.#input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); this._store.add(toDisposable(() => inputWidgetFocused.reset())); // Handle key events: ArrowDown to move to actions - this._store.add(this._input.onKeyDown(e => { + this._store.add(this.#input.onKeyDown(e => { if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { - const model = this._input.getModel(); - const position = this._input.getPosition(); + const model = this.#input.getModel(); + const position = this.#input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { e.preventDefault(); e.stopPropagation(); @@ -237,18 +244,18 @@ export class InlineChatInputWidget extends Disposable { if (firstItem?.element && dom.isAncestorOfActiveElement(firstItem.element)) { event.preventDefault(); event.stopPropagation(); - this._input.focus(); + this.#input.focus(); } } }, true)); // Track focus - hide when focus leaves - const focusTracker = this._store.add(dom.trackFocus(this._domNode)); + const focusTracker = this._store.add(dom.trackFocus(this.#domNode)); this._store.add(focusTracker.onDidBlur(() => this.hide())); } get value(): string { - return this._input.getModel().getValue().trim(); + return this.#input.getModel().getValue().trim(); } /** @@ -258,77 +265,77 @@ export class InlineChatInputWidget extends Disposable { * @param anchorAbove Whether to anchor above the position (widget grows upward) */ show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { - this._showStore.clear(); + this.#showStore.clear(); // Clear input state - this._input.updateOptions({ wordWrap: 'off', placeholder }); - this._input.getModel().setValue(''); + this.#input.updateOptions({ wordWrap: 'off', placeholder }); + this.#input.getModel().setValue(''); // Store anchor info for scroll updates - this._anchorLineNumber = lineNumber; - this._anchorLeft = left; - this._anchorAbove = anchorAbove; + this.#anchorLineNumber = lineNumber; + this.#anchorLeft = left; + this.#anchorAbove = anchorAbove; // Set initial position - this._updatePosition(); + this.#updatePosition(); // Create overlay widget via observable pattern - this._showStore.add(this._editorObs.createOverlayWidget({ - domNode: this._domNode, - position: this._position, + this.#showStore.add(this.#editorObs.createOverlayWidget({ + domNode: this.#domNode, + position: this.#position, minContentWidthInPx: constObservable(0), allowEditorOverflow: true, })); // If anchoring above, adjust position after render to account for widget height if (anchorAbove) { - this._updatePosition(); + this.#updatePosition(); } // Update position on scroll, hide if anchor line is out of view (only when input is empty) - this._showStore.add(this._editorObs.editor.onDidScrollChange(() => { - const visibleRanges = this._editorObs.editor.getVisibleRanges(); + this.#showStore.add(this.#editorObs.editor.onDidScrollChange(() => { + const visibleRanges = this.#editorObs.editor.getVisibleRanges(); const isLineVisible = visibleRanges.some(range => - this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber + this.#anchorLineNumber >= range.startLineNumber && this.#anchorLineNumber <= range.endLineNumber ); - const hasContent = !!this._input.getModel().getValue(); + const hasContent = !!this.#input.getModel().getValue(); if (!isLineVisible && !hasContent) { this.hide(); } else { - this._updatePosition(); + this.#updatePosition(); } })); // Focus the input editor - setTimeout(() => this._input.focus(), 0); + setTimeout(() => this.#input.focus(), 0); } - private _updatePosition(): void { - const editor = this._editorObs.editor; + #updatePosition(): void { + const editor = this.#editorObs.editor; const lineHeight = editor.getOption(EditorOption.lineHeight); - const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop(); + const top = editor.getTopForLineNumber(this.#anchorLineNumber) - editor.getScrollTop(); let adjustedTop = top; - if (this._anchorAbove) { - const widgetHeight = this._domNode.offsetHeight; + if (this.#anchorAbove) { + const widgetHeight = this.#domNode.offsetHeight; adjustedTop = top - widgetHeight; } else { adjustedTop = top + lineHeight; } // Clamp to viewport bounds when anchor line is out of view - const stickyScrollHeight = this._stickyScrollHeight.get(); + const stickyScrollHeight = this.#stickyScrollHeight.get(); const layoutInfo = editor.getLayoutInfo(); - const widgetHeight = this._domNode.offsetHeight; + const widgetHeight = this.#domNode.offsetHeight; const minTop = stickyScrollHeight; const maxTop = layoutInfo.height - widgetHeight; const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop)); const isClamped = clampedTop !== adjustedTop; - this._domNode.classList.toggle('clamped', isClamped); + this.#domNode.classList.toggle('clamped', isClamped); - this._position.set({ - preference: { top: clampedTop, left: this._anchorLeft }, + this.#position.set({ + preference: { top: clampedTop, left: this.#anchorLeft }, stackOrdinal: 10000, }, undefined); } @@ -338,13 +345,13 @@ export class InlineChatInputWidget extends Disposable { */ hide(): void { // Focus editor if focus is still within the editor's DOM - const editorDomNode = this._editorObs.editor.getDomNode(); + const editorDomNode = this.#editorObs.editor.getDomNode(); if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) { - this._editorObs.editor.focus(); + this.#editorObs.editor.focus(); } - this._position.set(null, undefined); - this._input.getModel().setValue(''); - this._showStore.clear(); + this.#position.set(null, undefined); + this.#input.getModel().setValue(''); + this.#showStore.clear(); } } @@ -353,52 +360,62 @@ export class InlineChatInputWidget extends Disposable { */ export class InlineChatSessionOverlayWidget extends Disposable { - private readonly _domNode: HTMLElement = document.createElement('div'); - private readonly _container: HTMLElement; - private readonly _statusNode: HTMLElement; - private readonly _icon: HTMLElement; - private readonly _message: HTMLElement; - private readonly _toolbarNode: HTMLElement; + readonly #domNode: HTMLElement = document.createElement('div'); + readonly #container: HTMLElement; + readonly #statusNode: HTMLElement; + readonly #icon: HTMLElement; + readonly #message: HTMLElement; + readonly #toolbarNode: HTMLElement; - private readonly _showStore = this._store.add(new DisposableStore()); - private readonly _position = observableValue(this, null); - private readonly _minContentWidthInPx = constObservable(0); + readonly #showStore = this._store.add(new DisposableStore()); + readonly #position = observableValue(this, null); + readonly #minContentWidthInPx = constObservable(0); - private readonly _stickyScrollHeight: IObservable; + readonly #stickyScrollHeight: IObservable; + + readonly #editorObs: ObservableCodeEditor; + readonly #instaService: IInstantiationService; + readonly #keybindingService: IKeybindingService; + readonly #logService: ILogService; constructor( - private readonly _editorObs: ObservableCodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ILogService private readonly _logService: ILogService, + editorObs: ObservableCodeEditor, + @IInstantiationService instaService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @ILogService logService: ILogService, ) { super(); - this._domNode.classList.add('inline-chat-session-overlay-widget'); + this.#editorObs = editorObs; + this.#instaService = instaService; + this.#keybindingService = keybindingService; + this.#logService = logService; + + this.#domNode.classList.add('inline-chat-session-overlay-widget'); - this._container = document.createElement('div'); - this._domNode.appendChild(this._container); - this._container.classList.add('inline-chat-session-overlay-container'); + this.#container = document.createElement('div'); + this.#domNode.appendChild(this.#container); + this.#container.classList.add('inline-chat-session-overlay-container'); // Create status node with icon and message - this._statusNode = document.createElement('div'); - this._statusNode.classList.add('status'); - this._icon = dom.append(this._statusNode, dom.$('span')); - this._message = dom.append(this._statusNode, dom.$('span.message')); - this._container.appendChild(this._statusNode); + this.#statusNode = document.createElement('div'); + this.#statusNode.classList.add('status'); + this.#icon = dom.append(this.#statusNode, dom.$('span')); + this.#message = dom.append(this.#statusNode, dom.$('span.message')); + this.#container.appendChild(this.#statusNode); // Create toolbar node - this._toolbarNode = document.createElement('div'); - this._toolbarNode.classList.add('toolbar'); + this.#toolbarNode = document.createElement('div'); + this.#toolbarNode.classList.add('toolbar'); // Initialize sticky scroll height observable - const stickyScrollController = StickyScrollController.get(this._editorObs.editor); - this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + const stickyScrollController = StickyScrollController.get(this.#editorObs.editor); + this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); } show(session: IInlineChatSession2): void { - assertType(this._editorObs.editor.hasModel()); - this._showStore.clear(); + assertType(this.#editorObs.editor.hasModel()); + this.#showStore.clear(); // Derived entry observable for this session const entry = derived(r => session.editingSession.readEntry(session.uri, r)); @@ -458,34 +475,34 @@ export class InlineChatSessionOverlayWidget extends Disposable { } }); - this._showStore.add(autorun(r => { + this.#showStore.add(autorun(r => { const value = requestMessage.read(r); if (value) { - this._message.innerText = renderAsPlaintext(value.message); - this._icon.className = ''; - this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); + this.#message.innerText = renderAsPlaintext(value.message); + this.#icon.className = ''; + this.#icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); } else { - this._message.innerText = ''; - this._icon.className = ''; + this.#message.innerText = ''; + this.#icon.className = ''; } })); // Log when pending confirmation changes - this._showStore.add(autorun(r => { + this.#showStore.add(autorun(r => { const response = session.chatModel.lastRequestObs.read(r)?.response; const pending = response?.isPendingConfirmation.read(r); if (pending) { - this._logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`); + this.#logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`); } })); // Add toolbar - this._container.appendChild(this._toolbarNode); - this._showStore.add(toDisposable(() => this._toolbarNode.remove())); + this.#container.appendChild(this.#toolbarNode); + this.#showStore.add(toDisposable(() => this.#toolbarNode.remove())); const that = this; - this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditorInlineExecute, { + this.#showStore.add(this.#instaService.createInstance(MenuWorkbenchToolBar, this.#toolbarNode, MenuId.ChatEditorInlineExecute, { telemetrySource: 'inlineChatProgress.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -501,52 +518,52 @@ export class InlineChatSessionOverlayWidget extends Disposable { return undefined; // use default action view item with label } - return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that._keybindingService, primaryActions); + return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that.#keybindingService, primaryActions); } })); // Position in top right of editor, below sticky scroll - const lineHeight = this._editorObs.getOption(EditorOption.lineHeight); + const lineHeight = this.#editorObs.getOption(EditorOption.lineHeight); // Track widget width changes const widgetWidth = observableValue(this, 0); const resizeObserver = new dom.DisposableResizeObserver(() => { - widgetWidth.set(this._domNode.offsetWidth, undefined); + widgetWidth.set(this.#domNode.offsetWidth, undefined); }); - this._showStore.add(resizeObserver); - this._showStore.add(resizeObserver.observe(this._domNode)); + this.#showStore.add(resizeObserver); + this.#showStore.add(resizeObserver.observe(this.#domNode)); - this._showStore.add(autorun(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - const stickyScrollHeight = this._stickyScrollHeight.read(r); + this.#showStore.add(autorun(r => { + const layoutInfo = this.#editorObs.layoutInfo.read(r); + const stickyScrollHeight = this.#stickyScrollHeight.read(r); const width = widgetWidth.read(r); const padding = Math.round(lineHeight.read(r) * 2 / 3); // Cap max-width to the editor viewport (content area) const maxWidth = layoutInfo.contentWidth - 2 * padding; - this._domNode.style.maxWidth = `${maxWidth}px`; + this.#domNode.style.maxWidth = `${maxWidth}px`; // Position: top right, below sticky scroll with padding, left of minimap and scrollbar const top = stickyScrollHeight + padding; const left = layoutInfo.width - width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - padding; - this._position.set({ + this.#position.set({ preference: { top, left }, stackOrdinal: 10000, }, undefined); })); // Create overlay widget - this._showStore.add(this._editorObs.createOverlayWidget({ - domNode: this._domNode, - position: this._position, - minContentWidthInPx: this._minContentWidthInPx, + this.#showStore.add(this.#editorObs.createOverlayWidget({ + domNode: this.#domNode, + position: this.#position, + minContentWidthInPx: this.#minContentWidthInPx, allowEditorOverflow: false, })); } hide(): void { - this._position.set(null, undefined); - this._showStore.clear(); + this.#position.set(null, undefined); + this.#showStore.clear(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 4a008a59b0f9d..4f36ec8bba0c0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -41,55 +41,58 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; - private readonly _store = new DisposableStore(); - private readonly _sessions = new ResourceMap(); + readonly #store = new DisposableStore(); + readonly #sessions = new ResourceMap(); - private readonly _onWillStartSession = this._store.add(new Emitter()); - readonly onWillStartSession: Event = this._onWillStartSession.event; + readonly #onWillStartSession = this.#store.add(new Emitter()); + readonly onWillStartSession: Event = this.#onWillStartSession.event; - private readonly _onDidChangeSessions = this._store.add(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + readonly #onDidChangeSessions = this.#store.add(new Emitter()); + readonly onDidChangeSessions: Event = this.#onDidChangeSessions.event; + + readonly #chatService: IChatService; constructor( - @IChatService private readonly _chatService: IChatService, + @IChatService chatService: IChatService, @IChatAgentService chatAgentService: IChatAgentService, ) { + this.#chatService = chatService; // Listen for agent changes and dispose all sessions when there is no agent const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const agent = agentObs.read(r); if (!agent) { // No agent available, dispose all sessions - dispose(this._sessions.values()); - this._sessions.clear(); + dispose(this.#sessions.values()); + this.#sessions.clear(); } })); } dispose() { - this._store.dispose(); + this.#store.dispose(); } createSession(editor: IActiveCodeEditor): IInlineChatSession2 { const uri = editor.getModel().uri; - if (this._sessions.has(uri)) { + if (this.#sessions.has(uri)) { throw new Error('Session already exists'); } - this._onWillStartSession.fire(editor); + this.#onWillStartSession.fire(editor); - const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); + const chatModelRef = this.#chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); const chatModel = chatModelRef.object; chatModel.startEditingSession(false); const store = new DisposableStore(); store.add(toDisposable(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); + this.#chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); chatModel.editingSession?.reject(); - this._sessions.delete(uri); - this._onDidChangeSessions.fire(this); + this.#sessions.delete(uri); + this.#onDidChangeSessions.fire(this); })); store.add(chatModelRef); @@ -104,7 +107,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) { const response = chatModel.getRequests().at(-1)?.response; if (response) { - this._chatService.notifyUserAction({ + this.#chatService.notifyUserAction({ sessionResource: response.session.sessionResource, requestId: response.requestId, agentId: response.agent?.id, @@ -138,16 +141,16 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { editingSession: chatModel.editingSession!, dispose: store.dispose.bind(store) }; - this._sessions.set(uri, result); - this._onDidChangeSessions.fire(this); + this.#sessions.set(uri, result); + this.#onDidChangeSessions.fire(this); return result; } getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { - let result = this._sessions.get(uri); + let result = this.#sessions.get(uri); if (!result) { // no direct session, try to find an editing session which has a file entry for the uri - for (const [_, candidate] of this._sessions) { + for (const [_, candidate] of this.#sessions) { const entry = candidate.editingSession.getEntry(uri); if (entry) { result = candidate; @@ -159,7 +162,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { - for (const session of this._sessions.values()) { + for (const session of this.#sessions.values()) { if (isEqual(session.chatModel.sessionResource, sessionResource)) { return session; } @@ -172,11 +175,11 @@ export class InlineChatEnabler { static Id = 'inlineChat.enabler'; - private readonly _ctxHasProvider2: IContextKey; - private readonly _ctxHasNotebookProvider: IContextKey; - private readonly _ctxPossible: IContextKey; + readonly #ctxHasProvider2: IContextKey; + readonly #ctxHasNotebookProvider: IContextKey; + readonly #ctxPossible: IContextKey; - private readonly _store = new DisposableStore(); + readonly #store = new DisposableStore(); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -184,41 +187,41 @@ export class InlineChatEnabler { @IEditorService editorService: IEditorService, @IConfigurationService configService: IConfigurationService, ) { - this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); - this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); - this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); + this.#ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); + this.#ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); + this.#ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const agent = agentObs.read(r); if (!agent) { - this._ctxHasProvider2.reset(); + this.#ctxHasProvider2.reset(); } else { - this._ctxHasProvider2.set(true); + this.#ctxHasProvider2.set(true); } })); - this._store.add(autorun(r => { - this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); + this.#store.add(autorun(r => { + this.#ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); })); const updateEditor = () => { const ctrl = editorService.activeEditorPane?.getControl(); const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl); - this._ctxPossible.set(isCodeEditorLike); + this.#ctxPossible.set(isCodeEditorLike); }; - this._store.add(editorService.onDidActiveEditorChange(updateEditor)); + this.#store.add(editorService.onDidActiveEditorChange(updateEditor)); updateEditor(); } dispose() { - this._ctxPossible.reset(); - this._ctxHasProvider2.reset(); - this._store.dispose(); + this.#ctxPossible.reset(); + this.#ctxHasProvider2.reset(); + this.#store.dispose(); } } @@ -229,7 +232,7 @@ export class InlineChatEscapeToolContribution extends Disposable { static readonly DONT_ASK_AGAIN_KEY = 'inlineChat.dontAskMoveToPanelChat'; - private static readonly _data: IToolData = { + static readonly #data: IToolData = { id: 'inline_chat_exit', source: ToolDataSource.Internal, canBeReferencedInPrompt: false, @@ -251,7 +254,7 @@ export class InlineChatEscapeToolContribution extends Disposable { super(); - this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, { + this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution.#data, { invoke: async (invocation, _tokenCountFn, _progress, _token) => { const sessionResource = invocation.context?.sessionResource; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index a449ed2a51206..9d3dc3edbcb77 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -95,37 +95,59 @@ export class InlineChatWidget { protected readonly _store = new DisposableStore(); - private readonly _ctxInputEditorFocused: IContextKey; - private readonly _ctxResponseFocused: IContextKey; + readonly #ctxInputEditorFocused: IContextKey; + readonly #ctxResponseFocused: IContextKey; - private readonly _chatWidget: ChatWidget; + readonly #chatWidget: ChatWidget; protected readonly _onDidChangeHeight = this._store.add(new Emitter()); - readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); + readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting); - private readonly _requestInProgress = observableValue(this, false); - readonly requestInProgress: IObservable = this._requestInProgress; + readonly #requestInProgress = observableValue(this, false); + readonly requestInProgress: IObservable = this.#requestInProgress; - private _isLayouting: boolean = false; + #isLayouting: boolean = false; readonly scopedContextKeyService: IContextKeyService; + readonly #options: IInlineChatWidgetConstructionOptions; + readonly #contextKeyService: IContextKeyService; + readonly #keybindingService: IKeybindingService; + readonly #accessibilityService: IAccessibilityService; + readonly #configurationService: IConfigurationService; + readonly #accessibleViewService: IAccessibleViewService; + readonly #chatService: IChatService; + readonly #hoverService: IHoverService; + readonly #chatEntitlementService: IChatEntitlementService; + readonly #markdownRendererService: IMarkdownRendererService; + constructor( location: IChatWidgetLocationOptions, - private readonly _options: IInlineChatWidgetConstructionOptions, + options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibleViewService accessibleViewService: IAccessibleViewService, @ITextModelService protected readonly _textModelResolverService: ITextModelService, - @IChatService private readonly _chatService: IChatService, - @IHoverService private readonly _hoverService: IHoverService, - @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, - @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, + @IChatService chatService: IChatService, + @IHoverService hoverService: IHoverService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, + @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, ) { - this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); + this.#options = options; + this.#contextKeyService = contextKeyService; + this.#keybindingService = keybindingService; + this.#accessibilityService = accessibilityService; + this.#configurationService = configurationService; + this.#accessibleViewService = accessibleViewService; + this.#chatService = chatService; + this.#hoverService = hoverService; + this.#chatEntitlementService = chatEntitlementService; + this.#markdownRendererService = markdownRendererService; + + this.scopedContextKeyService = this._store.add(contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ IContextKeyService, @@ -134,7 +156,7 @@ export class InlineChatWidget { this._store ); - this._chatWidget = scopedInstaService.createInstance( + this.#chatWidget = scopedInstaService.createInstance( ChatWidget, location, { isInlineChat: true }, @@ -154,14 +176,14 @@ export class InlineChatWidget { if (emptyResponse) { return false; } - if (item.response.value.every(item => item.kind === 'textEditGroup' && _options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { + if (item.response.value.every(item => item.kind === 'textEditGroup' && this.#options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { return false; } return true; }, dndContainer: this._elements.root, defaultMode: ChatMode.Ask, - ..._options.chatWidgetViewOptions + ...this.#options.chatWidgetViewOptions }, { listForeground: inlineChatForeground, @@ -171,11 +193,11 @@ export class InlineChatWidget { resultEditorBackground: editorBackground } ); - this._elements.root.classList.toggle('in-zone-widget', !!_options.inZoneWidget); - this._chatWidget.render(this._elements.chatWidget); + this._elements.root.classList.toggle('in-zone-widget', !!this.#options.inZoneWidget); + this.#chatWidget.render(this._elements.chatWidget); this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); - this._chatWidget.setVisible(true); - this._store.add(this._chatWidget); + this.#chatWidget.setVisible(true); + this._store.add(this.#chatWidget); const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService); const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService); @@ -184,10 +206,10 @@ export class InlineChatWidget { const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService); const viewModelStore = this._store.add(new DisposableStore()); - this._store.add(this._chatWidget.onDidChangeViewModel(() => { + this._store.add(this.#chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); - const viewModel = this._chatWidget.viewModel; + const viewModel = this.#chatWidget.viewModel; if (!viewModel) { return; } @@ -203,7 +225,7 @@ export class InlineChatWidget { viewModelStore.add(viewModel.onDidChange(() => { - this._requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); + this.#requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); const last = viewModel.getItems().at(-1); toolbar2.context = last; @@ -224,22 +246,22 @@ export class InlineChatWidget { })); // context keys - this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); + this.#ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this.#contextKeyService); const tracker = this._store.add(trackFocus(this.domNode)); - this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false))); - this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true))); + this._store.add(tracker.onDidBlur(() => this.#ctxResponseFocused.set(false))); + this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true))); - this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService); - this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true))); - this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); + this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this.#contextKeyService); + this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); + this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); - const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu; + const statusMenuId = this.#options.statusMenuId instanceof MenuId ? this.#options.statusMenuId : this.#options.statusMenuId.menu; // BUTTON bar - const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options; + const statusMenuOptions = this.#options.statusMenuId instanceof MenuId ? undefined : this.#options.statusMenuId.options; const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar1, statusMenuId, { toolbarOptions: { primaryGroup: '0_main' }, - telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource, + telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource, menuOptions: { renderShortTitle: true }, ...statusMenuOptions, }); @@ -247,8 +269,8 @@ export class InlineChatWidget { this._store.add(statusButtonBar); // secondary toolbar - const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, _options.secondaryMenuId ?? MenuId.for(''), { - telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource, + const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, this.#options.secondaryMenuId ?? MenuId.for(''), { + telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource, menuOptions: { renderShortTitle: true, shouldForwardArgs: true }, actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId) { @@ -261,60 +283,60 @@ export class InlineChatWidget { this._store.add(toolbar2); - this._store.add(this._configurationService.onDidChangeConfiguration(e => { + this._store.add(this.#configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); + this.#updateAriaLabel(); } })); this._elements.root.tabIndex = 0; this._elements.statusLabel.tabIndex = 0; - this._updateAriaLabel(); - this._setupDisclaimer(); + this.#updateAriaLabel(); + this.#setupDisclaimer(); - this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { + this._store.add(this.#hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { return this._elements.statusLabel.dataset['title']; })); - this._store.add(this._chatService.onDidPerformUserAction(e => { - if (isEqual(e.sessionResource, this._chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { + this._store.add(this.#chatService.onDidPerformUserAction(e => { + if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 }); } })); } - private _updateAriaLabel(): void { + #updateAriaLabel(): void { - this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._elements.root.ariaLabel = this.#accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); - if (this._accessibilityService.isScreenReaderOptimized()) { + if (this.#accessibilityService.isScreenReaderOptimized()) { let label = defaultAriaLabel; - if (this._configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { - const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + if (this.#configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { + const kbLabel = this.#keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); label = kbLabel ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - this._chatWidget.inputEditor.updateOptions({ ariaLabel: label }); + this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label }); } } - private _setupDisclaimer(): void { + #setupDisclaimer(): void { const disposables = this._store.add(new DisposableStore()); this._store.add(autorun(reader => { disposables.clear(); reset(this._elements.disclaimerLabel); - const sentiment = this._chatEntitlementService.sentimentObs.read(reader); - const anonymous = this._chatEntitlementService.anonymousObs.read(reader); - const requestInProgress = this._chatService.requestInProgressObs.read(reader); + const sentiment = this.#chatEntitlementService.sentimentObs.read(reader); + const anonymous = this.#chatEntitlementService.anonymousObs.read(reader); + const requestInProgress = this.#chatService.requestInProgressObs.read(reader); const showDisclaimer = !sentiment.installed && anonymous && !requestInProgress; this._elements.disclaimerLabel.classList.toggle('hidden', !showDisclaimer); if (showDisclaimer) { - const renderedMarkdown = disposables.add(this._markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); + const renderedMarkdown = disposables.add(this.#markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); this._elements.disclaimerLabel.appendChild(renderedMarkdown.element); } @@ -331,20 +353,20 @@ export class InlineChatWidget { } get chatWidget(): ChatWidget { - return this._chatWidget; + return this.#chatWidget; } saveState() { - this._chatWidget.saveState(); + this.#chatWidget.saveState(); } layout(widgetDim: Dimension) { const contentHeight = this.contentHeight; - this._isLayouting = true; + this.#isLayouting = true; try { this._doLayout(widgetDim); } finally { - this._isLayouting = false; + this.#isLayouting = false; if (this.contentHeight !== contentHeight) { this._onDidChangeHeight.fire(); @@ -361,7 +383,7 @@ export class InlineChatWidget { this._elements.root.style.height = `${dimension.height - extraHeight}px`; this._elements.root.style.width = `${dimension.width}px`; - this._chatWidget.layout( + this.#chatWidget.layout( dimension.height - statusHeight - extraHeight, dimension.width ); @@ -372,7 +394,7 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - chatWidgetContentHeight: this._chatWidget.contentHeight, + chatWidgetContentHeight: this.#chatWidget.contentHeight, statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; @@ -385,7 +407,7 @@ export class InlineChatWidget { // at least "maxWidgetHeight" high and at most the content height. let maxWidgetOutputHeight = 100; - for (const item of this._chatWidget.viewModel?.getItems() ?? []) { + for (const item of this.#chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { maxWidgetOutputHeight = 270; break; @@ -393,29 +415,29 @@ export class InlineChatWidget { } let value = this.contentHeight; - value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); + value -= this.#chatWidget.contentHeight; + value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight); return value; } protected _getExtraHeight(): number { - return this._options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); + return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); } get value(): string { - return this._chatWidget.getInput(); + return this.#chatWidget.getInput(); } set value(value: string) { - this._chatWidget.setInput(value); + this.#chatWidget.setInput(value); } selectAll() { - this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } set placeholder(value: string) { - this._chatWidget.setInputPlaceholder(value); + this.#chatWidget.setInputPlaceholder(value); } toggleStatus(show: boolean) { @@ -436,7 +458,7 @@ export class InlineChatWidget { } async getCodeBlockInfo(codeBlockIndex: number): Promise { - const { viewModel } = this._chatWidget; + const { viewModel } = this.#chatWidget; if (!viewModel) { return undefined; } @@ -483,18 +505,18 @@ export class InlineChatWidget { } get responseContent(): string | undefined { - const requests = this._chatWidget.viewModel?.model.getRequests(); + const requests = this.#chatWidget.viewModel?.model.getRequests(); return requests?.at(-1)?.response?.response.toString(); } getChatModel(): IChatModel | undefined { - return this._chatWidget.viewModel?.model; + return this.#chatWidget.viewModel?.model; } setChatModel(chatModel: IChatModel) { chatModel.inputModel.setState({ inputText: '', selections: [] }); - this._chatWidget.setModel(chatModel); + this.#chatWidget.setModel(chatModel); } updateInfo(message: string): void { @@ -533,8 +555,8 @@ export class InlineChatWidget { } reset() { - this._chatWidget.attachmentModel.clear(true); - this._chatWidget.saveState(); + this.#chatWidget.attachmentModel.clear(true); + this.#chatWidget.saveState(); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); @@ -547,7 +569,7 @@ export class InlineChatWidget { } focus() { - this._chatWidget.focusInput(); + this.#chatWidget.focusInput(); } hasFocus() { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 21113b9d0dea2..5f172c19672e8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -28,7 +28,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; export class InlineChatZoneWidget extends ZoneWidget { - private static readonly _options: IOptions = { + static readonly #options: IOptions = { showFrame: true, frameWidth: 1, // frameColor: 'var(--vscode-inlineChat-border)', @@ -43,9 +43,12 @@ export class InlineChatZoneWidget extends ZoneWidget { readonly widget: EditorBasedInlineChatWidget; - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - private notebookEditor?: INotebookEditor; + readonly #ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + #dimension?: Dimension; + #notebookEditor?: INotebookEditor; + + readonly #instaService: IInstantiationService; + #logService: ILogService; constructor( location: IChatWidgetLocationOptions, @@ -53,20 +56,22 @@ export class InlineChatZoneWidget extends ZoneWidget { editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor }, /** @deprecated should go away with inline2 */ clearDelegate: () => Promise, - @IInstantiationService private readonly _instaService: IInstantiationService, - @ILogService private _logService: ILogService, + @IInstantiationService instaService: IInstantiationService, + @ILogService logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super(editors.editor, InlineChatZoneWidget._options); - this.notebookEditor = editors.notebookEditor; + super(editors.editor, InlineChatZoneWidget.#options); + this.#instaService = instaService; + this.#logService = logService; + this.#notebookEditor = editors.notebookEditor; - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + this.#ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); this._disposables.add(toDisposable(() => { - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); })); - this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { + this.widget = this.#instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { statusMenuId: { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { @@ -105,14 +110,14 @@ export class InlineChatZoneWidget extends ZoneWidget { let revealFn: (() => void) | undefined; this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => { if (this.position) { - revealFn = this._createZoneAndScrollRestoreFn(this.position); + revealFn = this.#createZoneAndScrollRestoreFn(this.position); } })); this._disposables.add(this.widget.onDidChangeHeight(() => { if (this.position && !this._usesResizeHeight) { // only relayout when visible - revealFn ??= this._createZoneAndScrollRestoreFn(this.position); - const height = this._computeHeight(); + revealFn ??= this.#createZoneAndScrollRestoreFn(this.position); + const height = this.#computeHeight(); this._relayout(height.linesValue); revealFn?.(); revealFn = undefined; @@ -136,13 +141,13 @@ export class InlineChatZoneWidget extends ZoneWidget { // todo@jrieken listen ONLY when showing const updateCursorIsAboveContextKey = () => { if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); + this.#ctxCursorPosition.set('above'); } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); + this.#ctxCursorPosition.set('below'); } else { - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); } }; this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); @@ -159,19 +164,19 @@ export class InlineChatZoneWidget extends ZoneWidget { protected override _doLayout(heightInPixel: number): void { - this._updatePadding(); + this.#updatePadding(); const info = this.editor.getLayoutInfo(); const width = info.contentWidth - info.verticalScrollbarWidth; // width = Math.min(850, width); - this._dimension = new Dimension(width, heightInPixel); - this.widget.layout(this._dimension); + this.#dimension = new Dimension(width, heightInPixel); + this.widget.layout(this.#dimension); } - private _computeHeight(): { linesValue: number; pixelsValue: number } { + #computeHeight(): { linesValue: number; pixelsValue: number } { const chatContentHeight = this.widget.contentHeight; - const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height; + const editorHeight = this.#notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height; const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42)); const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight); @@ -192,25 +197,25 @@ export class InlineChatZoneWidget extends ZoneWidget { } protected override _onWidth(_widthInPixel: number): void { - if (this._dimension) { - this._doLayout(this._dimension.height); + if (this.#dimension) { + this._doLayout(this.#dimension.height); } } override show(position: Position): void { assertType(this.container); - this._updatePadding(); + this.#updatePadding(); - const revealZone = this._createZoneAndScrollRestoreFn(position); - super.show(position, this._computeHeight().linesValue); + const revealZone = this.#createZoneAndScrollRestoreFn(position); + super.show(position, this.#computeHeight().linesValue); this.widget.chatWidget.setVisible(true); this.widget.focus(); revealZone(); } - private _updatePadding() { + #updatePadding() { assertType(this.container); const info = this.editor.getLayoutInfo(); @@ -226,12 +231,12 @@ export class InlineChatZoneWidget extends ZoneWidget { } override updatePositionAndHeight(position: Position): void { - const revealZone = this._createZoneAndScrollRestoreFn(position); - super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined); + const revealZone = this.#createZoneAndScrollRestoreFn(position); + super.updatePositionAndHeight(position, !this._usesResizeHeight ? this.#computeHeight().linesValue : undefined); revealZone(); } - private _createZoneAndScrollRestoreFn(position: Position): () => void { + #createZoneAndScrollRestoreFn(position: Position): () => void { const scrollState = StableEditorBottomScrollState.capture(this.editor); @@ -242,7 +247,7 @@ export class InlineChatZoneWidget extends ZoneWidget { const scrollTop = this.editor.getScrollTop(); const lineTop = this.editor.getTopForLineNumber(lineNumber); - const zoneTop = lineTop - this._computeHeight().pixelsValue; + const zoneTop = lineTop - this.#computeHeight().pixelsValue; const editorHeight = this.editor.getLayoutInfo().height; const lineBottom = this.editor.getBottomForLineNumber(lineNumber); @@ -257,7 +262,7 @@ export class InlineChatZoneWidget extends ZoneWidget { } if (newScrollTop < scrollTop || forceScrollTop) { - this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); + this.#logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } }; @@ -269,7 +274,7 @@ export class InlineChatZoneWidget extends ZoneWidget { override hide(): void { const scrollState = StableEditorBottomScrollState.capture(this.editor); - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); this.widget.chatWidget.setVisible(false); super.hide(); aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts b/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts index 2a6ea9759da72..8e1573fdaed98 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts @@ -20,47 +20,49 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc export class TestWorkerService extends mock() implements IDisposable { - private readonly _store = new DisposableStore(); - private readonly _worker = this._store.add(new EditorWorker()); + readonly #store = new DisposableStore(); + readonly #worker = this.#store.add(new EditorWorker()); + readonly #modelService: IModelService; - constructor(@IModelService private readonly _modelService: IModelService) { + constructor(@IModelService modelService: IModelService) { super(); + this.#modelService = modelService; } dispose(): void { - this._store.dispose(); + this.#store.dispose(); } override async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean | undefined): Promise { return undefined; } override async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise { - await new Promise(resolve => disposableTimeout(() => resolve(), 0, this._store)); - if (this._store.isDisposed) { + await new Promise(resolve => disposableTimeout(() => resolve(), 0, this.#store)); + if (this.#store.isDisposed) { return null; } - const originalModel = this._modelService.getModel(original); - const modifiedModel = this._modelService.getModel(modified); + const originalModel = this.#modelService.getModel(original); + const modifiedModel = this.#modelService.getModel(modified); assertType(originalModel); assertType(modifiedModel); - this._worker.$acceptNewModel({ + this.#worker.$acceptNewModel({ url: originalModel.uri.toString(), versionId: originalModel.getVersionId(), lines: originalModel.getLinesContent(), EOL: originalModel.getEOL(), }); - this._worker.$acceptNewModel({ + this.#worker.$acceptNewModel({ url: modifiedModel.uri.toString(), versionId: modifiedModel.getVersionId(), lines: modifiedModel.getLinesContent(), EOL: modifiedModel.getEOL(), }); - const result = await this._worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm); + const result = await this.#worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm); if (!result) { return result; } From dc94486ab96338ab4506bb0a413889e9342a605f Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:29:58 -0800 Subject: [PATCH 13/14] Browser: context menus (#299013) * Browser: context menus * feedback * feedback * auxiliary fix --- .../browserView/common/browserView.ts | 29 +++ .../browserView/electron-main/browserView.ts | 38 ++- .../electron-main/browserViewMainService.ts | 242 ++++++++++++++---- .../electron-browser/browserFindWidget.ts | 7 +- .../electron-browser/browserViewActions.ts | 40 +-- .../browserViewWorkbenchService.ts | 32 ++- 6 files changed, 306 insertions(+), 82 deletions(-) diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index ec51dec6b8f04..fe4f22d727dcd 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -7,6 +7,29 @@ import { Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { URI } from '../../../base/common/uri.js'; +const commandPrefix = 'workbench.action.browser'; +export enum BrowserViewCommandId { + Open = `${commandPrefix}.open`, + NewTab = `${commandPrefix}.newTab`, + GoBack = `${commandPrefix}.goBack`, + GoForward = `${commandPrefix}.goForward`, + Reload = `${commandPrefix}.reload`, + HardReload = `${commandPrefix}.hardReload`, + FocusUrlInput = `${commandPrefix}.focusUrlInput`, + AddElementToChat = `${commandPrefix}.addElementToChat`, + AddConsoleLogsToChat = `${commandPrefix}.addConsoleLogsToChat`, + ToggleDevTools = `${commandPrefix}.toggleDevTools`, + OpenExternal = `${commandPrefix}.openExternal`, + ClearGlobalStorage = `${commandPrefix}.clearGlobalStorage`, + ClearWorkspaceStorage = `${commandPrefix}.clearWorkspaceStorage`, + ClearEphemeralStorage = `${commandPrefix}.clearEphemeralStorage`, + OpenSettings = `${commandPrefix}.openSettings`, + ShowFind = `${commandPrefix}.showFind`, + HideFind = `${commandPrefix}.hideFind`, + FindNext = `${commandPrefix}.findNext`, + FindPrevious = `${commandPrefix}.findPrevious`, +} + export interface IBrowserViewBounds { windowId: number; x: number; @@ -287,4 +310,10 @@ export interface IBrowserViewService { * @param id The browser view identifier */ clearStorage(id: string): Promise; + + /** + * Update the keybinding accelerators used in browser view context menus. + * @param keybindings A map of command ID to accelerator label + */ + updateKeybindings(keybindings: { [commandId: string]: string }): Promise; } diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 10fb6b62afaff..bcac609f8e6ec 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -11,15 +11,16 @@ import { VSBuffer } from '../../../base/common/buffer.js'; import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; -import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; +import { ICodeWindow } from '../../window/electron-main/window.js'; import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js'; -import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { isMacintosh } from '../../../base/common/platform.js'; import { BrowserViewUri } from '../common/browserViewUri.js'; import { BrowserViewDebugger } from './browserViewDebugger.js'; import { ILogService } from '../../log/common/log.js'; import { ICDPTarget, ICDPConnection, CDPTargetInfo } from '../common/cdp/types.js'; import { BrowserSession } from './browserSession.js'; +import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; +import { hasKey } from '../../../base/common/types.js'; /** Key combinations that are used in system-level shortcuts. */ const nativeShortcuts = new Set([ @@ -47,7 +48,7 @@ export class BrowserView extends Disposable implements ICDPTarget { private _lastUserGestureTimestamp: number = -Infinity; private _debugger: BrowserViewDebugger; - private _window: IBaseWindow | undefined; + private _window: ICodeWindow | IAuxiliaryWindow | undefined; private _isSendingKeyEvent = false; private _isDisposed = false; @@ -88,6 +89,7 @@ export class BrowserView extends Disposable implements ICDPTarget { public readonly id: string, public readonly session: BrowserSession, createChildView: (options?: Electron.WebContentsViewConstructorOptions) => BrowserView, + openContextMenu: (view: BrowserView, params: Electron.ContextMenuParams) => void, options: Electron.WebContentsViewConstructorOptions | undefined, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService, @@ -150,6 +152,10 @@ export class BrowserView extends Disposable implements ICDPTarget { }; }); + this._view.webContents.on('context-menu', (_event, params) => { + openContextMenu(this, params); + }); + this._view.webContents.on('destroyed', () => { this.dispose(); }); @@ -376,7 +382,7 @@ export class BrowserView extends Disposable implements ICDPTarget { */ layout(bounds: IBrowserViewBounds): void { if (this._window?.win?.id !== bounds.windowId) { - const newWindow = this.windowById(bounds.windowId); + const newWindow = this._windowById(bounds.windowId); if (newWindow) { this._window?.win?.contentView.removeChildView(this._view); this._window = newWindow; @@ -575,6 +581,22 @@ export class BrowserView extends Disposable implements ICDPTarget { return this._view; } + /** + * Get the hosting Electron window for this view, if any. + * This can be an auxiliary window, depending on where the view is currently hosted. + */ + getElectronWindow(): Electron.BrowserWindow | undefined { + return this._window?.win ?? undefined; + } + + /** + * Get the main code window hosting this browser view, if any. This is used for routing commands from the browser view to the correct window. + * If the browser view is hosted in an auxiliary window, this will return the parent code window of that auxiliary window. + */ + getTopCodeWindow(): ICodeWindow | undefined { + return this._window && hasKey(this._window, { parentId: true }) ? this._codeWindowById(this._window.parentId) : undefined; + } + // ============ ICDPTarget implementation ============ /** @@ -662,11 +684,11 @@ export class BrowserView extends Disposable implements ICDPTarget { return true; } - private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { - return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId); + private _windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { + return this._codeWindowById(windowId) ?? this._auxiliaryWindowById(windowId); } - private codeWindowById(windowId: number | undefined): ICodeWindow | undefined { + private _codeWindowById(windowId: number | undefined): ICodeWindow | undefined { if (typeof windowId !== 'number') { return undefined; } @@ -674,7 +696,7 @@ export class BrowserView extends Disposable implements ICDPTarget { return this.windowsMainService.getWindowById(windowId); } - private auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined { + private _auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined { if (typeof windowId !== 'number') { return undefined; } diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index c2a4e3aeefe64..313b3f416dee4 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -6,7 +6,8 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IBrowserViewBounds, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewService, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions } from '../common/browserView.js'; +import { IBrowserViewBounds, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewService, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, BrowserViewCommandId } from '../common/browserView.js'; +import { clipboard, Menu, MenuItem } from 'electron'; import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget } from '../common/cdp/types.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; @@ -17,9 +18,13 @@ import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { BrowserSession } from './browserSession.js'; import { IProductService } from '../../product/common/productService.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; -import { logBrowserOpen } from '../common/browserViewTelemetry.js'; +import { IntegratedBrowserOpenSource, logBrowserOpen } from '../common/browserViewTelemetry.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import { localize } from '../../../nls.js'; +import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; +import { ITextEditorOptions } from '../../editor/common/editor.js'; +import { htmlAttributeEncodeValue } from '../../../base/common/strings.js'; export const IBrowserViewMainService = createDecorator('browserViewMainService'); @@ -41,6 +46,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa } private readonly browserViews = this._register(new DisposableMap()); + private _keybindings: { [commandId: string]: string } = Object.create(null); // ICDPBrowserTarget events private readonly _onTargetCreated = this._register(new Emitter()); @@ -54,38 +60,12 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa @IInstantiationService private readonly instantiationService: IInstantiationService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IProductService private readonly productService: IProductService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService ) { super(); } - /** - * Create a browser view backed by the given {@link BrowserSession}. - */ - private createBrowserView(id: string, browserSession: BrowserSession, options?: Electron.WebContentsViewConstructorOptions): BrowserView { - if (this.browserViews.has(id)) { - throw new Error(`Browser view with id ${id} already exists`); - } - - const view = this.instantiationService.createInstance( - BrowserView, - id, - browserSession, - // Recursive factory for nested windows (child views share the same session) - (childOptions) => this.createBrowserView(generateUuid(), browserSession, childOptions), - options - ); - this.browserViews.set(id, view); - - this._onTargetCreated.fire(view); - Event.once(view.onDidClose)(() => { - this._onTargetDestroyed.fire(view); - this.browserViews.deleteAndDispose(id); - }); - - return view; - } - async getOrCreateBrowserView(id: string, scope: BrowserViewStorageScope, workspaceId?: string): Promise { if (this.browserViews.has(id)) { // Note: scope will be ignored if the view already exists. @@ -160,26 +140,14 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa } async createTarget(url: string, browserContextId?: string, windowId?: number): Promise { - const targetId = generateUuid(); - const browserSession = browserContextId && BrowserSession.get(browserContextId) || BrowserSession.getOrCreateEphemeral(targetId); - - // Create the browser view (fires onTargetCreated) - const view = this.createBrowserView(targetId, browserSession); - - logBrowserOpen(this.telemetryService, 'cdpCreated'); + const browserSession = browserContextId ? BrowserSession.get(browserContextId) : undefined; - const window = windowId !== undefined ? this.windowsMainService.getWindowById(windowId) : this.windowsMainService.getFocusedWindow(); - if (!window) { - throw new Error(`Window ${windowId} not found`); - } - - // Request the workbench to open the editor - window.sendWhenReady('vscode:runAction', CancellationToken.None, { - id: '_workbench.open', - args: [BrowserViewUri.forUrl(url, targetId), [undefined, { preserveFocus: true }], undefined] + return this.openNew(url, { + session: browserSession, + windowId, + editorOptions: { preserveFocus: true }, + source: 'cdpCreated' }); - - return view; } async activateTarget(target: ICDPTarget): Promise { @@ -372,4 +340,182 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa ); await browserSession.electronSession.clearData(); } + + async updateKeybindings(keybindings: { [commandId: string]: string }): Promise { + this._keybindings = keybindings; + } + + /** + * Create a browser view backed by the given {@link BrowserSession}. + */ + private createBrowserView(id: string, browserSession: BrowserSession, options?: Electron.WebContentsViewConstructorOptions): BrowserView { + if (this.browserViews.has(id)) { + throw new Error(`Browser view with id ${id} already exists`); + } + + const view = this.instantiationService.createInstance( + BrowserView, + id, + browserSession, + // Recursive factory for nested windows (child views share the same session) + (childOptions) => this.createBrowserView(generateUuid(), browserSession, childOptions), + (v, params) => this.showContextMenu(v, params), + options + ); + this.browserViews.set(id, view); + + this._onTargetCreated.fire(view); + Event.once(view.onDidClose)(() => { + this._onTargetDestroyed.fire(view); + this.browserViews.deleteAndDispose(id); + }); + + return view; + } + + private async openNew( + url: string, + { + session, + windowId, + editorOptions, + source + }: { + session: BrowserSession | undefined; + windowId: number | undefined; + editorOptions: ITextEditorOptions; + source: IntegratedBrowserOpenSource; + } + ): Promise { + const targetId = generateUuid(); + const view = this.createBrowserView(targetId, session || BrowserSession.getOrCreateEphemeral(targetId)); + + const window = windowId !== undefined ? this.windowsMainService.getWindowById(windowId) : this.windowsMainService.getFocusedWindow(); + if (!window) { + throw new Error(`Window ${windowId} not found`); + } + + + logBrowserOpen(this.telemetryService, source); + + // Request the workbench to open the editor + window.sendWhenReady('vscode:runAction', CancellationToken.None, { + id: '_workbench.open', + args: [BrowserViewUri.forUrl(url, targetId), [undefined, editorOptions], undefined] + }); + + return view; + } + + private showContextMenu(view: BrowserView, params: Electron.ContextMenuParams): void { + const win = view.getElectronWindow(); + if (!win) { + return; + } + const webContents = view.webContents; + if (webContents.isDestroyed()) { + return; + } + const menu = new Menu(); + + if (params.linkURL) { + menu.append(new MenuItem({ + label: localize('browser.contextMenu.openLinkInNewTab', 'Open Link in New Tab'), + click: () => { + void this.openNew(params.linkURL, { + session: view.session, + windowId: view.getTopCodeWindow()?.id, + editorOptions: { preserveFocus: true, inactive: true }, + source: 'browserLinkBackground' + }); + } + })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.openLinkInExternalBrowser', 'Open Link in External Browser'), + click: () => { void this.nativeHostMainService.openExternal(undefined, params.linkURL); } + })); + menu.append(new MenuItem({ type: 'separator' })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.copyLink', 'Copy Link'), + click: () => { + clipboard.write({ + text: params.linkURL, + html: `${htmlAttributeEncodeValue(params.linkText || params.linkURL)}` + }); + } + })); + } + + if (params.hasImageContents && params.srcURL) { + if (menu.items.length > 0) { + menu.append(new MenuItem({ type: 'separator' })); + } + menu.append(new MenuItem({ + label: localize('browser.contextMenu.openImageInNewTab', 'Open Image in New Tab'), + click: () => { + void this.openNew(params.srcURL!, { + session: view.session, + windowId: view.getTopCodeWindow()?.id, + editorOptions: { preserveFocus: true, inactive: true }, + source: 'browserLinkBackground' + }); + } + })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.copyImage', 'Copy Image'), + click: () => { view.webContents.copyImageAt(params.x, params.y); } + })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.copyImageUrl', 'Copy Image URL'), + click: () => { clipboard.writeText(params.srcURL!); } + })); + } + + if (params.isEditable) { + menu.append(new MenuItem({ role: 'cut', enabled: params.editFlags.canCut })); + menu.append(new MenuItem({ role: 'copy', enabled: params.editFlags.canCopy })); + menu.append(new MenuItem({ role: 'paste', enabled: params.editFlags.canPaste })); + menu.append(new MenuItem({ role: 'pasteAndMatchStyle', enabled: params.editFlags.canPaste })); + menu.append(new MenuItem({ role: 'selectAll', enabled: params.editFlags.canSelectAll })); + } else if (params.selectionText) { + menu.append(new MenuItem({ role: 'copy' })); + } + + // Add navigation items as defaults + if (menu.items.length === 0) { + if (webContents.navigationHistory.canGoBack()) { + menu.append(new MenuItem({ + label: localize('browser.contextMenu.back', 'Back'), + accelerator: this._keybindings[BrowserViewCommandId.GoBack], + click: () => webContents.navigationHistory.goBack() + })); + } + if (webContents.navigationHistory.canGoForward()) { + menu.append(new MenuItem({ + label: localize('browser.contextMenu.forward', 'Forward'), + accelerator: this._keybindings[BrowserViewCommandId.GoForward], + click: () => webContents.navigationHistory.goForward() + })); + } + menu.append(new MenuItem({ + label: localize('browser.contextMenu.reload', 'Reload'), + accelerator: this._keybindings[BrowserViewCommandId.Reload], + click: () => webContents.reload() + })); + } + + menu.append(new MenuItem({ type: 'separator' })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.inspect', 'Inspect'), + click: () => webContents.inspectElement(params.x, params.y) + })); + + const viewBounds = view.getWebContentsView().getBounds(); + menu.popup({ + window: win, + x: viewBounds.x + params.x, + y: viewBounds.y + params.y, + sourceType: params.menuSourceType + }); + } } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts index aec2add429b94..a1ad809a6f386 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts @@ -11,6 +11,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IBrowserViewModel } from '../common/browserView.js'; +import { BrowserViewCommandId } from '../../../../platform/browserView/common/browserView.js'; import { localize } from '../../../../nls.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -45,9 +46,9 @@ export class BrowserFindWidget extends SimpleFindWidget { showResultCount: true, enableSash: true, initialWidth: 350, - previousMatchActionId: 'workbench.action.browser.findPrevious', - nextMatchActionId: 'workbench.action.browser.findNext', - closeWidgetActionId: 'workbench.action.browser.hideFind' + previousMatchActionId: BrowserViewCommandId.FindPrevious, + nextMatchActionId: BrowserViewCommandId.FindNext, + closeWidgetActionId: BrowserViewCommandId.HideFind }, contextViewService, contextKeyService, hoverService, keybindingService, configurationService, accessibilityService); this._findWidgetVisible = CONTEXT_BROWSER_FIND_WIDGET_VISIBLE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index b32e1c70ea2b1..5fd326273c466 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -14,7 +14,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_ERROR, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js'; import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js'; import { IBrowserViewWorkbenchService } from '../common/browserView.js'; -import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; +import { BrowserViewCommandId, BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; @@ -37,7 +37,7 @@ interface IOpenBrowserOptions { class OpenIntegratedBrowserAction extends Action2 { constructor() { super({ - id: 'workbench.action.browser.open', + id: BrowserViewCommandId.Open, title: localize2('browser.openAction', "Open Integrated Browser"), category: BrowserCategory, f1: true @@ -67,7 +67,7 @@ class OpenIntegratedBrowserAction extends Action2 { class NewTabAction extends Action2 { constructor() { super({ - id: 'workbench.action.browser.newTab', + id: BrowserViewCommandId.NewTab, title: localize2('browser.newTabAction', "New Tab"), category: BrowserCategory, f1: true, @@ -97,7 +97,7 @@ class NewTabAction extends Action2 { } class GoBackAction extends Action2 { - static readonly ID = 'workbench.action.browser.goBack'; + static readonly ID = BrowserViewCommandId.GoBack; constructor() { super({ @@ -129,7 +129,7 @@ class GoBackAction extends Action2 { } class GoForwardAction extends Action2 { - static readonly ID = 'workbench.action.browser.goForward'; + static readonly ID = BrowserViewCommandId.GoForward; constructor() { super({ @@ -161,7 +161,7 @@ class GoForwardAction extends Action2 { } class ReloadAction extends Action2 { - static readonly ID = 'workbench.action.browser.reload'; + static readonly ID = BrowserViewCommandId.Reload; constructor() { super({ @@ -199,7 +199,7 @@ class ReloadAction extends Action2 { } class HardReloadAction extends Action2 { - static readonly ID = 'workbench.action.browser.hardReload'; + static readonly ID = BrowserViewCommandId.HardReload; constructor() { super({ @@ -227,7 +227,7 @@ class HardReloadAction extends Action2 { } class FocusUrlInputAction extends Action2 { - static readonly ID = 'workbench.action.browser.focusUrlInput'; + static readonly ID = BrowserViewCommandId.FocusUrlInput; constructor() { super({ @@ -251,7 +251,7 @@ class FocusUrlInputAction extends Action2 { } class AddElementToChatAction extends Action2 { - static readonly ID = 'workbench.action.browser.addElementToChat'; + static readonly ID = BrowserViewCommandId.AddElementToChat; constructor() { const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true)); @@ -288,7 +288,7 @@ class AddElementToChatAction extends Action2 { } class AddConsoleLogsToChatAction extends Action2 { - static readonly ID = 'workbench.action.browser.addConsoleLogsToChat'; + static readonly ID = BrowserViewCommandId.AddConsoleLogsToChat; constructor() { const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true)); @@ -316,7 +316,7 @@ class AddConsoleLogsToChatAction extends Action2 { } class ToggleDevToolsAction extends Action2 { - static readonly ID = 'workbench.action.browser.toggleDevTools'; + static readonly ID = BrowserViewCommandId.ToggleDevTools; constructor() { super({ @@ -347,7 +347,7 @@ class ToggleDevToolsAction extends Action2 { } class OpenInExternalBrowserAction extends Action2 { - static readonly ID = 'workbench.action.browser.openExternal'; + static readonly ID = BrowserViewCommandId.OpenExternal; constructor() { super({ @@ -383,7 +383,7 @@ class OpenInExternalBrowserAction extends Action2 { } class ClearGlobalBrowserStorageAction extends Action2 { - static readonly ID = 'workbench.action.browser.clearGlobalStorage'; + static readonly ID = BrowserViewCommandId.ClearGlobalStorage; constructor() { super({ @@ -408,7 +408,7 @@ class ClearGlobalBrowserStorageAction extends Action2 { } class ClearWorkspaceBrowserStorageAction extends Action2 { - static readonly ID = 'workbench.action.browser.clearWorkspaceStorage'; + static readonly ID = BrowserViewCommandId.ClearWorkspaceStorage; constructor() { super({ @@ -433,7 +433,7 @@ class ClearWorkspaceBrowserStorageAction extends Action2 { } class ClearEphemeralBrowserStorageAction extends Action2 { - static readonly ID = 'workbench.action.browser.clearEphemeralStorage'; + static readonly ID = BrowserViewCommandId.ClearEphemeralStorage; constructor() { super({ @@ -460,7 +460,7 @@ class ClearEphemeralBrowserStorageAction extends Action2 { } class OpenBrowserSettingsAction extends Action2 { - static readonly ID = 'workbench.action.browser.openSettings'; + static readonly ID = BrowserViewCommandId.OpenSettings; constructor() { super({ @@ -486,7 +486,7 @@ class OpenBrowserSettingsAction extends Action2 { // Find actions class ShowBrowserFindAction extends Action2 { - static readonly ID = 'workbench.action.browser.showFind'; + static readonly ID = BrowserViewCommandId.ShowFind; constructor() { super({ @@ -515,7 +515,7 @@ class ShowBrowserFindAction extends Action2 { } class HideBrowserFindAction extends Action2 { - static readonly ID = 'workbench.action.browser.hideFind'; + static readonly ID = BrowserViewCommandId.HideFind; constructor() { super({ @@ -540,7 +540,7 @@ class HideBrowserFindAction extends Action2 { } class BrowserFindNextAction extends Action2 { - static readonly ID = 'workbench.action.browser.findNext'; + static readonly ID = BrowserViewCommandId.FindNext; constructor() { super({ @@ -571,7 +571,7 @@ class BrowserFindNextAction extends Action2 { } class BrowserFindPreviousAction extends Action2 { - static readonly ID = 'workbench.action.browser.findPrevious'; + static readonly ID = BrowserViewCommandId.FindPrevious; constructor() { super({ diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts index c60a295cd5a55..d189013b3adb7 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts @@ -3,15 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBrowserViewService, ipcBrowserViewChannelName } from '../../../../platform/browserView/common/browserView.js'; +import { BrowserViewCommandId, IBrowserViewService, ipcBrowserViewChannelName } from '../../../../platform/browserView/common/browserView.js'; import { IBrowserViewWorkbenchService, IBrowserViewModel, BrowserViewModel } from '../common/browserView.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { Event } from '../../../../base/common/event.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; -export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService { +/** Command IDs whose accelerators are shown in browser view context menus. */ +const browserViewContextMenuCommands = [ + BrowserViewCommandId.GoBack, + BrowserViewCommandId.GoForward, + BrowserViewCommandId.Reload, +]; + +export class BrowserViewWorkbenchService extends Disposable implements IBrowserViewWorkbenchService { declare readonly _serviceBrand: undefined; private readonly _browserViewService: IBrowserViewService; @@ -20,10 +29,15 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService constructor( @IMainProcessService mainProcessService: IMainProcessService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IKeybindingService private readonly keybindingService: IKeybindingService ) { + super(); const channel = mainProcessService.getChannel(ipcBrowserViewChannelName); this._browserViewService = ProxyChannel.toService(channel); + + this.sendKeybindings(); + this._register(this.keybindingService.onDidUpdateKeybindings(() => this.sendKeybindings())); } async getOrCreateBrowserViewModel(id: string): Promise { @@ -67,4 +81,16 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService return model; } + + private sendKeybindings(): void { + const keybindings: { [commandId: string]: string } = Object.create(null); + for (const commandId of browserViewContextMenuCommands) { + const binding = this.keybindingService.lookupKeybinding(commandId); + const accelerator = binding?.getElectronAccelerator(); + if (accelerator) { + keybindings[commandId] = accelerator; + } + } + void this._browserViewService.updateKeybindings(keybindings); + } } From 4b1876f38c477d1f8fe56c5ad8e1ad5d54ce773c Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 6 Mar 2026 16:52:29 +0100 Subject: [PATCH 14/14] fix tests --- src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 47c38236ad38a..50815341dbb9a 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -844,7 +844,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { const action = toAction({ id: optionGroup.id, label: optionGroup.name, run: () => { } }); const widget = this.instantiationService.createInstance( optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, - action, initialState, itemDelegate + action, initialState, itemDelegate, undefined ); this._toolbarPickerDisposables.add(widget);