From 2f061e9fac1b9153a623331013ac87fbdd1caa6a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Feb 2026 11:47:08 +0100 Subject: [PATCH 01/24] focus input when chat view pane becomes visible (#296241) --- src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 61fd3a1cd3dbb..e8842b2956c38 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -1189,6 +1189,9 @@ export class NewChatViewPane extends ViewPane { override setVisible(visible: boolean): void { super.setVisible(visible); this._widget?.setVisible(visible); + if (visible) { + this._widget?.focusInput(); + } } } From 8e87038b6553e36a88d020c59124b7ed4dceb2b6 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 19 Feb 2026 10:47:10 +0000 Subject: [PATCH 02/24] refactor(theme): update selectors to include 'vs' class for improved styling consistency --- extensions/theme-2026/themes/styles.css | 53 ++++++++++++++----------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index ec6b840db4708..343d812b62c92 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -30,7 +30,7 @@ /* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ /* Activity Bar */ -.monaco-workbench .part.activitybar { +.monaco-workbench.vs .part.activitybar { z-index: 50; position: relative; } @@ -44,55 +44,54 @@ } /* Sidebar */ -.monaco-workbench .part.sidebar { +.monaco-workbench.vs .part.sidebar { box-shadow: var(--shadow-md); z-index: 40; position: relative; } -.monaco-workbench.sidebar-right .part.sidebar { +.monaco-workbench.sidebar-right.vs .part.sidebar { box-shadow: var(--shadow-md); } -.monaco-workbench .part.auxiliarybar { +.monaco-workbench.vs .part.auxiliarybar { box-shadow: var(--shadow-md); z-index: 35; position: relative; } /* Ensure iframe containers in pane-body render above sidebar z-index */ -.monaco-workbench > div[data-keybinding-context] { +.monaco-workbench.vs > div[data-keybinding-context] { z-index: 50 !important; } /* Ensure in-editor pane iframes render below sidebar z-index */ -.monaco-workbench > div[data-parent-flow-to-element-id] { +.monaco-workbench.vs > div[data-parent-flow-to-element-id] { z-index: 0 !important; } /* Ensure webview containers render above sidebar z-index */ -.monaco-workbench .part.sidebar .webview, -.monaco-workbench .part.sidebar .webview-container, -.monaco-workbench .part.auxiliarybar .webview, -.monaco-workbench .part.auxiliarybar .webview-container { +.monaco-workbench.vs .part.sidebar .webview, +.monaco-workbench.vs .part.sidebar .webview-container, +.monaco-workbench.vs .part.auxiliarybar .webview, +.monaco-workbench.vs .part.auxiliarybar .webview-container { position: relative; z-index: 50; transform: translateZ(0); } /* Panel */ -.monaco-workbench .part.panel { +.monaco-workbench.vs .part.panel { box-shadow: var(--shadow-md); - /* z-index: 35; */ position: relative; } -.monaco-workbench.panel-position-left .part.panel { +.monaco-workbench.panel-position-left.vs .part.panel { box-shadow: var(--shadow-md); } -.monaco-workbench.panel-position-right .part.panel { +.monaco-workbench.panel-position-right.vs .part.panel { box-shadow: var(--shadow-md); } @@ -101,42 +100,45 @@ } /* Sashes - ensure they extend full height and are above other panels */ -.monaco-workbench .monaco-sash { +.monaco-workbench.vs .monaco-sash { z-index: 35; } -.monaco-workbench .monaco-sash.vertical { +.monaco-workbench.vs .monaco-sash.vertical { z-index: 40; } -.monaco-workbench .monaco-sash.vertical:nth-child(2) { +.monaco-workbench.vs .monaco-sash.vertical:nth-child(2) { z-index: 45; } -.monaco-workbench .monaco-sash.horizontal { +.monaco-workbench.vs .monaco-sash.horizontal { z-index: 35; } /* Editor */ -.monaco-workbench .part.editor { +.monaco-workbench.vs .part.editor { position: relative; } -.monaco-workbench .part.editor > .content .editor-group-container > .title { +.monaco-workbench.vs .part.editor > .content .editor-group-container > .title { box-shadow: none; position: relative; z-index: 10; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + border-radius: 0; + border-top: none !important; +} + +.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: inset var(--shadow-active-tab); position: relative; z-index: 5; - border-radius: 0; - border-top: none !important; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { +.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { box-shadow: var(--shadow-sm); } @@ -250,10 +252,13 @@ /* Chat Widget */ .monaco-workbench .interactive-session .chat-input-container { - box-shadow: inset var(--shadow-sm); border-radius: var(--radius-lg); } +.monaco-workbench.vs .interactive-session .chat-input-container { + box-shadow: inset var(--shadow-sm); +} + .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, .monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { border-radius: var(--radius-lg) var(--radius-lg) 0 0; From aac4555c2b753286f419be104d91845ce6023bda Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Feb 2026 11:47:43 +0100 Subject: [PATCH 03/24] sessions - tweaks to default settings for a better experience (#296242) --- .../configuration/browser/configuration.contribution.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 9a5d00df68597..bf5a13e8b0a19 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -24,6 +24,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'github.copilot.chat.claudeCode.enabled': true, 'github.copilot.chat.cli.branchSupport.enabled': true, 'github.copilot.chat.languageContext.typescript.enabled': true, + 'github.copilot.chat.cli.mcp.enabled': true, 'inlineChat.affordance': 'editor', 'inlineChat.renderMode': 'hover', @@ -33,7 +34,10 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.startupEditor': 'none', 'workbench.tips.enabled': false, 'workbench.layoutControl.type': 'toggles', - 'workbench.editor.allowOpenInModalEditor': false + 'workbench.editor.allowOpenInModalEditor': false, + 'window.menuStyle': 'custom', + + 'terminal.integrated.initialHint': false }, donotCache: true }]); From d194983712c5c24ba4b322f65a3b0bf7c57b11c4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Feb 2026 11:48:02 +0100 Subject: [PATCH 04/24] Revert "sessions - move actions down out of title into view" (#296247) * Revert "sessions - move actions down out of title into view (#296107)" This reverts commit da59fee119c865d884434574331afd4b7541de4c. * . --- .../browser/media/sidebarActionButton.css | 3 +- .../browser/media/sessionsViewPane.css | 2 + .../sessions/browser/sessionsViewPane.ts | 37 +++++++------------ 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/vs/sessions/browser/media/sidebarActionButton.css b/src/vs/sessions/browser/media/sidebarActionButton.css index 3d9b3a393fa2f..f170a6ed4fd0f 100644 --- a/src/vs/sessions/browser/media/sidebarActionButton.css +++ b/src/vs/sessions/browser/media/sidebarActionButton.css @@ -19,8 +19,7 @@ border: none; padding: 4px 8px; margin: 0; - font-size: 11px; - font-weight: 500; + font-size: 12px; height: auto; white-space: nowrap; overflow: hidden; diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css index b054dfd8f0d9b..1666be701275e 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css @@ -46,6 +46,8 @@ display: flex; align-items: center; gap: 4px; + padding-top: 10px; + padding-right: 12px; -webkit-user-select: none; user-select: none; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 81f0aa6849afb..a1e45eb3d0cb6 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -11,7 +11,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { autorun } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -44,7 +44,6 @@ import { getCustomizationTotalCount } from './customizationCounts.js'; const $ = DOM.$; export const SessionsViewId = 'agentic.workbench.view.sessionsView'; const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu'); -const SessionsViewHeaderMenu = new MenuId('AgentSessionsViewHeaderMenu'); const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed'; @@ -88,18 +87,15 @@ export class AgenticSessionsViewPane extends ViewPane { private createControls(parent: HTMLElement): void { const sessionsContainer = DOM.append(parent, $('.agent-sessions-container')); + // Sessions Filter (actions go to view title bar via menu registration) + const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { + filterMenuId: SessionsViewFilterSubMenu, + groupResults: () => AgentSessionsGrouping.Date + })); + // Sessions section (top, fills available space) const sessionsSection = DOM.append(sessionsContainer, $('.agent-sessions-section')); - // Sessions header with title and toolbar actions - const sessionsHeader = DOM.append(sessionsSection, $('.agent-sessions-header')); - const headerText = DOM.append(sessionsHeader, $('span')); - headerText.textContent = localize('sessions', "SESSIONS"); - const headerToolbarContainer = DOM.append(sessionsHeader, $('.agent-sessions-header-toolbar')); - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, headerToolbarContainer, SessionsViewHeaderMenu, { - menuOptions: { shouldForwardArgs: true }, - })); - // Sessions content container const sessionsContent = DOM.append(sessionsSection, $('.agent-sessions-content')); @@ -116,12 +112,6 @@ export class AgenticSessionsViewPane extends ViewPane { keybindingHint.textContent = keybinding.getLabel() ?? ''; } - // Sessions filter: contributes filter actions via SessionsViewFilterSubMenu; actions are rendered in the sessions header toolbar (SessionsViewHeaderMenu) - const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { - filterMenuId: SessionsViewFilterSubMenu, - groupResults: () => AgentSessionsGrouping.Date - })); - // Sessions Control this.sessionsControlContainer = DOM.append(sessionsContent, $('.agent-sessions-control-container')); const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { @@ -297,12 +287,13 @@ KeybindingsRegistry.registerKeybindingRule({ primary: KeyMod.CtrlCmd | KeyCode.KeyN, }); -MenuRegistry.appendMenuItem(SessionsViewHeaderMenu, { +MenuRegistry.appendMenuItem(MenuId.ViewTitle, { submenu: SessionsViewFilterSubMenu, title: localize2('filterAgentSessions', "Filter Agent Sessions"), group: 'navigation', order: 3, icon: Codicon.filter, + when: ContextKeyExpr.equals('view', SessionsViewId) } satisfies ISubmenuItem); registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { @@ -311,11 +302,8 @@ registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { id: 'sessionsView.refresh', title: localize2('refresh', "Refresh Agent Sessions"), icon: Codicon.refresh, - menu: [{ - id: SessionsViewHeaderMenu, - group: 'navigation', - order: 1, - }], + f1: true, + category: localize2('sessionsViewCategory', "Agent Sessions"), }); } override run(accessor: ServicesAccessor) { @@ -333,9 +321,10 @@ registerAction2(class FindAgentSessionInViewerAction extends Action2 { title: localize2('find', "Find Agent Session"), icon: Codicon.search, menu: [{ - id: SessionsViewHeaderMenu, + id: MenuId.ViewTitle, group: 'navigation', order: 2, + when: ContextKeyExpr.equals('view', SessionsViewId), }] }); } From db6251a1125b32a95f3379df16d3e9b04a62743e Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 19 Feb 2026 12:06:54 +0100 Subject: [PATCH 05/24] fix https://github.com/microsoft/vscode/issues/296232 --- .../widget/input/modePickerActionItem.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index b429dfde84a39..da98033c79fae 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -214,11 +214,11 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const agentMode = modes.builtin.find(mode => mode.id === ChatMode.Agent.id); const otherBuiltinModes = modes.builtin.filter(mode => { - return mode.id !== ChatMode.Agent.id && shouldShowBuiltInMode(mode, assignments.get()); + return mode.id !== ChatMode.Agent.id && shouldShowBuiltInMode(mode, assignments.get(), agentModeDisabledViaPolicy); }); const filteredCustomModes = modes.custom.filter(mode => { if (isModeConsideredBuiltIn(mode, this._productService)) { - return shouldShowBuiltInMode(mode, assignments.get()); + return shouldShowBuiltInMode(mode, assignments.get(), agentModeDisabledViaPolicy); } return true; }); @@ -335,19 +335,22 @@ function isModeConsideredBuiltIn(mode: IChatMode, productService: IProductServic return !isOrganizationPromptFile(modeUri, mode.source.extensionId, productService); } -function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: boolean }): boolean { - // The built-in "Edit" mode is deprecated, but still supported for older conversations. - if (mode.id === ChatMode.Edit.id) { - return false; +function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: boolean }, agentModeDisabledViaPolicy: boolean): boolean { + // The built-in "Edit" mode is deprecated, but still supported for older conversations and agent disablement. + if (mode.id === ChatMode.Edit.id || mode.name.get().toLowerCase() === 'edit') { + if (mode.id === ChatMode.Edit.id) { + return agentModeDisabledViaPolicy; + } + return true; } - // The "Ask" mode is a special case - we want to show either the old or new version based on the assignment, but not both + // The "Ask" mode is a special case - we want to show either the old or new version based on the assignment or agent disablement, but not both // We still support the old "Ask" mode for conversations that already use it. - if (mode.id === ChatMode.Ask.id) { - return assignments.showOldAskMode; - } - if (mode.name.get().toLowerCase() === 'ask') { - return !assignments.showOldAskMode; + if (mode.id === ChatMode.Ask.id || mode.name.get().toLowerCase() === 'ask') { + if (mode.id === ChatMode.Ask.id) { + return assignments.showOldAskMode || agentModeDisabledViaPolicy; + } + return true; } return true; From edf78b572b27977ef585a0893dddad6a5d8b816b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 19 Feb 2026 11:12:29 +0000 Subject: [PATCH 06/24] style: update quick input list and settings count widget for improved visibility --- extensions/theme-2026/themes/styles.css | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index ec6b840db4708..857eca22974e2 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -211,22 +211,20 @@ display: flex; align-items: center; font-size: 11px; - padding: 0 4px 1px 4px; + padding: 0 4px; border-radius: var(--vscode-cornerRadius-small) !important; - background: color-mix(in srgb, var(--vscode-badge-background) 70%, transparent) !important; - color: var(--vscode-badge-foreground) !important; + background: transparent !important; + color: var(--vscode-descriptionForeground) !important; + border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; margin-right: 8px; } -.monaco-workbench.vs-dark .quick-input-list .quick-input-list-entry .quick-input-list-separator { - background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important; -} - .monaco-workbench .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator, .monaco-workbench .monaco-list-row.selected .quick-input-list-entry .quick-input-list-separator, .monaco-workbench .monaco-list-row:hover .quick-input-list-entry .quick-input-list-separator { background: transparent !important; color: inherit !important; + border: none; padding: 0; } @@ -448,11 +446,9 @@ .monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--vscode-badge-background) 70%, transparent) !important; -} - -.monaco-workbench.vs-dark .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { - background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important; + background: transparent !important; + color: var(--vscode-descriptionForeground) !important; + border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } /* Welcome Tiles */ From 1b91ab65e6ad88adfbada3cfeaf4bd4e2e196d77 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 19 Feb 2026 11:15:37 +0000 Subject: [PATCH 07/24] style: enforce important flag on quick input list separator border for consistency --- extensions/theme-2026/themes/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 857eca22974e2..febeedf6646e6 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -224,7 +224,7 @@ .monaco-workbench .monaco-list-row:hover .quick-input-list-entry .quick-input-list-separator { background: transparent !important; color: inherit !important; - border: none; + border: none !important; padding: 0; } From 1a9c882dd3510ffbce35dd72851f04ddca09fb1b Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 19 Feb 2026 12:16:43 +0100 Subject: [PATCH 08/24] fix bug --- .../chat/browser/widget/input/modePickerActionItem.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index da98033c79fae..8cc57f3238fd4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -340,8 +340,9 @@ function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: b if (mode.id === ChatMode.Edit.id || mode.name.get().toLowerCase() === 'edit') { if (mode.id === ChatMode.Edit.id) { return agentModeDisabledViaPolicy; + } else { + return !agentModeDisabledViaPolicy; } - return true; } // The "Ask" mode is a special case - we want to show either the old or new version based on the assignment or agent disablement, but not both @@ -349,8 +350,9 @@ function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: b if (mode.id === ChatMode.Ask.id || mode.name.get().toLowerCase() === 'ask') { if (mode.id === ChatMode.Ask.id) { return assignments.showOldAskMode || agentModeDisabledViaPolicy; + } else { + return !(assignments.showOldAskMode || agentModeDisabledViaPolicy); } - return true; } return true; From 91cafa504f99f68de0d39497b57ab143266fa18f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Feb 2026 12:46:34 +0100 Subject: [PATCH 09/24] extensions - stop opening them on installation (#296251) refactor: simplify installation alert messages and remove unnecessary editor opening --- .../contrib/extensions/browser/extensionsActions.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index d1dacd0dc6184..05ecf2338ad64 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -585,9 +585,7 @@ export class InstallAction extends ExtensionAction { } } - this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: this.options.installPreReleaseVersion }); - - alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); + alert(localize('installExtensionStart', "Installing extension {0} started.", this.extension.displayName)); /* __GDPR__ "extensions:action:install" : { @@ -824,8 +822,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { if (!this.server) { return; } - this.extensionsWorkbenchService.open(this.extension); - alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); + alert(localize('installExtensionStart', "Installing extension {0} started.", this.extension.displayName)); return this.extensionsWorkbenchService.installInServer(this.extension, this.server); } @@ -1556,7 +1553,6 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { if (!this.extension) { return; } - this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: !this.extension.preRelease }); await this.extensionsWorkbenchService.togglePreRelease(this.extension); } } From b9eb51b0f24d6c6167a2a215675963de764e8838 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 19 Feb 2026 22:48:38 +1100 Subject: [PATCH 10/24] Support display commands for terminal commands in background agents (#296261) --- src/vs/workbench/api/common/extHostTypeConverters.ts | 5 +++++ .../chatTerminalToolProgressPart.ts | 2 +- .../vscode.proposed.chatParticipantAdditions.d.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 33aeea43820a5..d565aa7634c67 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2987,8 +2987,13 @@ export namespace ChatToolInvocationPart { language: data.language }; } else if ('commandLine' in data && 'language' in data) { + const presentationOverrides = data.presentationOverrides && typeof data.presentationOverrides.commandLine === 'string' ? { + commandLine: data.presentationOverrides.commandLine, + language: data.presentationOverrides.language + } : undefined; const result: IChatTerminalToolInvocationData = { kind: 'terminal', + presentationOverrides, commandLine: data.commandLine, language: data.language, terminalCommandOutput: typeof data.output?.text === 'string' ? { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 29d0f93534ec1..1cc7ae425003e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -366,7 +366,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart if (terminalToolsInThinking && !requiresConfirmation) { this._isInThinkingContainer = true; - this.domNode = this._createCollapsibleWrapper(progressPart.domNode, command, toolInvocation, context); + this.domNode = this._createCollapsibleWrapper(progressPart.domNode, displayCommand, toolInvocation, context); } else { this.domNode = progressPart.domNode; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index b23503fa2328f..f122a55550a83 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -223,6 +223,16 @@ declare module 'vscode' { }; language: string; + /** + * Overrides for how the command is presented in the UI. + * For example, when a `cd && ` prefix is detected, + * the presentation can show only the actual command. + */ + presentationOverrides?: { + commandLine: string; + language?: string; + }; + /** * Terminal command output. Displayed when the terminal is no longer available. */ From 09822475f6c12d0c80e79b0e2e7f6221ee3c6168 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:57:07 +0100 Subject: [PATCH 11/24] Add when clause to skill contribution (#296049) * Add when clause to skill contribution * Fix tests * Add test * Address CCR comments --- .../platform/extensions/common/extensions.ts | 1 + .../chatPromptFilesContribution.ts | 12 +++- .../promptSyntax/service/promptsService.ts | 3 +- .../service/promptsServiceImpl.ts | 57 +++++++++++++++++-- .../computeAutomaticInstructions.test.ts | 4 ++ .../service/mockPromptsService.ts | 2 +- .../service/promptsService.test.ts | 50 +++++++++++++++- 7 files changed, 119 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 021ad016e0222..3e5b309b82643 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -203,6 +203,7 @@ export interface IChatFileContribution { readonly path: string; readonly name?: string; readonly description?: string; + readonly when?: string; } export interface IExtensionContributions { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index f2d6dd208104b..763de5e22c7b9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -18,11 +18,13 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; interface IRawChatFileContribution { readonly path: string; readonly name?: string; readonly description?: string; + readonly when?: string; } enum ChatContributionPoint { @@ -65,6 +67,10 @@ function registerChatFilesExtensionPoint(point: ChatContributionPoint) { description: localize('chatContribution.property.description', '(Optional) Description of the entry.'), deprecationMessage: localize('chatContribution.property.description.deprecated', 'Specify "description" in the prompt file itself instead.'), type: 'string' + }, + when: { + description: localize('chatContribution.property.when', '(Optional) A condition which must be true to enable this entry.'), + type: 'string' } } } @@ -122,8 +128,12 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut ext.collector.error(localize('extension.invalid.path', "Extension '{0}' {1} entry '{2}' resolves outside the extension.", ext.description.identifier.value, contributionPoint, raw.path)); continue; } + if (raw.when && !ContextKeyExpr.deserialize(raw.when)) { + ext.collector.error(localize('extension.invalid.when', "Extension '{0}' {1} entry '{2}' has an invalid when clause: '{3}'.", ext.description.identifier.value, contributionPoint, raw.path, raw.when)); + continue; + } try { - const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description); + const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description, raw.when); this.registrations.set(key(ext.description.identifier, type, raw.path), d); } catch (e) { const msg = e instanceof Error ? e.message : String(e); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index c94c7a1069657..1ba51c3703aed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -101,6 +101,7 @@ export interface IExtensionPromptPath extends IPromptPathBase { readonly source: ExtensionAgentSourceType; readonly name?: string; readonly description?: string; + readonly when?: string; } export interface ILocalPromptPath extends IPromptPathBase { readonly storage: PromptsStorage.local; @@ -391,7 +392,7 @@ export interface IPromptsService extends IDisposable { * Internal: register a contributed file. Returns a disposable that removes the contribution. * Not intended for extension authors; used by contribution point handler. */ - registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined): IDisposable; + registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined, when?: string): IDisposable; getPromptLocationLabel(promptPath: IPromptPath): string; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 912a103fe5845..ded88d9804ab3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -40,6 +40,7 @@ import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../ho import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptValidator.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; /** * Error thrown when a skill file is missing the required name attribute. @@ -133,6 +134,13 @@ export class PromptsService extends Disposable implements IPromptsService { [PromptsType.hook]: new ResourceMap>(), }; + /** + * Context keys referenced by contributed file `when` clauses. + */ + private readonly _contributedWhenKeys = new Set(); + private readonly _contributedWhenClauses = new Map(); + private readonly _onDidContributedWhenChange = this._register(new Emitter()); + constructor( @ILogService public readonly logger: ILogService, @ILabelService private readonly labelService: ILabelService, @@ -147,6 +155,7 @@ export class PromptsService extends Disposable implements IPromptsService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IPathService private readonly pathService: IPathService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); @@ -155,10 +164,19 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedParsedPromptFromModels.delete(model.uri); })); + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(this._contributedWhenKeys)) { + for (const type of Object.keys(this.cachedFileLocations) as PromptsType[]) { + this.cachedFileLocations[type] = undefined; + } + this._onDidContributedWhenChange.fire(); + } + })); + const modelChangeEvent = this._register(new ModelChangeTracker(this.modelService)).onDidPromptChange; this.cachedCustomAgents = this._register(new CachedPromise( (token) => this.computeCustomAgents(token), - () => Event.any(this.getFileLocatorEvent(PromptsType.agent), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent)) + () => Event.any(this.getFileLocatorEvent(PromptsType.agent), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent), this._onDidContributedWhenChange.event) )); this.cachedSlashCommands = this._register(new CachedPromise( @@ -167,12 +185,13 @@ export class PromptsService extends Disposable implements IPromptsService { this.getFileLocatorEvent(PromptsType.prompt), this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.prompt), - Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill)), + Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), + this._onDidContributedWhenChange.event), )); this.cachedSkills = this._register(new CachedPromise( (token) => this.computeAgentSkills(token), - () => Event.any(this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill)) + () => Event.any(this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), this._onDidContributedWhenChange.event) )); this.cachedHooks = this._register(new CachedPromise( @@ -388,7 +407,18 @@ export class PromptsService extends Disposable implements IPromptsService { const settledResults = await Promise.allSettled(this.contributedFiles[type].values()); const contributedFiles = settledResults .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map(result => result.value); + .map(result => result.value) + .filter(file => { + if (!file.when) { + return true; + } + const expr = ContextKeyExpr.deserialize(file.when); + if (!expr) { + this.logger.warn(`[getExtensionPromptFiles] Ignoring contributed prompt file with invalid when clause: ${file.when}`); + return false; + } + return this.contextKeyService.contextMatchesRules(expr); + }); const activationEvent = this.getProviderActivationEvent(type); if (!activationEvent) { @@ -631,7 +661,7 @@ export class PromptsService extends Disposable implements IPromptsService { return new PromptFileParser().parse(uri, fileContent.value.toString()); } - public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string) { + public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string, when?: string) { const bucket = this.contributedFiles[type]; if (bucket.has(uri)) { // keep first registration per extension (handler filters duplicates per extension already) @@ -657,11 +687,15 @@ export class PromptsService extends Disposable implements IPromptsService { const msg = e instanceof Error ? e.message : String(e); this.logger.error(`[registerContributedFile] Failed to make prompt file readonly: ${uri}`, msg); } - return { uri, name, description, storage: PromptsStorage.extension, type, extension, source: ExtensionAgentSourceType.contribution } satisfies IExtensionPromptPath; + return { uri, name, description, when, storage: PromptsStorage.extension, type, extension, source: ExtensionAgentSourceType.contribution } satisfies IExtensionPromptPath; })(); bucket.set(uri, entryPromise); + if (when) { + this._contributedWhenClauses.set(`${type}/${uri.toString()}`, when); + } const flushCachesIfRequired = () => { + this._updateContributedWhenKeys(); this.cachedFileLocations[type] = undefined; switch (type) { case PromptsType.agent: @@ -680,11 +714,22 @@ export class PromptsService extends Disposable implements IPromptsService { return { dispose: () => { bucket.delete(uri); + this._contributedWhenClauses.delete(`${type}/${uri.toString()}`); flushCachesIfRequired(); } }; } + private _updateContributedWhenKeys(): void { + this._contributedWhenKeys.clear(); + for (const whenClause of this._contributedWhenClauses.values()) { + const expr = ContextKeyExpr.deserialize(whenClause); + for (const key of expr?.keys() ?? []) { + this._contributedWhenKeys.add(key); + } + } + } + getPromptLocationLabel(promptPath: IPromptPath): string { switch (promptPath.storage) { case PromptsStorage.local: return this.labelService.getUriLabel(dirname(promptPath.uri), { relative: true }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index 2d008a2567f07..abde6d1f8453d 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -44,6 +44,8 @@ import { IRemoteAgentService } from '../../../../../../workbench/services/remote import { basename } from '../../../../../../base/common/resources.js'; import { match } from '../../../../../../base/common/glob.js'; import { ChatModeKind } from '../../../common/constants.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; suite('ComputeAutomaticInstructions', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -175,6 +177,8 @@ suite('ComputeAutomaticInstructions', () => { getEnvironment: () => Promise.resolve(null), }); + instaService.stub(IContextKeyService, new MockContextKeyService()); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index ab269f0e48597..525d116ff6520 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -54,7 +54,7 @@ export class MockPromptsService implements IPromptsService { // eslint-disable-next-line @typescript-eslint/no-explicit-any parseNew(_uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { throw new Error('Not implemented'); } - registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined): IDisposable { throw new Error('Not implemented'); } + registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined, when?: string): IDisposable { throw new Error('Not implemented'); } getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } listNestedAgentMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } listAgentInstructions(token: CancellationToken): Promise { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index dd8f81d4dd3a8..e3c508205ac3f 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../../../base/common/event.js'; import { match } from '../../../../../../../base/common/glob.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; import { Schemas } from '../../../../../../../base/common/network.js'; @@ -50,6 +50,8 @@ import { IExtensionService } from '../../../../../../services/extensions/common/ import { IRemoteAgentService } from '../../../../../../services/remote/common/remoteAgentService.js'; import { ChatModeKind } from '../../../../common/constants.js'; import { HookType } from '../../../../common/promptSyntax/hookSchema.js'; +import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -158,6 +160,8 @@ suite('PromptsService', () => { getEnvironment: () => Promise.resolve(null), }); + instaService.stub(IContextKeyService, new MockContextKeyService()); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -1910,6 +1914,50 @@ suite('PromptsService', () => { registered1.dispose(); registered2.dispose(); }); + + test('Contributed file with when clause is filtered by context key', async () => { + const uri = URI.parse('file://extensions/my-extension/conditional.instructions.md'); + const extension = {} as IExtensionDescription; + + // Create a mock context key service that we can control + let matchResult = false; + const contextKeyChangeEmitter = disposables.add(new Emitter()); + const testContextKeyService = new class extends MockContextKeyService { + override contextMatchesRules(): boolean { + return matchResult; + } + override get onDidChangeContext() { + return contextKeyChangeEmitter.event; + } + }(); + instaService.stub(IContextKeyService, testContextKeyService); + const testService = disposables.add(instaService.createInstance(PromptsService)); + + const registered = testService.registerContributedFile( + PromptsType.instructions, uri, extension, + 'Conditional Instructions', 'Only when enabled', 'myFeature.enabled', + ); + + // When clause is false - should be filtered out + const before = await testService.listPromptFiles(PromptsType.instructions, CancellationToken.None); + assert.strictEqual(before.length, 0, 'Should be filtered out when context key is false'); + + // Change context to make when clause true + matchResult = true; + contextKeyChangeEmitter.fire({ + affectsSome: (keys) => keys.has('myFeature.enabled'), + allKeysContainedIn: () => false, + }); + + const after = await testService.listPromptFiles(PromptsType.instructions, CancellationToken.None); + assert.strictEqual(after.length, 1, 'Should be included when context key is true'); + assert.strictEqual(after[0].uri.toString(), uri.toString()); + + registered.dispose(); + + // Restore original stub + instaService.stub(IContextKeyService, new MockContextKeyService()); + }); }); test('Instructions provider', async () => { From 5a3602ec068fd05dfe84b787bd8cde090ccc7f49 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 19 Feb 2026 11:59:15 +0000 Subject: [PATCH 12/24] style(titlebar): adjust inactive titlebar opacity for improved visibility --- extensions/theme-2026/themes/styles.css | 13 ------------- .../browser/parts/titlebar/media/titlebarpart.css | 4 ++++ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 343d812b62c92..e0fbee151aa0f 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -127,11 +127,6 @@ z-index: 10; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { - border-radius: 0; - border-top: none !important; -} - .monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: inset var(--shadow-active-tab); position: relative; @@ -156,14 +151,6 @@ box-shadow: var(--shadow-md); } -.monaco-workbench .part.titlebar.inactive { - background: var(--vscode-titleBar-inactiveBackground) !important; - - & > * { - opacity: 0.3; - } -} - /* Quick Input (Command Palette) */ .monaco-workbench .quick-input-widget { box-shadow: var(--shadow-xl) !important; diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 982f5a620df98..454939c8afc4a 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -146,6 +146,10 @@ visibility: hidden; } +.monaco-workbench .part.titlebar.inactive > * { + opacity: 0.3; +} + .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label, .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item.monaco-dropdown-with-primary .action-label { color: var(--vscode-titleBar-activeForeground); From a45b02bfda6e272f183d20fc08703c51ba01b5f0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Feb 2026 13:28:05 +0100 Subject: [PATCH 13/24] Revert "extensions - stop opening them on installation" (#296270) Revert "extensions - stop opening them on installation (#296251)" This reverts commit 91cafa504f99f68de0d39497b57ab143266fa18f. --- .../contrib/extensions/browser/extensionsActions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 05ecf2338ad64..d1dacd0dc6184 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -585,7 +585,9 @@ export class InstallAction extends ExtensionAction { } } - alert(localize('installExtensionStart', "Installing extension {0} started.", this.extension.displayName)); + this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: this.options.installPreReleaseVersion }); + + alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); /* __GDPR__ "extensions:action:install" : { @@ -822,7 +824,8 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { if (!this.server) { return; } - alert(localize('installExtensionStart', "Installing extension {0} started.", this.extension.displayName)); + this.extensionsWorkbenchService.open(this.extension); + alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); return this.extensionsWorkbenchService.installInServer(this.extension, this.server); } @@ -1553,6 +1556,7 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { if (!this.extension) { return; } + this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: !this.extension.preRelease }); await this.extensionsWorkbenchService.togglePreRelease(this.extension); } } From ad841aced14556d0333dcebce19f94dda7f4335a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Feb 2026 14:20:00 +0100 Subject: [PATCH 14/24] sessions - tweak settings (#296277) --- .../contrib/configuration/browser/configuration.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index bf5a13e8b0a19..36776de6ce951 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -36,6 +36,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.layoutControl.type': 'toggles', 'workbench.editor.allowOpenInModalEditor': false, 'window.menuStyle': 'custom', + 'window.dialogStyle': 'custom', 'terminal.integrated.initialHint': false }, From 9ab0a4c5eabf3b6d312206c199774e9ce53179a2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:37:56 +0100 Subject: [PATCH 15/24] Sessions - enable auto-fetch (#296288) --- .../contrib/configuration/browser/configuration.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 36776de6ce951..4da1e012c757b 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -19,6 +19,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'files.autoSave': 'afterDelay', + 'git.autofetch': true, 'git.showProgress': false, 'github.copilot.chat.claudeCode.enabled': true, From 7ffca955fd4d5a22fa0fd806c0cf7557451ee6a9 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 19 Feb 2026 06:43:49 -0800 Subject: [PATCH 16/24] Update Windows background updates setting title and description (#296150) --- src/vs/platform/update/common/update.config.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index 5d1a419f8f2b3..93dd62df97ec6 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -67,8 +67,8 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true, scope: ConfigurationScope.APPLICATION, - title: localize('enableWindowsBackgroundUpdatesTitle', "Enable Background Updates on Windows"), - description: localize('enableWindowsBackgroundUpdates', "Enable to download and install new VS Code versions in the background on Windows."), + title: localize('enableWindowsBackgroundUpdatesTitle', "Enable Background Updates"), + description: localize('enableWindowsBackgroundUpdates', "Enable to download and install new VS Code versions in the background."), included: isWindows && !isWeb }, 'update.showReleaseNotes': { From b67d97f14686fe0feecd9f04ba8dd951f6685b5e Mon Sep 17 00:00:00 2001 From: Isidor Date: Thu, 19 Feb 2026 15:53:09 +0100 Subject: [PATCH 17/24] Use 10/20-minute focus windows for edit telemetry --- .../telemetry/editSourceTrackingImpl.ts | 22 +++++++++---------- .../test/browser/editTelemetry.test.ts | 6 ++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts index 2a60ae28108aa..2a2ae1cc5d6bf 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../../base/common/arrays.js'; -import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js'; +import { IntervalTimer } from '../../../../../base/common/async.js'; import { toDisposable, Disposable } from '../../../../../base/common/lifecycle.js'; import { mapObservableArrayCached, derived, IObservable, observableSignal, runOnChange, autorun } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -18,7 +18,7 @@ import { sumByCategory } from '../helpers/utils.js'; import { ScmAdapter, ScmRepoAdapter } from './scmAdapter.js'; import { IRandomService } from '../randomService.js'; -type EditTelemetryMode = 'longterm' | '5minWindow' | '20minFocusWindow'; +type EditTelemetryMode = 'longterm' | '10minFocusWindow' | '20minFocusWindow'; type EditTelemetryTrigger = '10hours' | 'hashChange' | 'branchChange' | 'closed' | 'time'; export class EditSourceTrackingImpl extends Disposable { @@ -112,7 +112,7 @@ class TrackedDocumentInfo extends Disposable { this._store.add(this._instantiationService.createInstance(EditTelemetryReportEditArcForChatOrInlineChatSender, _doc.documentWithAnnotations, this._repo)); this._store.add(this._instantiationService.createInstance(CreateSuggestionIdForChatOrInlineChatCaller, _doc.documentWithAnnotations)); - // Wall-clock time based 5-minute window tracker + // Focus time based 10-minute window tracker const resetSignal = observableSignal('resetSignal'); this.windowedTracker = derived((reader) => { @@ -123,17 +123,17 @@ class TrackedDocumentInfo extends Disposable { } resetSignal.read(reader); - // Reset after 5 minutes of wall-clock time - reader.store.add(new TimeoutTimer(() => { + // Reset after 10 minutes of accumulated focus time + reader.store.add(this._userAttentionService.fireAfterGivenFocusTimePassed(10 * 60 * 1000, () => { resetSignal.trigger(undefined); - }, 5 * 60 * 1000)); + })); const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); const startFocusTime = this._userAttentionService.totalFocusTimeMs; const startTime = Date.now(); reader.store.add(toDisposable(async () => { // send windowed document telemetry - this.sendTelemetry('5minWindow', 'time', t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); + this.sendTelemetry('10minFocusWindow', 'time', t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); t.dispose(); })); @@ -217,9 +217,9 @@ class TrackedDocumentInfo extends Disposable { totalModifiedCount: number; }, { owner: 'hediet'; - comment: 'Provides detailed character count breakdown for individual edit sources (typing, paste, inline completions, NES, etc.) within a session. Reports the top 10-30 sources per session with granular metadata including extension IDs and model IDs for AI edits. Sessions are scoped to either 5-minute wall-clock time windows, 20-minute focus time windows for visible documents, or longer periods ending on branch changes, commits, or 10-hour intervals. Focus time is computed as the accumulated time where VS Code has focus and there was recent user activity (within the last minute). This event complements editSources.stats by providing source-specific details. @sentToGitHub'; + comment: 'Provides detailed character count breakdown for individual edit sources (typing, paste, inline completions, NES, etc.) within a session. Reports the top 10-30 sources per session with granular metadata including extension IDs and model IDs for AI edits. Sessions are scoped to either 10-minute or 20-minute focus time windows for visible documents, or longer periods ending on branch changes, commits, or 10-hour intervals. Focus time is computed as the accumulated time where VS Code has focus and there was recent user activity (within the last minute). This event complements editSources.stats by providing source-specific details. @sentToGitHub'; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either \'longterm\', \'5minWindow\', or \'20minFocusWindow\'.' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either \'longterm\', \'10minFocusWindow\', or \'20minFocusWindow\'.' }; sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A description of the source of the edit.' }; sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit with some properties (such as extensionId, extensionVersion and modelId) removed.' }; @@ -275,9 +275,9 @@ class TrackedDocumentInfo extends Disposable { trigger: EditTelemetryTrigger; }, { owner: 'hediet'; - comment: 'Aggregates character counts by edit source category (user typing, AI completions, NES, IDE actions, external changes) for each editing session. Sessions represent units of work and end when documents close, branches change, commits occur, or time limits are reached (5 minutes of wall-clock time, 20 minutes of focus time for visible documents, or 10 hours otherwise). Focus time is computed as accumulated 1-minute blocks where VS Code has focus and there was recent user activity. Tracks both total characters inserted and characters remaining at session end to measure retention. This high-level summary complements editSources.details which provides granular per-source breakdowns. @sentToGitHub'; + comment: 'Aggregates character counts by edit source category (user typing, AI completions, NES, IDE actions, external changes) for each editing session. Sessions represent units of work and end when documents close, branches change, commits occur, or time limits are reached (10 or 20 minutes of focus time for visible documents, or 10 hours otherwise). Focus time is computed as accumulated 1-minute blocks where VS Code has focus and there was recent user activity. Tracks both total characters inserted and characters remaining at session end to measure retention. This high-level summary complements editSources.details which provides granular per-source breakdowns. @sentToGitHub'; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm, 5minWindow, or 20minFocusWindow' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm, 10minFocusWindow, or 20minFocusWindow' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; diff --git a/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts b/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts index 8c6cf1052645f..5ac15f260deb4 100644 --- a/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts +++ b/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts @@ -111,11 +111,11 @@ function fib(n) { '00:11:010 editTelemetry.codeSuggested: {\"eventId\":\"evt-5c9c6fe7-b219-4ff8-aaa7-ab2b355b21c0\",\"suggestionId\":\"sgt-74379122-0452-4e26-9c38-9d62f1e7ae73\",\"presentation\":\"highlightedEdit\",\"feature\":\"sideBarChat\",\"languageId\":\"plaintext\",\"editCharsInserted\":19,\"editCharsDeleted\":20,\"editLinesInserted\":1,\"editLinesDeleted\":1,\"modelId\":{\"isTrustedTelemetryValue\":true}}', '01:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', '01:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', - '05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', - '05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', - '05:00:000 editTelemetry.editSources.stats: {\"mode\":\"5minWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":250010,\"actualTime\":300000,\"trigger\":\"time\"}', '05:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', '05:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', + '12:00:000 editTelemetry.editSources.details: {\"mode\":\"10minFocusWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', + '12:00:000 editTelemetry.editSources.details: {\"mode\":\"10minFocusWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', + '12:00:000 editTelemetry.editSources.stats: {\"mode\":\"10minFocusWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":600000,\"actualTime\":720000,\"trigger\":\"time\"}', '22:00:000 editTelemetry.editSources.details: {\"mode\":\"20minFocusWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', '22:00:000 editTelemetry.editSources.details: {\"mode\":\"20minFocusWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', '22:00:000 editTelemetry.editSources.stats: {\"mode\":\"20minFocusWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":1200000,\"actualTime\":1320000,\"trigger\":\"time\"}' From 515078bdc00c42923a9527791d12353d4eb1722c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:10:21 +0100 Subject: [PATCH 18/24] Bump tar and dmg-builder in /build (#295921) Bumps [tar](https://github.com/isaacs/node-tar) to 7.5.9 and updates ancestor dependency [dmg-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/dmg-builder). These dependencies need to be updated together. Updates `tar` from 6.2.1 to 7.5.9 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.1...v7.5.9) Updates `dmg-builder` from 26.5.0 to 26.7.0 - [Release notes](https://github.com/electron-userland/electron-builder/releases) - [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/dmg-builder/CHANGELOG.md) - [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.7.0/packages/dmg-builder) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.9 dependency-type: indirect - dependency-name: dmg-builder dependency-version: 26.7.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/package-lock.json | 459 ++++++++++++++++------------------------ build/package.json | 2 +- 2 files changed, 185 insertions(+), 276 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index 36b85902e22d6..5cf0189d3e690 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -53,7 +53,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "26.5.0", + "dmg-builder": "26.8.1", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", @@ -813,14 +813,13 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.1.tgz", - "integrity": "sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", @@ -831,7 +830,7 @@ "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", - "tar": "^6.0.5", + "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { @@ -841,142 +840,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@electron/rebuild/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/rebuild/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@electron/rebuild/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@electron/rebuild/node_modules/node-abi": { "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", @@ -991,9 +854,9 @@ } }, "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -1003,38 +866,6 @@ "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@electron/universal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", @@ -1792,9 +1623,9 @@ } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3136,23 +2967,24 @@ "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.5.0.tgz", - "integrity": "sha512-iRRiJhM0uFMauDeIuv8ESHZSn+LESbdDEuHi7rKdeETjrvBObecXnWJx1f3vs3KtoGcd3hCk1zURKypyvZOtFQ==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "4.0.1", + "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", - "builder-util": "26.4.1", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", @@ -3160,7 +2992,7 @@ "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.4.1", + "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", @@ -3170,9 +3002,10 @@ "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", + "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", - "tar": "7.5.3", + "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" @@ -3181,8 +3014,55 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.5.0", - "electron-builder-squirrel-windows": "26.5.0" + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/app-builder-lib/node_modules/@electron/osx-sign": { @@ -3227,6 +3107,29 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -3242,14 +3145,37 @@ "node": ">=12" } }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/app-builder-lib/node_modules/js-yaml": { @@ -3265,27 +3191,14 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { "node": "20 || >=22" @@ -3295,9 +3208,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3307,16 +3220,6 @@ "node": ">=10" } }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -3398,6 +3301,13 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -3603,9 +3513,9 @@ "license": "MIT" }, "node_modules/builder-util": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", - "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3822,6 +3732,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -4595,14 +4506,14 @@ } }, "node_modules/dmg-builder": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.5.0.tgz", - "integrity": "sha512-AyOCzpS1TCxDkSWxAzpfw5l7jBX4C8jKCucmT/6y6/24H5VKSHpjcVJD0W8o5BrFi+skC7Z7+F4aNyHmvn4AAw==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.5.0", - "builder-util": "26.4.1", + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -4883,17 +4794,17 @@ } }, "node_modules/electron-publish": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.4.1.tgz", - "integrity": "sha512-nByal9K5Ar3BNJUfCSglXltpKUhJqpwivNpKVHnkwxTET9LKl+NxoojpGF1dSXVFcoBKVm+OhsVa28ZsoshEPA==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "26.4.1", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" @@ -5509,9 +5420,9 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -6328,13 +6239,6 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -7007,19 +6911,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -7110,9 +7001,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7160,19 +7051,19 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7903,6 +7794,25 @@ "node": ">=10" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9056,10 +8966,9 @@ } }, "node_modules/tar": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz", - "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/build/package.json b/build/package.json index e889b6ac54d7b..0b18b8daff84b 100644 --- a/build/package.json +++ b/build/package.json @@ -47,7 +47,7 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "dmg-builder": "26.5.0", + "dmg-builder": "26.8.1", "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", From 45a66bc98f296816330e3ba6239f51c24ce41d5f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Feb 2026 16:21:43 +0100 Subject: [PATCH 19/24] sessions - fix title bar dragging on macOS (#296292) --- src/vs/sessions/browser/parts/media/titlebarpart.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index b005dbe50de41..22273655103c7 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -38,3 +38,8 @@ .agent-sessions-workbench:not(.nosidebar) .part.titlebar > .titlebar-container > .titlebar-left > .left-toolbar-container { display: none !important; } + +/* macOS native: the spacer uses window-controls-container but should not block dragging */ +.agent-sessions-workbench.mac .part.titlebar .window-controls-container { + -webkit-app-region: drag; +} From c00448c8ee863274bdabeec11446d8a65a6c9952 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Feb 2026 16:22:31 +0100 Subject: [PATCH 20/24] fix(tasks): update beginsPattern for transpilation background task (#296295) https://github.com/microsoft/vscode/issues/295523 --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8d0d6552d05ec..03fe5fb263fc0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -26,7 +26,7 @@ "message": 4 }, "background": { - "beginsPattern": "Starting transpilation...", + "beginsPattern": "Starting transpilation\\.\\.\\.", "endsPattern": "Finished transpilation with" } } From c613dcbf0838e9f522f43c3e929e144cf910e2d7 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Feb 2026 16:42:01 +0100 Subject: [PATCH 21/24] Enhance inline chat functionality and affordances (#296291) * Enhance inline chat functionality with new actions which allow to continue/enrich an existing chat session * Refactor inline chat affordances and adjust z-index for overlay widgets * Add support for queued chat requests in SubmitToChatAction --- .../browser/inlineChat.contribution.ts | 22 +- .../inlineChat/browser/inlineChatActions.ts | 254 +++++++++++++++++- .../browser/inlineChatAffordance.ts | 10 +- .../browser/inlineChatController.ts | 46 ++-- .../browser/inlineChatEditorAffordance.ts | 10 +- .../browser/inlineChatGutterAffordance.ts | 36 +-- .../browser/inlineChatOverlayWidget.ts | 56 +--- .../media/inlineChatEditorAffordance.css | 1 + .../browser/media/inlineChatOverlayWidget.css | 2 +- .../contrib/inlineChat/common/inlineChat.ts | 3 + 10 files changed, 319 insertions(+), 121 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ce4653f5cfbea..f99d40fbd0fb5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -6,10 +6,10 @@ import './inlineChatDefaultModel.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS, ACTION_START } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; @@ -23,8 +23,6 @@ import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { Codicon } from '../../../../base/common/codicons.js'; registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -86,25 +84,17 @@ const cancelActionMenuItem: IMenuItem = { MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem); -// --- InlineChatEditorAffordance menu --- -MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { - group: '0_chat', - order: 1, - command: { - id: ACTION_START, - title: localize('editCode', "Ask for Edits"), - shortTitle: localize('editCodeShort', "Ask for Edits"), - icon: Codicon.sparkle, - }, - when: EditorContextKeys.hasNonEmptySelection, -}); // --- actions --- registerAction2(InlineChatActions.StartSessionAction); +registerAction2(InlineChatActions.AskInChatAction); registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.SubmitInlineChatInputAction); +registerAction2(InlineChatActions.SubmitToChatAction); +registerAction2(InlineChatActions.AttachToChatAction); +registerAction2(InlineChatActions.HideInlineChatInputAction); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 1b1a639631153..39bbc646a0099 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,10 +11,10 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, ACTION_ASK_IN_CHAT, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, InlineChatConfigKeys } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; -import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; +import { Action2, IAction2Options, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -25,6 +25,13 @@ import { CommandsRegistry } from '../../../../platform/commands/common/commands. import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IChatEditingService } from '../../chat/common/editing/chatEditingService.js'; +import { IChatWidgetService } from '../../chat/browser/chat.js'; +import { IAgentFeedbackVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { basename } from '../../../../base/common/resources.js'; +import { ChatRequestQueueKind } from '../../chat/common/chatService/chatService.js'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); @@ -59,7 +66,7 @@ export class StartSessionAction extends Action2 { shortTitle: localize2('runShort', 'Inline Chat'), category: AbstractInlineChatAction.category, f1: true, - precondition: inlineChatContextKey, + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, @@ -103,6 +110,8 @@ export class StartSessionAction extends Action2 { private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { + const configServce = accessor.get(IConfigurationService); + const ctrl = InlineChatController.get(editor); if (!ctrl) { return; @@ -117,10 +126,36 @@ export class StartSessionAction extends Action2 { if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { options = arg; } - await InlineChatController.get(editor)?.run({ ...options }); + + // use hover overlay to ask for input + if (!options?.message && configServce.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { + const selection = editor.getSelection(); + const placeholder = selection && !selection.isEmpty() + ? localize('placeholderWithSelection', "Describe how to change this") + : localize('placeholderNoSelection', "Describe what to generate"); + // show menu and RETURN because the menu is re-entrant + await ctrl.inputOverlayWidget.showMenuAtSelection(placeholder); + return; + } + + await ctrl?.run({ ...options }); } } +// --- InlineChatEditorAffordance menu --- + +MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { + group: '0_chat', + order: 1, + when: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + command: { + id: ACTION_START, + title: localize('editCode', "Ask for Edits"), + shortTitle: localize('editCodeShort', "Ask for Edits"), + icon: Codicon.sparkle, + } +}); + export class FocusInlineChat extends EditorAction2 { constructor() { @@ -334,11 +369,17 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { id: 'inlineChat.submitInput', title: localize2('submitInput', "Send"), icon: Codicon.send, - precondition: CTX_INLINE_CHAT_INPUT_HAS_TEXT, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Enter + }, menu: [{ id: MenuId.InlineChatInput, group: '0_main', order: 1, + when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate() }] }); } @@ -351,3 +392,206 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { } } } + +export class HideInlineChatInputAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat.hideInput', + title: localize2('hideInput', "Hide Input"), + precondition: CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, + keybinding: { + when: CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Escape + } + }); + } + + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: unknown[]): void { + ctrl.inputWidget.hide(); + } +} + + +export class AskInChatAction extends EditorAction2 { + + constructor() { + super({ + id: ACTION_ASK_IN_CHAT, + title: localize2('askInChat', 'Ask in Chat'), + category: AbstractInlineChatAction.category, + f1: true, + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + icon: Codicon.chatSparkle, + menu: [{ + id: MenuId.EditorContext, + group: '1_chat', + order: 3, + when: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + }, { + id: MenuId.InlineChatEditorAffordance, + group: '0_chat', + order: 1, + when: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + }] + }); + } + + override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) { + const chatEditingService = accessor.get(IChatEditingService); + const ctrl = InlineChatController.get(editor); + if (!ctrl || !editor.hasModel()) { + return; + } + const entry = chatEditingService.editingSessionsObs.get().find(value => value.getEntry(editor.getModel().uri)); + if (entry) { + ctrl.inputOverlayWidget.showMenuAtSelection(localize('placeholderAskInChat', "Describe how to proceed in Chat")); + } + } +} + +export class SubmitToChatAction extends AbstractInlineChatAction { + + + constructor() { + super({ + id: 'inlineChat.submitToChat', + title: localize2('submitToChat', "Send to Chat"), + icon: Codicon.arrowUp, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Enter + }, + menu: [{ + id: MenuId.InlineChatInput, + group: '0_main', + order: 1, + when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, + alt: { + id: AttachToChatAction.Id, + title: localize2('attachToChat', "Attach to Chat"), + icon: Codicon.attach + } + }] + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const chatWidgetService = accessor.get(IChatWidgetService); + if (!editor.hasModel()) { + return; + } + + const value = ctrl.inputWidget.value; + ctrl.inputWidget.hide(); + if (!value) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(s => s.getEntry(editor.getModel().uri)); + if (!session) { + return; + } + + const widget = await chatWidgetService.openSession(session.chatSessionResource); + if (!widget) { + return; + } + + const selection = editor.getSelection(); + if (selection && !selection.isEmpty()) { + await widget.attachmentModel.addFile(editor.getModel().uri, selection); + } + await widget.acceptInput(value, { queue: ChatRequestQueueKind.Queued }); + } +} + +export class AttachToChatAction extends AbstractInlineChatAction { + + static readonly Id = 'inlineChat.attachToChat'; + + constructor() { + super({ + id: AttachToChatAction.Id, + title: localize2('attachToChat', "Attach to Chat"), + icon: Codicon.attach, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + secondary: [KeyMod.Alt | KeyCode.Enter] + }, + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const chatWidgetService = accessor.get(IChatWidgetService); + if (!editor.hasModel()) { + return; + } + + const value = ctrl.inputWidget.value; + const selection = editor.getSelection(); + ctrl.inputWidget.hide(); + if (!value || !selection || selection.isEmpty()) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(s => s.getEntry(editor.getModel().uri)); + if (!session) { + return; + } + + const widget = await chatWidgetService.openSession(session.chatSessionResource); + if (!widget) { + return; + } + + const uri = editor.getModel().uri; + const selectedText = editor.getModel().getValueInRange(selection); + const fileName = basename(uri); + const lineRef = selection.startLineNumber === selection.endLineNumber + ? `${selection.startLineNumber}` + : `${selection.startLineNumber}-${selection.endLineNumber}`; + + const feedbackValue = [ + ``, + ``, + selectedText, + ``, + ``, + value, + ``, + `` + ].join('\n'); + + const feedbackId = generateUuid(); + const entry: IAgentFeedbackVariableEntry = { + kind: 'agentFeedback', + id: `inlineChat.feedback.${feedbackId}`, + name: localize('attachToChat.name', "{0}:{1}", fileName, lineRef), + icon: Codicon.comment, + sessionResource: session.chatSessionResource, + feedbackItems: [{ + id: feedbackId, + text: value, + resourceUri: uri, + range: selection, + }], + value: feedbackValue, + }; + + widget.attachmentModel.addContext(entry); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index ea0933b7e448c..4e3cf4c660003 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -44,7 +44,7 @@ export class InlineChatAffordance extends Disposable { readonly #editor: ICodeEditor; readonly #inputWidget: InlineChatInputWidget; readonly #instantiationService: IInstantiationService; - readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>(this, undefined); + readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number; placeholder: string } | undefined>(this, undefined); constructor( editor: ICodeEditor, @@ -118,7 +118,6 @@ export class InlineChatAffordance extends Disposable { InlineChatGutterAffordance, editorObs, derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), - this.#menuData )); const editorAffordance = this.#instantiationService.createInstance( @@ -157,7 +156,7 @@ export class InlineChatAffordance extends Disposable { const left = data.rect.left - editorRect.left; // Show the overlay widget - this.#inputWidget.show(data.lineNumber, left, data.above); + this.#inputWidget.show(data.lineNumber, left, data.above, data.placeholder); })); this._store.add(autorun(r => { @@ -168,7 +167,7 @@ export class InlineChatAffordance extends Disposable { })); } - async showMenuAtSelection() { + async showMenuAtSelection(placeholder: string): Promise { assertType(this.#editor.hasModel()); const direction = this.#editor.getSelection().getDirection(); @@ -182,7 +181,8 @@ export class InlineChatAffordance extends Disposable { this.#menuData.set({ rect: new DOMRect(x, y, 0, scrolledPosition.height), above: direction === SelectionDirection.RTL, - lineNumber: position.lineNumber + lineNumber: position.lineNumber, + placeholder }, undefined); await waitForState(this.#inputWidget.position, pos => pos === null); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 79335bf93cd9a..6415bfcfe49de 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,7 +39,7 @@ import { ISharedWebContentExtractorService } from '../../../../platform/webConte import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; -import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; @@ -51,14 +51,13 @@ import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../. import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; -import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; - export abstract class InlineChatRunOptions { initialSelection?: ISelection; @@ -117,7 +116,7 @@ export class InlineChatController implements IEditorContribution { private readonly _isActiveController = observableValue(this, false); private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; - private readonly _gutterIndicator: InlineChatAffordance; + readonly inputOverlayWidget: InlineChatAffordance; private readonly _inputWidget: InlineChatInputWidget; private readonly _currentSession: IObservable; @@ -149,17 +148,42 @@ export class InlineChatController implements IEditorContribution { @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, @ILogService private readonly _logService: ILogService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, ) { const editorObs = observableCodeEditor(_editor); - const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); 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 => { + const model = editorObs.model.read(r); + if (!model) { + ctxFileBelongsToChat.set(false); + return; + } + const sessions = this._chatEditingService.editingSessionsObs.read(r); + let hasEdits = false; + for (const session of sessions) { + const entries = session.entries.read(r); + for (const entry of entries) { + if (isEqual(entry.modifiedURI, model.uri)) { + hasEdits = true; + break; + } + } + if (hasEdits) { + break; + } + } + 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._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); + this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); this._zone = new Lazy(() => { @@ -231,8 +255,6 @@ export class InlineChatController implements IEditorContribution { return result; }); - - const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); this._currentSession = derived(r => { @@ -474,18 +496,10 @@ export class InlineChatController implements IEditorContribution { existingSession.dispose(); } - // use hover overlay to ask for input - if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { - // show menu and RETURN because the menu is re-entrant - await this._gutterIndicator.showMenuAtSelection(); - return true; - } - this._isActiveController.set(true, undefined); const session = this._inlineChatSessionService.createSession(this._editor); - // Store for tracking model changes during this session const sessionStore = new DisposableStore(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 519ae158e7f68..1123978c2e85b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -28,7 +28,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { ACTION_START } from '../common/inlineChat.js'; +import { ACTION_START, ACTION_ASK_IN_CHAT } from '../common/inlineChat.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; class QuickFixActionViewItem extends MenuEntryActionViewItem { @@ -105,7 +105,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { } } -class InlineChatStartActionViewItem extends MenuEntryActionViewItem { +class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { private readonly _kbLabel: string | undefined; @@ -150,7 +150,7 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi private readonly _onDidRunAction = this._store.add(new Emitter()); readonly onDidRunAction: Event = this._onDidRunAction.event; - readonly allowEditorOverflow = false; + readonly allowEditorOverflow = true; readonly suppressMouseDown = false; constructor( @@ -173,8 +173,8 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi if (action instanceof MenuItemAction && action.id === quickFixCommandId) { return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); } - if (action instanceof MenuItemAction && action.id === ACTION_START) { - return instantiationService.createInstance(InlineChatStartActionViewItem, action); + if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT)) { + return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action); } return undefined; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 19d67a29e9503..3d82cec90ec04 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -5,11 +5,11 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { constObservable, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; import { InlineCompletionCommand } from '../../../../editor/common/languages.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; @@ -30,9 +30,8 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { readonly onDidRunAction: Event = this._onDidRunAction.event; constructor( - private readonly _myEditorObs: ObservableCodeEditor, + myEditorObs: ObservableCodeEditor, selection: IObservable, - private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, @IKeybindingService _keybindingService: IKeybindingService, @IHoverService hoverService: HoverService, @IInstantiationService instantiationService: IInstantiationService, @@ -46,7 +45,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const menu = menuService.createMenu(MenuId.InlineChatEditorAffordance, contextKeyService); const menuObs = observableFromEvent(menu.onDidChange, () => menu.getActions({ renderShortTitle: false })); - const codeActionController = CodeActionController.get(_myEditorObs.editor); + const codeActionController = CodeActionController.get(myEditorObs.editor); const lightBulbObs = codeActionController?.lightBulbState; const data = derived(r => { @@ -93,7 +92,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { return new InlineEditsGutterIndicatorData( gutterMenuData, lineRange, - new SimpleInlineSuggestModel(() => { }, () => this._doShowHover()), + new SimpleInlineSuggestModel(() => { }, () => { }), undefined, // altAction { icon } ); @@ -102,36 +101,13 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const focusIsInMenu = observableValue({}, false); super( - _myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, + myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, hoverService, instantiationService, accessibilityService, themeService, userInteractionService ); this._store.add(menu); - this._store.add(autorun(r => { - const element = _hover.read(r); - this._hoverVisible.set(!!element, undefined); - })); this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); } - - private _doShowHover(): void { - if (this._hoverVisible.get()) { - return; - } - - // Use the icon element from the base class as anchor - const iconElement = this._iconRef.element; - if (!iconElement) { - this._hover.set(undefined, undefined); - return; - } - - const selection = this._myEditorObs.cursorSelection.get(); - const direction = selection?.getDirection() ?? SelectionDirection.LTR; - const lineNumber = selection?.getPosition().lineNumber ?? 1; - this._hover.set({ rect: iconElement.getBoundingClientRect(), above: direction === SelectionDirection.RTL, lineNumber }, undefined); - } - } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 844b7dae00309..1d27ddb7da5dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -28,9 +28,8 @@ import { getFlatActionBarActions } from '../../../../platform/actions/browser/me import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ChatEditingAcceptRejectActionViewItem } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; -import { CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED } from '../common/inlineChat.js'; import { StickyScrollController } from '../../../../editor/contrib/stickyScroll/browser/stickyScrollController.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -53,7 +52,6 @@ export class InlineChatInputWidget extends Disposable { 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 }>; @@ -65,7 +63,6 @@ export class InlineChatInputWidget extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ICommandService private readonly _commandService: ICommandService, @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @@ -153,24 +150,14 @@ export class InlineChatInputWidget extends Disposable { this._store.add(resizeObserver); this._store.add(resizeObserver.observe(toolbar.getElement())); - // Compute min and max widget width based on editor content width - const maxWidgetWidth = derived(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - return Math.max(0, Math.round(layoutInfo.contentWidth * 0.70)); - }); - const minWidgetWidth = derived(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - return Math.max(0, Math.round(layoutInfo.contentWidth * 0.33)); - }); - const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); this._layoutData = derived(r => { const editorPad = 6; const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); - const minWidth = minWidgetWidth.read(r); - const maxWidth = maxWidgetWidth.read(r); + const minWidth = 220; + const maxWidth = 600; const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' ? maxWidth : Math.max(minWidth, Math.min(totalWidth, maxWidth)); @@ -210,17 +197,6 @@ export class InlineChatInputWidget extends Disposable { this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); })); - // Update placeholder based on selection state - this._store.add(autorun(r => { - const selection = this._editorObs.cursorSelection.read(r); - const hasSelection = selection && !selection.isEmpty(); - const placeholderText = hasSelection - ? localize('placeholderWithSelection', "Describe how to change this") - : localize('placeholderNoSelection', "Describe what to generate"); - - this._input.updateOptions({ placeholder: placeholderText }); - })); - // Track input text for context key and adjust width based on content const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService); @@ -229,21 +205,15 @@ export class InlineChatInputWidget extends Disposable { })); this._store.add(toDisposable(() => inputHasText.reset())); - // Handle Enter key to submit, ArrowDown to move to actions + // 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))); + this._store.add(toDisposable(() => inputWidgetFocused.reset())); + + // Handle key events: ArrowDown to move to actions this._store.add(this._input.onKeyDown(e => { - if (e.keyCode === KeyCode.Enter && !e.shiftKey) { - e.preventDefault(); - e.stopPropagation(); - this._commandService.executeCommand('inlineChat.submitInput'); - } else if (e.keyCode === KeyCode.Escape) { - // Hide overlay if input is empty - const value = this._input.getModel().getValue() ?? ''; - if (!value) { - e.preventDefault(); - e.stopPropagation(); - this.hide(); - } - } else if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { + if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { const model = this._input.getModel(); const position = this._input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { @@ -282,11 +252,11 @@ export class InlineChatInputWidget extends Disposable { * @param left Left offset relative to editor * @param anchorAbove Whether to anchor above the position (widget grows upward) */ - show(lineNumber: number, left: number, anchorAbove: boolean): void { + show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { this._showStore.clear(); // Clear input state - this._input.updateOptions({ wordWrap: 'off' }); + this._input.updateOptions({ wordWrap: 'off', placeholder }); this._input.getModel().setValue(''); // Store anchor info for scroll updates diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index b50a69f8752d0..37d4268342add 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -15,6 +15,7 @@ min-height: var(--vscode-inline-chat-affordance-height); line-height: var(--vscode-inline-chat-affordance-height); border: 1px solid var(--vscode-input-border, transparent); + z-index: 100; } .inline-chat-content-widget .action-label.codicon.codicon-light-bulb, diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 294867b3056ea..9acc9ecf81703 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -10,7 +10,7 @@ border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); border-radius: 8px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); - z-index: 10000; + z-index: 100; } .inline-chat-gutter-menu .input { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 99591004f8210..28c824b62beaf 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -109,6 +109,7 @@ export const CTX_INLINE_CHAT_EDITING = new RawContextKey('inlineChatEdi export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); export const CTX_INLINE_CHAT_INPUT_HAS_TEXT = new RawContextKey('inlineChatInputHasText', false, localize('inlineChatInputHasText', "Whether the inline chat input widget has text")); +export const CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED = new RawContextKey('inlineChatInputWidgetFocused', false, localize('inlineChatInputWidgetFocused', "Whether the inline chat input widget editor is focused")); export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); @@ -117,6 +118,7 @@ export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlin export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); +export const CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT = new RawContextKey('inlineChatFileBelongsToChat', false, localize('inlineChatFileBelongsToChat', "Whether the current file belongs to a chat editing session")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE) @@ -132,6 +134,7 @@ export const CTX_HOVER_MODE = ContextKeyExpr.equals('config.inlineChat.renderMod // --- (selected) action identifier export const ACTION_START = 'inlineChat.start'; +export const ACTION_ASK_IN_CHAT = 'inlineChat.askInChat'; export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; export const ACTION_DISCARD_CHANGES = 'inlineChat.discardHunkChange'; export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; From 00fe1591833dffa30360d73f3163608897619555 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 19 Feb 2026 16:46:12 +0100 Subject: [PATCH 22/24] sessions papercut fixes --- .vscode/sessions.json | 28 ++ .../browser/agentFeedback.contribution.ts | 1 + .../agentFeedbackEditorInputContribution.ts | 6 +- .../browser/agentFeedbackEditorOverlay.ts | 14 +- .../browser/agentFeedbackEditorUtils.ts | 11 +- .../agentFeedbackGlyphMarginContribution.ts | 189 ++++++++++++ .../browser/agentFeedbackService.ts | 284 +----------------- .../media/agentFeedbackGlyphMargin.css | 26 ++ .../aiCustomizationManagementEditor.ts | 29 +- .../contrib/chat/browser/chat.contribution.ts | 2 + .../contrib/chat/browser/newChatViewPane.ts | 5 +- .../contrib/chat/browser/runScriptAction.ts | 172 +++++------ .../browser/sessionsConfigurationService.ts | 148 +++++++++ .../browser/sessionsManagementService.ts | 46 +++ 14 files changed, 545 insertions(+), 416 deletions(-) create mode 100644 .vscode/sessions.json create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css create mode 100644 src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts diff --git a/.vscode/sessions.json b/.vscode/sessions.json new file mode 100644 index 0000000000000..539b14b5dd49e --- /dev/null +++ b/.vscode/sessions.json @@ -0,0 +1,28 @@ +{ + "scripts": [ + { + "name": "run Windows", + "command": "code.bat" + }, + { + "name": "run macOS", + "command": "code.sh" + }, + { + "name": "run Linux", + "command": "code.sh" + }, + { + "name": "run tests Windows", + "command": "test.bat" + }, + { + "name": "run tests macOS", + "command": "test.sh" + }, + { + "name": "run tests Linux", + "command": "test.sh" + } + ] +} diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts index 5b4028dda8e7b..82acbdb1c5be3 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './agentFeedbackEditorInputContribution.js'; +import './agentFeedbackGlyphMarginContribution.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { AgentFeedbackService, IAgentFeedbackService } from './agentFeedbackService.js'; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts index bcb3a079ad4be..7e17e30caf3d3 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts @@ -145,13 +145,13 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return; } - const match = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); - if (!match) { + const sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); + if (!sessionResource) { this._hide(); return; } - this._sessionResource = match.sessionResource; + this._sessionResource = sessionResource; this._show(); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts index 30c75a7556dfd..56cb43ad9347d 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts @@ -21,8 +21,10 @@ import { IAgentFeedbackService } from './agentFeedbackService.js'; import { navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from './agentFeedbackEditorActions.js'; import { assertType } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; -import { getActiveResourceCandidates } from './agentFeedbackEditorUtils.js'; +import { getActiveResourceCandidates, getSessionForResource } from './agentFeedbackEditorUtils.js'; import { Menus } from '../../../browser/menus.js'; +import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; class AgentFeedbackActionViewItem extends ActionViewItem { @@ -140,7 +142,9 @@ class AgentFeedbackOverlayController { container: HTMLElement, group: IEditorGroup, @IAgentFeedbackService agentFeedbackService: IAgentFeedbackService, + @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IInstantiationService instaService: IInstantiationService, + @IChatEditingService chatEditingService: IChatEditingService, ) { this._domNode.classList.add('agent-feedback-editor-overlay'); this._domNode.style.position = 'absolute'; @@ -176,18 +180,16 @@ class AgentFeedbackOverlayController { activeSignal.read(r); const candidates = getActiveResourceCandidates(group.activeEditorPane?.input); - let shouldShow = false; - let navigationBearings = { activeIdx: -1, totalCount: 0 }; + let navigationBearings = undefined; for (const candidate of candidates) { - const sessionResource = agentFeedbackService.getMostRecentSessionForResource(candidate); + const sessionResource = getSessionForResource(candidate, chatEditingService, agentSessionsService); if (sessionResource && agentFeedbackService.getFeedback(sessionResource).length > 0) { - shouldShow = true; navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource); break; } } - if (!shouldShow) { + if (!navigationBearings) { hide(); return; } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts index c4fc944e1bb6b..bb42a4ff24241 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts @@ -9,11 +9,6 @@ import { IChatEditingService } from '../../../../workbench/contrib/chat/common/e import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { agentSessionContainsResource, editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; -export interface ISessionResourceMatch { - readonly sessionResource: URI; - readonly resourceUri: URI; -} - /** * Find the session that contains the given resource by checking editing sessions and agent sessions. */ @@ -21,16 +16,16 @@ export function getSessionForResource( resourceUri: URI, chatEditingService: IChatEditingService, agentSessionsService: IAgentSessionsService, -): ISessionResourceMatch | undefined { +): URI | undefined { for (const editingSession of chatEditingService.editingSessionsObs.get()) { if (editingEntriesContainResource(editingSession.entries.get(), resourceUri)) { - return { sessionResource: editingSession.chatSessionResource, resourceUri }; + return editingSession.chatSessionResource; } } for (const session of agentSessionsService.model.sessions) { if (agentSessionContainsResource(session, resourceUri)) { - return { sessionResource: session.resource, resourceUri }; + return session.resource; } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts new file mode 100644 index 0000000000000..6b45bec865e75 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/agentFeedbackGlyphMargin.css'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; +import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; +import { GlyphMarginLane, IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../editor/common/model.js'; +import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IAgentFeedbackService } from './agentFeedbackService.js'; +import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { getSessionForResource } from './agentFeedbackEditorUtils.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; + +const GLYPH_MARGIN_LANE = GlyphMarginLane.Left; + +const feedbackGlyphDecoration = ModelDecorationOptions.register({ + description: 'agent-feedback-glyph', + glyphMarginClassName: `${ThemeIcon.asClassName(Codicon.comment)} agent-feedback-glyph`, + glyphMargin: { position: GLYPH_MARGIN_LANE }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, +}); + +const addFeedbackHintDecoration = ModelDecorationOptions.register({ + description: 'agent-feedback-add-hint', + glyphMarginClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, + glyphMargin: { position: GLYPH_MARGIN_LANE }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, +}); + +export class AgentFeedbackGlyphMarginContribution extends Disposable implements IEditorContribution { + + static readonly ID = 'agentFeedback.glyphMarginContribution'; + + private readonly _feedbackDecorations = this._editor.createDecorationsCollection(); + + private _hintDecorationId: string | null = null; + private _hintLine = -1; + private _sessionResource: URI | undefined; + private _feedbackLines = new Set(); + + constructor( + private readonly _editor: ICodeEditor, + @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + ) { + super(); + + this._store.add(this._agentFeedbackService.onDidChangeFeedback(() => this._updateFeedbackDecorations())); + this._store.add(this._editor.onDidChangeModel(() => this._onModelChanged())); + this._store.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onMouseMove(e))); + this._store.add(this._editor.onMouseLeave(() => this._updateHintDecoration(-1))); + this._store.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onMouseDown(e))); + + this._resolveSession(); + this._updateFeedbackDecorations(); + } + + private _onModelChanged(): void { + this._updateHintDecoration(-1); + this._resolveSession(); + this._updateFeedbackDecorations(); + } + + private _resolveSession(): void { + const model = this._editor.getModel(); + if (!model) { + this._sessionResource = undefined; + return; + } + this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); + } + + private _updateFeedbackDecorations(): void { + if (!this._sessionResource) { + this._feedbackDecorations.clear(); + this._feedbackLines.clear(); + return; + } + + const feedbackItems = this._agentFeedbackService.getFeedback(this._sessionResource); + const decorations: IModelDeltaDecoration[] = []; + const lines = new Set(); + + for (const item of feedbackItems) { + const model = this._editor.getModel(); + if (!model || item.resourceUri.toString() !== model.uri.toString()) { + continue; + } + + const line = item.range.startLineNumber; + lines.add(line); + decorations.push({ + range: new Range(line, 1, line, 1), + options: feedbackGlyphDecoration, + }); + } + + this._feedbackLines = lines; + this._feedbackDecorations.set(decorations); + } + + private _onMouseMove(e: IEditorMouseEvent): void { + if (!this._sessionResource) { + this._updateHintDecoration(-1); + return; + } + + if (e.target.position + && e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN + && !e.target.detail.isAfterLines + && !this._feedbackLines.has(e.target.position.lineNumber) + ) { + this._updateHintDecoration(e.target.position.lineNumber); + } else { + this._updateHintDecoration(-1); + } + } + + private _updateHintDecoration(line: number): void { + if (line === this._hintLine) { + return; + } + + this._hintLine = line; + this._editor.changeDecorations(accessor => { + if (this._hintDecorationId) { + accessor.removeDecoration(this._hintDecorationId); + this._hintDecorationId = null; + } + if (line !== -1) { + this._hintDecorationId = accessor.addDecoration( + new Range(line, 1, line, 1), + addFeedbackHintDecoration, + ); + } + }); + } + + private _onMouseDown(e: IEditorMouseEvent): void { + if (!e.target.position + || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN + || e.target.detail.isAfterLines + || !this._sessionResource + ) { + return; + } + + const lineNumber = e.target.position.lineNumber; + + // Lines with existing feedback - do nothing + if (this._feedbackLines.has(lineNumber)) { + return; + } + + // Select the line content and focus the editor + const model = this._editor.getModel(); + if (!model) { + return; + } + + const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); + const endColumn = model.getLineLastNonWhitespaceColumn(lineNumber); + if (startColumn === 0 || endColumn === 0) { + // Empty line - select the whole line range + this._editor.setSelection(new Selection(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber))); + } else { + this._editor.setSelection(new Selection(lineNumber, startColumn, lineNumber, endColumn)); + } + this._editor.focus(); + } + + override dispose(): void { + this._feedbackDecorations.clear(); + this._updateHintDecoration(-1); + super.dispose(); + } +} + +registerEditorContribution(AgentFeedbackGlyphMarginContribution.ID, AgentFeedbackGlyphMarginContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 19f5baf47c227..701495fc188b9 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -7,15 +7,8 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { Comment, CommentThread, CommentThreadCollapsibleState, CommentThreadState, CommentInput } from '../../../../editor/common/languages.js'; -import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICommentController, ICommentInfo, ICommentService, INotebookCommentInfo } from '../../../../workbench/contrib/comments/browser/commentService.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { localize } from '../../../../nls.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; @@ -90,8 +83,6 @@ export interface IAgentFeedbackService { // --- Implementation ----------------------------------------------------------- -const AGENT_FEEDBACK_OWNER = 'agentFeedbackController'; -const AGENT_FEEDBACK_CONTEXT_VALUE = 'agentFeedback'; const AGENT_FEEDBACK_ATTACHMENT_ID_PREFIX = 'agentFeedback:'; export class AgentFeedbackService extends Disposable implements IAgentFeedbackService { @@ -109,11 +100,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe private _sessionUpdatedSequence = 0; private readonly _navigationAnchorBySession = new Map(); - private _controllerRegistered = false; - private _nextThreadHandle = 1; - constructor( - @ICommentService private readonly _commentService: ICommentService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @@ -153,81 +140,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe })); } - private _ensureController(): void { - if (this._controllerRegistered) { - return; - } - this._controllerRegistered = true; - - const self = this; - - const controller: ICommentController = { - id: AGENT_FEEDBACK_OWNER, - label: 'Agent Feedback', - features: {}, - contextValue: AGENT_FEEDBACK_CONTEXT_VALUE, - owner: AGENT_FEEDBACK_OWNER, - activeComment: undefined, - createCommentThreadTemplate: async () => { }, - updateCommentThreadTemplate: async () => { }, - deleteCommentThreadMain: () => { }, - toggleReaction: async () => { }, - getDocumentComments: async (resource: URI, _token: CancellationToken): Promise> => { - // Return threads for this resource from all sessions - const threads: CommentThread[] = []; - for (const [, sessionFeedback] of self._feedbackBySession) { - for (const f of sessionFeedback) { - if (f.resourceUri.toString() === resource.toString()) { - threads.push(self._createThread(f)); - } - } - } - return { - threads, - commentingRanges: { ranges: [], resource, fileComments: false }, - uniqueOwner: AGENT_FEEDBACK_OWNER, - }; - }, - getNotebookComments: async (_resource: URI, _token: CancellationToken): Promise => { - return { threads: [], uniqueOwner: AGENT_FEEDBACK_OWNER }; - }, - setActiveCommentAndThread: async () => { }, - }; - - this._commentService.registerCommentController(AGENT_FEEDBACK_OWNER, controller); - this._store.add({ dispose: () => this._commentService.unregisterCommentController(AGENT_FEEDBACK_OWNER) }); - - // Register delete action for our feedback threads - this._store.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: 'agentFeedback.deleteThread', - title: localize('agentFeedback.delete', "Delete Feedback"), - icon: Codicon.trash, - menu: { - id: MenuId.CommentThreadTitle, - when: ContextKeyExpr.equals('commentController', AGENT_FEEDBACK_CONTEXT_VALUE), - group: 'navigation', - } - }); - } - run(accessor: ServicesAccessor, ...args: unknown[]): void { - const agentFeedbackService = accessor.get(IAgentFeedbackService); - const arg = args[0] as { thread?: { threadId?: string }; threadId?: string } | undefined; - const thread = arg?.thread ?? arg; - if (thread?.threadId) { - const sessionResource = self._findSessionForFeedback(thread.threadId); - if (sessionResource) { - agentFeedbackService.removeFeedback(sessionResource, thread.threadId); - } - } - } - })); - } - addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string): IAgentFeedback { - this._ensureController(); - const key = sessionResource.toString(); let feedbackItems = this._feedbackBySession.get(key); if (!feedbackItems) { @@ -246,7 +159,6 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._sessionUpdatedOrder.set(key, ++this._sessionUpdatedSequence); this._onDidChangeNavigation.fire(sessionResource); - this._syncThreads(sessionResource); this._onDidChangeFeedback.fire({ sessionResource, feedbackItems }); return feedback; @@ -261,9 +173,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe const idx = feedbackItems.findIndex(f => f.id === feedbackId); if (idx >= 0) { - const removed = feedbackItems[idx]; feedbackItems.splice(idx, 1); - this._activeThreadIds.delete(feedbackId); if (this._navigationAnchorBySession.get(key) === feedbackId) { this._navigationAnchorBySession.delete(key); this._onDidChangeNavigation.fire(sessionResource); @@ -274,34 +184,10 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._sessionUpdatedOrder.delete(key); } - // Fire updateComments with the thread in removed[] so the editor - // controller's onDidUpdateCommentThreads handler removes the zone widget - const thread = this._createThread(removed); - thread.isDisposed = true; - this._commentService.updateComments(AGENT_FEEDBACK_OWNER, { - added: [], - removed: [thread], - changed: [], - pending: [], - }); - this._onDidChangeFeedback.fire({ sessionResource, feedbackItems }); } } - /** - * Find which session a feedback item belongs to by its ID. - */ - _findSessionForFeedback(feedbackId: string): URI | undefined { - for (const [, feedbackItems] of this._feedbackBySession) { - const item = feedbackItems.find(f => f.id === feedbackId); - if (item) { - return item.sessionResource; - } - } - return undefined; - } - getFeedback(sessionResource: URI): readonly IAgentFeedback[] { return this._feedbackBySession.get(sessionResource.toString()) ?? []; } @@ -393,178 +279,10 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe clearFeedback(sessionResource: URI): void { const key = sessionResource.toString(); - const feedbackItems = this._feedbackBySession.get(key); - if (feedbackItems && feedbackItems.length > 0) { - const removedThreads = feedbackItems.map(f => { - this._activeThreadIds.delete(f.id); - const thread = this._createThread(f); - thread.isDisposed = true; - return thread; - }); - - this._commentService.updateComments(AGENT_FEEDBACK_OWNER, { - added: [], - removed: removedThreads, - changed: [], - pending: [], - }); - } this._feedbackBySession.delete(key); this._sessionUpdatedOrder.delete(key); this._navigationAnchorBySession.delete(key); this._onDidChangeNavigation.fire(sessionResource); this._onDidChangeFeedback.fire({ sessionResource, feedbackItems: [] }); } - - /** Threads currently known to the comment service, keyed by feedback id */ - private readonly _activeThreadIds = new Set(); - - /** - * Sync feedback threads to the ICommentService using updateComments for - * incremental add/remove, which the editor controller listens to. - */ - private _syncThreads(_sessionResource: URI): void { - // Collect all current feedback IDs - const currentIds = new Set(); - const allFeedback: IAgentFeedback[] = []; - for (const [, sessionFeedback] of this._feedbackBySession) { - for (const f of sessionFeedback) { - currentIds.add(f.id); - allFeedback.push(f); - } - } - - // Determine added and removed - const added: CommentThread[] = []; - const removed: CommentThread[] = []; - - for (const f of allFeedback) { - if (!this._activeThreadIds.has(f.id)) { - added.push(this._createThread(f)); - } - } - - for (const id of this._activeThreadIds) { - if (!currentIds.has(id)) { - // Create a minimal thread just for removal (needs threadId and resource) - removed.push(this._createRemovedThread(id)); - } - } - - // Update tracking - this._activeThreadIds.clear(); - for (const id of currentIds) { - this._activeThreadIds.add(id); - } - - if (added.length || removed.length) { - this._commentService.updateComments(AGENT_FEEDBACK_OWNER, { - added, - removed, - changed: [], - pending: [], - }); - } - } - - private _createRemovedThread(feedbackId: string): CommentThread { - const noopEvent = Event.None; - return { - isDocumentCommentThread(): this is CommentThread { return true; }, - commentThreadHandle: -1, - controllerHandle: 0, - threadId: feedbackId, - resource: null, - range: undefined, - label: undefined, - contextValue: undefined, - comments: undefined, - onDidChangeComments: noopEvent, - collapsibleState: CommentThreadCollapsibleState.Collapsed, - initialCollapsibleState: CommentThreadCollapsibleState.Collapsed, - onDidChangeInitialCollapsibleState: noopEvent, - state: undefined, - applicability: undefined, - canReply: false, - input: undefined, - onDidChangeInput: noopEvent, - onDidChangeLabel: noopEvent, - onDidChangeCollapsibleState: noopEvent, - onDidChangeState: noopEvent, - onDidChangeCanReply: noopEvent, - isDisposed: true, - isTemplate: false, - }; - } - - private _createThread(feedback: IAgentFeedback): CommentThread { - const handle = this._nextThreadHandle++; - - const threadComment: Comment = { - uniqueIdInThread: 1, - body: feedback.text, - userName: 'You', - }; - - return new AgentFeedbackThread(handle, feedback.id, feedback.resourceUri.toString(), feedback.range, [threadComment]); - } -} - -/** - * A CommentThread implementation with proper emitters so the editor - * comment controller can react to state changes (collapse/expand). - */ -class AgentFeedbackThread implements CommentThread { - - private readonly _onDidChangeComments = new Emitter(); - readonly onDidChangeComments = this._onDidChangeComments.event; - - private readonly _onDidChangeCollapsibleState = new Emitter(); - readonly onDidChangeCollapsibleState = this._onDidChangeCollapsibleState.event; - - private readonly _onDidChangeInitialCollapsibleState = new Emitter(); - readonly onDidChangeInitialCollapsibleState = this._onDidChangeInitialCollapsibleState.event; - - private readonly _onDidChangeInput = new Emitter(); - readonly onDidChangeInput = this._onDidChangeInput.event; - - private readonly _onDidChangeLabel = new Emitter(); - readonly onDidChangeLabel = this._onDidChangeLabel.event; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState = this._onDidChangeState.event; - - private readonly _onDidChangeCanReply = new Emitter(); - readonly onDidChangeCanReply = this._onDidChangeCanReply.event; - - readonly controllerHandle = 0; - readonly label = undefined; - readonly contextValue = undefined; - readonly applicability = undefined; - readonly input = undefined; - readonly isTemplate = false; - - private _collapsibleState = CommentThreadCollapsibleState.Collapsed; - get collapsibleState(): CommentThreadCollapsibleState { return this._collapsibleState; } - set collapsibleState(value: CommentThreadCollapsibleState) { - this._collapsibleState = value; - this._onDidChangeCollapsibleState.fire(value); - } - - readonly initialCollapsibleState = CommentThreadCollapsibleState.Collapsed; - readonly state = CommentThreadState.Unresolved; - readonly canReply = false; - isDisposed = false; - - constructor( - readonly commentThreadHandle: number, - readonly threadId: string, - readonly resource: string, - readonly range: IRange, - readonly comments: readonly Comment[], - ) { } - - isDocumentCommentThread(): this is CommentThread { - return true; - } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css new file mode 100644 index 0000000000000..16aac443e4802 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackGlyphMargin.css @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-glyph, +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint { + border-radius: 3px; + display: flex !important; + align-items: center; + justify-content: center; +} + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-glyph { + background-color: var(--vscode-editorGutter-commentGlyphForeground, var(--vscode-icon-foreground)); + color: var(--vscode-editor-background); +} + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint { + background-color: var(--vscode-toolbar-hoverBackground); + opacity: 0.7; +} + +.monaco-editor .glyph-margin-widgets .cgmr.agent-feedback-add-hint:hover { + opacity: 1; +} diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts index 4571e7b0e0eca..caaf09e7ca242 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts @@ -48,7 +48,6 @@ import { SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, CONTENT_MIN_WIDTH, - getActiveSessionRoot, } from './aiCustomizationManagement.js'; import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; import { ChatModelsWidget } from '../../../../workbench/contrib/chat/browser/chatManagement/chatModelsWidget.js'; @@ -58,9 +57,7 @@ import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, import { showConfigureHooksQuickPick } from '../../../../workbench/contrib/chat/browser/promptSyntax/hookActions.js'; import { CustomizationCreatorService } from './customizationCreatorService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IWorkingCopyService } from '../../../../workbench/services/workingCopy/common/workingCopyService.js'; const $ = DOM.$; @@ -148,7 +145,7 @@ export class AICustomizationManagementEditor extends EditorPane { private editorSaveIndicator!: HTMLElement; private readonly editorModelChangeDisposables = this._register(new DisposableStore()); private currentEditingUri: URI | undefined; - private currentWorktreeUri: URI | undefined; + private currentActiveSession: IActiveSessionItem | undefined; private currentEditingIsWorktree = false; private currentModelRef: IReference | undefined; private viewMode: 'list' | 'editor' = 'list'; @@ -177,7 +174,6 @@ export class AICustomizationManagementEditor extends EditorPane { @ILayoutService private readonly layoutService: ILayoutService, @ICommandService private readonly commandService: ICommandService, @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, - @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -192,7 +188,7 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode !== 'editor' || !this.currentEditingIsWorktree) { return; } - this.currentWorktreeUri = getActiveSessionRoot(this.activeSessionService); + this.currentActiveSession = this.activeSessionService.getActiveSession() ?? undefined; })); // Safety disposal for the embedded editor model reference @@ -547,8 +543,7 @@ export class AICustomizationManagementEditor extends EditorPane { this.editorItemPathElement.textContent = basename(uri); // Track worktree URI for auto-commit on close - const worktreeDir = getActiveSessionRoot(this.activeSessionService); - this.currentWorktreeUri = isWorktreeFile ? worktreeDir : undefined; + this.currentActiveSession = isWorktreeFile ? this.activeSessionService.getActiveSession() ?? undefined : undefined; this.currentEditingIsWorktree = isWorktreeFile; // Update visibility @@ -593,16 +588,16 @@ export class AICustomizationManagementEditor extends EditorPane { private goBackToList(): void { // Auto-commit worktree files when leaving the embedded editor const fileUri = this.currentEditingUri; - const worktreeUri = this.currentWorktreeUri; - if (fileUri && worktreeUri) { - this.commitWorktreeFile(worktreeUri, fileUri); + const session = this.currentActiveSession; + if (fileUri && session) { + this.commitWorktreeFile(session, fileUri); } // Dispose model reference this.currentModelRef?.dispose(); this.currentModelRef = undefined; this.currentEditingUri = undefined; - this.currentWorktreeUri = undefined; + this.currentActiveSession = undefined; this.currentEditingIsWorktree = false; this.editorModelChangeDisposables.clear(); this.clearSaveIndicator(); @@ -786,12 +781,8 @@ export class AICustomizationManagementEditor extends EditorPane { /** * Commits a worktree file via the extension and refreshes the Changes view. */ - private async commitWorktreeFile(worktreeUri: URI, fileUri: URI): Promise { - await this.commandService.executeCommand( - 'github.copilot.cli.sessions.commitToWorktree', - { worktreeUri, fileUri } - ); - await this.agentSessionsService.model.resolve(AgentSessionProviders.Background); + private async commitWorktreeFile(session: IActiveSessionItem, fileUri: URI): Promise { + await this.activeSessionService.commitWorktreeFiles(session, [fileUri]); this.refreshList(); } diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index b17a408300976..a00c358a7b9cf 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -26,6 +26,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { AgenticPromptsService } from './promptsService.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ISessionsConfigurationService, SessionsConfigurationService } from './sessionsConfigurationService.js'; import { ChatViewContainerId, ChatViewId } from '../../../../workbench/contrib/chat/browser/chat.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { NewChatViewPane, SessionsViewId } from './newChatViewPane.js'; @@ -213,3 +214,4 @@ registerWorkbenchContribution2(RunScriptContribution.ID, RunScriptContribution, // register services registerSingleton(IPromptsService, AgenticPromptsService, InstantiationType.Delayed); +registerSingleton(ISessionsConfigurationService, SessionsConfigurationService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index e8842b2956c38..a8c69daa78eb9 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -12,7 +12,6 @@ import { Separator, toAction } from '../../../../base/common/actions.js'; import { Radio } from '../../../../base/browser/ui/radio/radio.js'; import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; @@ -237,6 +236,7 @@ class NewChatWidget extends Disposable { @IFileDialogService private readonly fileDialogService: IFileDialogService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, @IStorageService private readonly storageService: IStorageService, + @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, ) { super(); this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments)); @@ -383,8 +383,7 @@ class NewChatWidget extends Disposable { }); this._pendingSessionResources.set(target, this._pendingSessionResource); - // Create the session in the extension so that session options can be stored - this.chatSessionsService.getOrCreateChatSession(this._pendingSessionResource, CancellationToken.None) + this.sessionsManagementService.createNewPendingSession(this._pendingSessionResource,) .catch((err) => this.logService.trace('Failed to create pending session:', err)); } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index 6138f48d98e69..914bfe0396189 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -5,22 +5,20 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, IObservable, observableSignal } from '../../../../base/common/observable.js'; +import { autorun, derived, IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../nls.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { Menus } from '../../../browser/menus.js'; +import { ISessionsConfigurationService, ISessionScript } from './sessionsConfigurationService.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -// Storage keys -const STORAGE_KEY_DEFAULT_RUN_ACTION = 'workbench.agentSessions.defaultRunAction'; - // Menu IDs - exported for use in auxiliary bar part export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdown'); @@ -28,15 +26,9 @@ export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdow const RUN_SCRIPT_ACTION_ID = 'workbench.action.agentSessions.runScript'; const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction'; -// Types for stored default action -interface IStoredRunAction { - readonly name: string; - readonly command: string; -} - interface IRunScriptActionContext { - readonly storageKey: string; - readonly action: IStoredRunAction | undefined; + readonly session: IActiveSessionItem; + readonly scripts: readonly ISessionScript[]; readonly cwd: URI; } @@ -49,149 +41,141 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr static readonly ID = 'workbench.contrib.agentSessions.runScript'; private readonly _activeRunState: IObservable; - private readonly _updateSignal = observableSignal(this); constructor( - @IStorageService private readonly _storageService: IStorageService, @ITerminalService private readonly _terminalService: ITerminalService, - @ISessionsManagementService activeSessionService: ISessionsManagementService, + @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, @IQuickInputService private readonly _quickInputService: IQuickInputService, + @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, ) { super(); this._activeRunState = derived(this, reader => { - const activeSession = activeSessionService.activeSession.read(reader); - if (!activeSession || !activeSession.repository) { + const activeSession = this._activeSessionService.activeSession.read(reader); + const cwd = activeSession?.worktree ?? activeSession?.repository; + if (!activeSession || !cwd) { return undefined; } - this._updateSignal.read(reader); - const storageKey = `${STORAGE_KEY_DEFAULT_RUN_ACTION}.${activeSession.repository.toString()}`; - const action = this._getStoredDefaultAction(storageKey); - - return { - storageKey, - action, - cwd: activeSession.worktree ?? activeSession.repository - }; + const scripts = this._sessionsConfigService.getScripts(activeSession).read(reader); + return { session: activeSession, scripts, cwd }; }); this._registerActions(); } - private _getStoredDefaultAction(storageKey: string): IStoredRunAction | undefined { - const stored = this._storageService.get(storageKey, StorageScope.WORKSPACE); - if (stored) { - try { - const parsed = JSON.parse(stored); - if (typeof parsed?.name === 'string' && typeof parsed?.command === 'string') { - return parsed; - } - } catch { - return undefined; - } - } - return undefined; - } - - private _setStoredDefaultAction(storageKey: string, action: IStoredRunAction): void { - this._storageService.store(storageKey, JSON.stringify(action), StorageScope.WORKSPACE, StorageTarget.MACHINE); - this._updateSignal.trigger(undefined); - } - private _registerActions(): void { const that = this; - // Main play action this._register(autorun(reader => { const activeSession = this._activeRunState.read(reader); if (!activeSession) { return; } - const title = activeSession.action ? activeSession.action.name : localize('runScriptNoAction', "Run Script"); - const tooltip = activeSession.action ? - localize('runScriptTooltip', "Run '{0}' in terminal", activeSession.action.name) - : localize('runScriptTooltipNoAction', "Configure run action"); - - reader.store.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: RUN_SCRIPT_ACTION_ID, - title: title, - tooltip: tooltip, - icon: Codicon.play, - category: localize2('agentSessions', 'Agent Sessions'), - menu: [{ - id: RunScriptDropdownMenuId, - group: 'navigation', - order: 0, - }] - }); - } + const { scripts, cwd, session } = activeSession; + const configureScriptPrecondition = session.worktree ? ContextKeyExpr.true() : ContextKeyExpr.false(); + + if (scripts.length === 0) { + // No scripts configured - show a "Run Script" button that opens the configure quick pick + reader.store.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: RUN_SCRIPT_ACTION_ID, + title: localize('runScriptNoAction', "Run Script"), + tooltip: localize('runScriptTooltipNoAction', "Configure run action"), + icon: Codicon.play, + category: localize2('agentSessions', 'Agent Sessions'), + precondition: configureScriptPrecondition, + menu: [{ + id: RunScriptDropdownMenuId, + when: configureScriptPrecondition, + group: 'navigation', + order: 0, + }] + }); + } - async run(): Promise { - if (activeSession.action) { - await that._runScript(activeSession.cwd, activeSession.action); - } else { - // Open quick pick to configure run action - await that._showConfigureQuickPick(activeSession); + async run(): Promise { + await that._showConfigureQuickPick(session, cwd); } + })); + } else { + // Register an action for each script + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + const actionId = `${RUN_SCRIPT_ACTION_ID}.${i}`; + + reader.store.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: actionId, + title: script.name, + tooltip: localize('runScriptTooltip', "Run '{0}' in terminal", script.name), + icon: Codicon.play, + category: localize2('agentSessions', 'Agent Sessions'), + menu: [{ + id: RunScriptDropdownMenuId, + group: '0_scripts', + order: i, + }] + }); + } + + async run(): Promise { + await that._runScript(cwd, script); + } + })); } - })); + } - // Configure run action (shown in dropdown) + // Configure run action (always shown in dropdown) reader.store.add(registerAction2(class extends Action2 { constructor() { super({ id: CONFIGURE_DEFAULT_RUN_ACTION_ID, - title: localize2('configureDefaultRunAction', "Configure Run Action..."), + title: localize2('configureDefaultRunAction', "Add Run Script..."), category: localize2('agentSessions', 'Agent Sessions'), - icon: Codicon.play, + icon: Codicon.add, + precondition: configureScriptPrecondition, menu: [{ id: RunScriptDropdownMenuId, - group: '0_configure', + group: '1_configure', order: 0 }] }); } async run(): Promise { - await that._showConfigureQuickPick(activeSession); + await that._showConfigureQuickPick(session, cwd); } })); })); } - private async _showConfigureQuickPick(activeSession: IRunScriptActionContext): Promise { - - // Show input box for command + private async _showConfigureQuickPick(session: IActiveSessionItem, cwd: URI): Promise { const command = await this._quickInputService.input({ placeHolder: localize('enterCommandPlaceholder', "Enter command (e.g., npm run dev)"), prompt: localize('enterCommandPrompt', "This command will be run in the integrated terminal") }); if (command) { - const storedAction: IStoredRunAction = { - name: command, - command - }; - this._setStoredDefaultAction(activeSession.storageKey, storedAction); - await this._runScript(activeSession.cwd, storedAction); + const script: ISessionScript = { name: command, command }; + await this._sessionsConfigService.addScript(script, session); + await this._runScript(cwd, script); } } - private async _runScript(cwd: URI, action: IStoredRunAction): Promise { - // Create a new terminal and run the command + private async _runScript(cwd: URI, script: ISessionScript): Promise { const terminal = await this._terminalService.createTerminal({ location: TerminalLocation.Panel, config: { - name: action.name + name: script.name }, cwd }); - terminal.sendText(action.command, true); + terminal.sendText(script.command, true); await this._terminalService.revealTerminal(terminal); } } diff --git a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts new file mode 100644 index 0000000000000..f2ac0049198c3 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableSignal, observableValue } from '../../../../base/common/observable.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; + +const SESSIONS_CONFIG_RELATIVE = '.vscode/sessions.json'; + +export interface ISessionScript { + readonly name: string; + readonly command: string; +} + +function isISessionScript(s: unknown): s is ISessionScript { + return typeof s === 'object' && s !== null && + typeof (s as ISessionScript).name === 'string' && + typeof (s as ISessionScript).command === 'string'; +} + +export interface ISessionsConfigurationService { + readonly _serviceBrand: undefined; + + /** + * Observable list of scripts for the active session. + * Automatically reloads when the active session changes or the file is modified. + */ + getScripts(session: IActiveSessionItem): IObservable; + + /** Append a script to the session's config file. */ + addScript(script: ISessionScript, session: IActiveSessionItem): Promise; + + /** Remove a script from the session's config file. */ + removeScript(script: ISessionScript, session: IActiveSessionItem): Promise; +} + +export const ISessionsConfigurationService = createDecorator('sessionsConfigurationService'); + +export class SessionsConfigurationService extends Disposable implements ISessionsConfigurationService { + + declare readonly _serviceBrand: undefined; + + private readonly _scripts = observableValue(this, []); + private readonly _refreshSignal = observableSignal(this); + + constructor( + @IFileService private readonly _fileService: IFileService, + @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + // Watch active session changes + file changes, load scripts reactively + this._register(autorun(reader => { + const activeSession = this._activeSessionService.activeSession.read(reader); + this._refreshSignal.read(reader); + + if (!activeSession) { + this._scripts.set([], undefined); + return; + } + + const configUri = this._getConfigFileUri(activeSession); + if (!configUri) { + this._scripts.set([], undefined); + return; + } + + // Watch the file for external changes + reader.store.add(this._fileService.watch(configUri)); + reader.store.add(this._fileService.onDidFilesChange(e => { + if (e.contains(configUri)) { + this._refreshSignal.trigger(undefined); + } + })); + + // Read the file (async, updates _scripts when done) + this._readScripts(configUri).then( + scripts => this._scripts.set(scripts, undefined), + () => this._scripts.set([], undefined), + ); + })); + } + + getScripts(_session: IActiveSessionItem): IObservable { + return this._scripts; + } + + async addScript(script: ISessionScript, session: IActiveSessionItem): Promise { + const uri = this._getConfigFileUri(session); + if (!uri) { + return; + } + + const current = await this._readScripts(uri); + const updated = [...current, script]; + await this._writeScripts(uri, updated, session); + } + + async removeScript(script: ISessionScript, session: IActiveSessionItem): Promise { + const uri = this._getConfigFileUri(session); + if (!uri) { + return; + } + + const current = await this._readScripts(uri); + const updated = current.filter(s => s.name !== script.name || s.command !== script.command); + await this._writeScripts(uri, updated, session); + } + + private _getConfigFileUri(session: IActiveSessionItem): URI | undefined { + const root = session.worktree ?? session.repository; + if (!root) { + return undefined; + } + return joinPath(root, SESSIONS_CONFIG_RELATIVE); + } + + private async _readScripts(uri: URI): Promise { + try { + const content = await this._fileService.readFile(uri); + const parsed = JSON.parse(content.value.toString()); + if (parsed && Array.isArray(parsed.scripts)) { + return parsed.scripts.filter(isISessionScript); + } + } catch { + // File doesn't exist or is malformed - return empty + } + return []; + } + + private async _writeScripts(uri: URI, scripts: readonly ISessionScript[], session: IActiveSessionItem): Promise { + const data = JSON.stringify({ scripts }, null, '\t'); + await this._fileService.writeFile(uri, VSBuffer.fromString(data)); + this._logService.trace(`[SessionsConfigurationService] Wrote ${scripts.length} script(s) to ${uri.toString()}`); + + await this._activeSessionService.commitWorktreeFiles(session, [uri]); + this._refreshSignal.trigger(undefined); + } +} diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index b25676606aa52..86a963db393c8 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -20,9 +20,12 @@ import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/con import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { LocalChatSessionUri } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { localize } from '../../../../nls.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -73,11 +76,22 @@ export interface ISessionsManagementService { */ openNewSession(): void; + /** + * Create a new session and set it as active, without opening a chat view. + */ + createNewPendingSession(pendingSessionResource: URI): Promise; + /** * Open a new session, apply options, and send the initial request. * This is the main entry point for the new-chat welcome widget. */ sendRequestForNewSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap, folderUri?: URI): Promise; + + /** + * Commit files in a worktree and refresh the agent sessions model + * so the Changes view reflects the update. + */ + commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]): Promise; } export const ISessionsManagementService = createDecorator('sessionsManagementService'); @@ -104,6 +118,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IViewsService private readonly viewsService: IViewsService, + @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -224,6 +239,23 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } + async createNewPendingSession(pendingSessionResource: URI): Promise { + const chatsSession = await this.chatSessionsService.getOrCreateChatSession(pendingSessionResource, CancellationToken.None); + const chatSessionItem: IChatSessionItem = { + resource: chatsSession.sessionResource, + label: localize('sessionsManagement.newPendingAgentSessionLabel', 'Pending Session'), + timing: { + created: Date.now(), + lastRequestStarted: undefined, + lastRequestEnded: undefined, + } + }; + const repository = this.getRepositoryFromSessionOption(chatsSession.sessionResource); + const activeSessionItem = { ...chatSessionItem, repository, worktree: undefined }; + this._activeSession.set(activeSessionItem, undefined); + return activeSessionItem; + } + /** * Open an existing agent session - set it as active and reveal it. */ @@ -392,6 +424,20 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this._activeSession.set(activeSessionItem, undefined); } + async commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]): Promise { + const worktreeUri = session.worktree; + if (!worktreeUri) { + throw new Error('Cannot commit worktree files: active session has no associated worktree'); + } + for (const fileUri of fileUris) { + await this.commandService.executeCommand( + 'github.copilot.cli.sessions.commitToWorktree', + { worktreeUri, fileUri } + ); + } + await this.agentSessionsService.model.resolve(AgentSessionProviders.Background); + } + private loadLastSelectedSession(): URI | undefined { const cached = this.storageService.get(LAST_SELECTED_SESSION_KEY, StorageScope.WORKSPACE); if (!cached) { From f5688a3c75b2bdf1375602bed7bc6f30df9b770e Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 19 Feb 2026 16:48:53 +0100 Subject: [PATCH 23/24] we now allow this caracter --- build/hygiene.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/hygiene.ts b/build/hygiene.ts index 8778907f13f63..936b0cbe63063 100644 --- a/build/hygiene.ts +++ b/build/hygiene.ts @@ -70,7 +70,7 @@ export function hygiene(some: NodeJS.ReadWriteStream | string[] | undefined, run } // Please do not add symbols that resemble ASCII letters! // eslint-disable-next-line no-misleading-character-class - const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); + const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷—·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); if (m) { console.error( file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line` From b5ce399c365d8764e57f1146d94488789076b863 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 19 Feb 2026 16:58:01 +0100 Subject: [PATCH 24/24] fix --- .../browser/agentFeedbackGlyphMarginContribution.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts index 6b45bec865e75..6436e692fc2a8 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackGlyphMarginContribution.ts @@ -40,7 +40,7 @@ export class AgentFeedbackGlyphMarginContribution extends Disposable implements static readonly ID = 'agentFeedback.glyphMarginContribution'; - private readonly _feedbackDecorations = this._editor.createDecorationsCollection(); + private readonly _feedbackDecorations; private _hintDecorationId: string | null = null; private _hintLine = -1; @@ -55,6 +55,8 @@ export class AgentFeedbackGlyphMarginContribution extends Disposable implements ) { super(); + this._feedbackDecorations = this._editor.createDecorationsCollection(); + this._store.add(this._agentFeedbackService.onDidChangeFeedback(() => this._updateFeedbackDecorations())); this._store.add(this._editor.onDidChangeModel(() => this._onModelChanged())); this._store.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onMouseMove(e)));