From 9670c208f75c3dba89bf8b2f99656dbd00ef8a68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:08:52 +0000 Subject: [PATCH 001/448] Initial plan From 26d8fe005c1a9c3611178628b8a2fb617588b878 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:19:22 +0000 Subject: [PATCH 002/448] Add minimal border-radius for hovers with pointers Co-authored-by: mrleemurray <25487940+mrleemurray@users.noreply.github.com> --- src/vs/platform/hover/browser/hover.css | 4 ++++ src/vs/platform/hover/browser/hoverWidget.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/vs/platform/hover/browser/hover.css b/src/vs/platform/hover/browser/hover.css index 597738d3069..7110be193b1 100644 --- a/src/vs/platform/hover/browser/hover.css +++ b/src/vs/platform/hover/browser/hover.css @@ -20,6 +20,10 @@ box-shadow: 0 2px 8px var(--vscode-widget-shadow); } +.monaco-hover.workbench-hover.with-pointer { + border-radius: 3px; +} + .monaco-hover.workbench-hover .monaco-action-bar .action-item .codicon { /* Given our font-size, adjust action icons accordingly */ width: 13px; diff --git a/src/vs/platform/hover/browser/hoverWidget.ts b/src/vs/platform/hover/browser/hoverWidget.ts index f897c073bdb..3c397e020a4 100644 --- a/src/vs/platform/hover/browser/hoverWidget.ts +++ b/src/vs/platform/hover/browser/hoverWidget.ts @@ -128,6 +128,9 @@ export class HoverWidget extends Widget implements IHoverWidget { if (options.appearance?.compact) { this._hover.containerDomNode.classList.add('workbench-hover', 'compact'); } + if (this._hoverPointer) { + this._hover.containerDomNode.classList.add('with-pointer'); + } if (options.additionalClasses) { this._hover.containerDomNode.classList.add(...options.additionalClasses); } From 1a13b9ca22c82b81b9da95a120a33b7abeab17cc Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 13 Feb 2026 15:22:53 +0100 Subject: [PATCH 003/448] Add additional markdown extensions This marks the `.litcoffee` file extension as markdown. This is a markdown format, where indented code blocks can be interpreted as CoffeeScript. This also adds the `.mkdn` and `.ron` extensions, which are taken from https://github.com/sindresorhus/markdown-extensions/blob/main/index.js. --- extensions/markdown-basics/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index c77aad6a301..b24c8594cc2 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -18,14 +18,17 @@ "markdown" ], "extensions": [ + ".litcoffee", ".md", ".mkd", + ".mkdn", ".mdwn", ".mdown", ".markdown", ".markdn", ".mdtxt", ".mdtext", + ".ron", ".workbook" ], "filenamePatterns": [ From bb8e88443f794099d1bb8162a514f01f5bd695eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:29:55 +0000 Subject: [PATCH 004/448] Initial plan From b506711b0d016cbfa3013018c1977420ec0879f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:41:14 +0000 Subject: [PATCH 005/448] Remove gray background container for single editor buttons Co-authored-by: jo-oikawa <14115185+jo-oikawa@users.noreply.github.com> --- .../floatingMenu/browser/floatingMenu.css | 7 +++++++ .../contrib/floatingMenu/browser/floatingMenu.ts | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 422e073e5e7..3ced927df03 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -17,6 +17,13 @@ box-shadow: 0 2px 8px var(--vscode-widget-shadow); overflow: hidden; + &.single-button { + background-color: transparent; + border-color: transparent; + box-shadow: none; + padding: 0; + } + .actions-container { gap: 4px; } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 1a530186e66..170d42c8fea 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -9,6 +9,7 @@ import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platfo import { autorun, constObservable, derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { Separator } from '../../../../base/common/actions.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -75,18 +76,27 @@ export class FloatingEditorToolbarWidget extends Disposable { const menu = this._register(menuService.createMenu(_menuId, _scopedContextKeyService)); const menuGroupsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); - const menuPrimaryActionIdObs = derived(reader => { + const menuPrimaryActionsObs = derived(reader => { const menuGroups = menuGroupsObs.read(reader); - const { primary } = getActionBarActions(menuGroups, () => true); + return primary.filter(a => a.id !== Separator.ID); + }); + + const menuPrimaryActionIdObs = derived(reader => { + const primary = menuPrimaryActionsObs.read(reader); return primary.length > 0 ? primary[0].id : undefined; }); - this.hasActions = derived(reader => menuGroupsObs.read(reader).length > 0); + this.hasActions = derived(reader => menuPrimaryActionsObs.read(reader).length > 0); this.element = h('div.floating-menu-overlay-widget').root; this._register(toDisposable(() => this.element.remove())); + this._register(autorun(reader => { + const actionsCount = menuPrimaryActionsObs.read(reader).length; + this.element.classList.toggle('single-button', actionsCount === 1); + })); + // Set height explicitly to ensure that the floating menu element // is rendered in the lower right corner at the correct position. this.element.style.height = '26px'; From 8130d4e63692097a2917f426a22644130a784a80 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 24 Feb 2026 11:41:28 +0000 Subject: [PATCH 006/448] Refactor floating menu styles for single button and adjust height dynamically --- .../floatingMenu/browser/floatingMenu.css | 29 ++++++++++++++++++- .../floatingMenu/browser/floatingMenu.ts | 19 +++++------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 3ced927df03..08fecdaf30f 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -20,8 +20,23 @@ &.single-button { background-color: transparent; border-color: transparent; - box-shadow: none; padding: 0; + + .action-item > .action-label { + height: 28px; + line-height: 28px; + padding: 0 8px; + border-radius: 6px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + } + + .action-item > .action-label.codicon:not(.separator) { + height: 28px; + width: 28px; + line-height: 28px; + border-radius: 6px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + } } .actions-container { @@ -57,3 +72,15 @@ background-color: var(--vscode-button-hoverBackground) !important; } } + +.hc-black .floating-menu-overlay-widget.single-button, +.hc-light .floating-menu-overlay-widget.single-button { + border-color: var(--vscode-contrastBorder); + background-color: var(--vscode-editorWidget-background); + padding: 0; + .action-item > .action-label, + .action-item > .action-label.codicon:not(.separator) { + box-shadow: none; + border-radius: 0; + } +} diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 170d42c8fea..331874c099c 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -82,28 +82,23 @@ export class FloatingEditorToolbarWidget extends Disposable { return primary.filter(a => a.id !== Separator.ID); }); - const menuPrimaryActionIdObs = derived(reader => { - const primary = menuPrimaryActionsObs.read(reader); - return primary.length > 0 ? primary[0].id : undefined; - }); - this.hasActions = derived(reader => menuPrimaryActionsObs.read(reader).length > 0); this.element = h('div.floating-menu-overlay-widget').root; this._register(toDisposable(() => this.element.remove())); - this._register(autorun(reader => { - const actionsCount = menuPrimaryActionsObs.read(reader).length; - this.element.classList.toggle('single-button', actionsCount === 1); - })); - // Set height explicitly to ensure that the floating menu element // is rendered in the lower right corner at the correct position. this.element.style.height = '26px'; this._register(autorun(reader => { - const hasActions = this.hasActions.read(reader); - const menuPrimaryActionId = menuPrimaryActionIdObs.read(reader); + const primaryActions = menuPrimaryActionsObs.read(reader); + const hasActions = primaryActions.length > 0; + const menuPrimaryActionId = hasActions ? primaryActions[0].id : undefined; + + const isSingleButton = primaryActions.length === 1; + this.element.classList.toggle('single-button', isSingleButton); + this.element.style.height = isSingleButton ? '28px' : '26px'; if (!hasActions) { return; From 6ff0262d29d93c57a71f56aee5bb27935805155d Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 24 Feb 2026 11:53:05 +0000 Subject: [PATCH 007/448] Refactor floating menu styles for single button and adjust height dynamically Co-authored-by: Copilot --- .../floatingMenu/browser/floatingMenu.css | 24 ++++++++++--------- .../floatingMenu/browser/floatingMenu.ts | 10 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 08fecdaf30f..fa5d0f8bac0 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -7,7 +7,7 @@ padding: 2px 4px; color: var(--vscode-foreground); background-color: var(--vscode-editorWidget-background); - border-radius: 6px; + border-radius: var(--vscode-cornerRadius-medium); border: 1px solid var(--vscode-contrastBorder); display: flex; align-items: center; @@ -19,23 +19,24 @@ &.single-button { background-color: transparent; - border-color: transparent; + border-width: 0; padding: 0; + overflow: visible; - .action-item > .action-label { + .action-item > .action-label, + .action-item > .action-label.codicon:not(.separator) { height: 28px; line-height: 28px; - padding: 0 8px; - border-radius: 6px; + border-radius: var(--vscode-cornerRadius-medium); box-shadow: 0 2px 8px var(--vscode-widget-shadow); } + .action-item > .action-label { + padding: 0 8px; + } + .action-item > .action-label.codicon:not(.separator) { - height: 28px; width: 28px; - line-height: 28px; - border-radius: 6px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); } } @@ -47,7 +48,7 @@ padding: 4px 6px; font-size: 11px; line-height: 14px; - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-small); } .action-item > .action-label.codicon:not(.separator) { @@ -75,12 +76,13 @@ .hc-black .floating-menu-overlay-widget.single-button, .hc-light .floating-menu-overlay-widget.single-button { + border-width: 1px; + border-style: solid; border-color: var(--vscode-contrastBorder); background-color: var(--vscode-editorWidget-background); padding: 0; .action-item > .action-label, .action-item > .action-label.codicon:not(.separator) { box-shadow: none; - border-radius: 0; } } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 331874c099c..5e7be22f374 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Separator } from '../../../../base/common/actions.js'; import { h } from '../../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { autorun, constObservable, derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; +import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { Separator } from '../../../../base/common/actions.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -87,10 +87,6 @@ export class FloatingEditorToolbarWidget extends Disposable { this.element = h('div.floating-menu-overlay-widget').root; this._register(toDisposable(() => this.element.remove())); - // Set height explicitly to ensure that the floating menu element - // is rendered in the lower right corner at the correct position. - this.element.style.height = '26px'; - this._register(autorun(reader => { const primaryActions = menuPrimaryActionsObs.read(reader); const hasActions = primaryActions.length > 0; @@ -98,6 +94,8 @@ export class FloatingEditorToolbarWidget extends Disposable { const isSingleButton = primaryActions.length === 1; this.element.classList.toggle('single-button', isSingleButton); + // Set height explicitly to ensure that the floating menu element + // is rendered in the lower right corner at the correct position. this.element.style.height = isSingleButton ? '28px' : '26px'; if (!hasActions) { From 8f8b99bdb6d7e9a3b3689aefeefc170df20bb285 Mon Sep 17 00:00:00 2001 From: Pierce Boggan Date: Tue, 24 Feb 2026 16:56:44 -0700 Subject: [PATCH 008/448] Add copy button to chat message toolbar The copy action was only available via right-click context menu (ChatContext). Add it to the ChatMessageTitle toolbar so it appears as a visible icon button on hover, using the copy codicon. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../chat/browser/actions/chatCopyActions.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index e0647b11c4a..03ae16210cc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -54,11 +55,20 @@ export function registerChatCopyActions() { title: localize2('interactive.copyItem.label', "Copy"), f1: false, category: CHAT_CATEGORY, - menu: { - id: MenuId.ChatContext, - when: ChatContextKeys.responseIsFiltered.negate(), - group: 'copy', - } + icon: Codicon.copy, + menu: [ + { + id: MenuId.ChatContext, + when: ChatContextKeys.responseIsFiltered.negate(), + group: 'copy', + }, + { + id: MenuId.ChatMessageTitle, + group: 'navigation', + order: 5, + when: ChatContextKeys.responseIsFiltered.negate(), + } + ] }); } From bf38bf2127c86dc054048fafccd9a4fb5b0beb33 Mon Sep 17 00:00:00 2001 From: Pierce Boggan Date: Tue, 24 Feb 2026 17:36:41 -0700 Subject: [PATCH 009/448] Add copy button to chat response footer toolbar Register CopyItemAction on MenuId.ChatMessageFooter so responses show a copy button alongside thumbs up/down and retry actions. --- .../contrib/chat/browser/actions/chatCopyActions.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 03ae16210cc..23b4712e2a5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -9,6 +9,7 @@ import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { katexContainerClassName, katexContainerLatexAttributeName } from '../../../markdown/common/markedKatexExtension.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatRequestViewModel, IChatResponseViewModel, isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; @@ -67,6 +68,12 @@ export function registerChatCopyActions() { group: 'navigation', order: 5, when: ChatContextKeys.responseIsFiltered.negate(), + }, + { + id: MenuId.ChatMessageFooter, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(ChatContextKeys.isResponse, ChatContextKeys.responseIsFiltered.negate()), } ] }); From 8bfcb33fd3c2ad5e0d49e9e3d0d7dd9e5d385a40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:21:16 +0000 Subject: [PATCH 010/448] Initial plan From dfe6844f744bde97c415563c205277323b6dc329 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:27:37 +0000 Subject: [PATCH 011/448] Fix markdown syntax showing in deprecated setting diagnostic messages Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- .../configuration/common/configurationRegistry.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index a212c47ef1b..14a24d763bb 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -742,8 +742,12 @@ class ConfigurationRegistry extends Disposable implements IConfigurationRegistry } this.configurationProperties[key] = properties[key]; if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) { - // If not set, default deprecationMessage to the markdown source - properties[key].deprecationMessage = properties[key].markdownDeprecationMessage; + // If not set, default deprecationMessage to the markdown source with markdown stripped + // since diagnostics don't support markdown rendering + properties[key].deprecationMessage = properties[key].markdownDeprecationMessage! + .replace(/`#([^#`]*)#`/g, '$1') // strip setting links: `#settingId#` -> settingId + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // strip links: [text](url) -> text + .replace(/`([^`]*)`/g, '$1'); // strip inline code: `code` -> code } } From d2345ae4806d679dd1f58e78adb6f105d3b74105 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:57:49 +0000 Subject: [PATCH 012/448] Use renderAsPlaintext and rewriteSettingLinks for deprecation message stripping Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- .../common/configurationRegistry.ts | 8 ++----- .../browser/configurationService.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 14a24d763bb..a212c47ef1b 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -742,12 +742,8 @@ class ConfigurationRegistry extends Disposable implements IConfigurationRegistry } this.configurationProperties[key] = properties[key]; if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) { - // If not set, default deprecationMessage to the markdown source with markdown stripped - // since diagnostics don't support markdown rendering - properties[key].deprecationMessage = properties[key].markdownDeprecationMessage! - .replace(/`#([^#`]*)#`/g, '$1') // strip setting links: `#settingId#` -> settingId - .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // strip links: [text](url) -> text - .replace(/`([^`]*)`/g, '$1'); // strip inline code: `code` -> code + // If not set, default deprecationMessage to the markdown source + properties[key].deprecationMessage = properties[key].markdownDeprecationMessage; } } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index b6b3e48dcd3..29ffc197e14 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -47,6 +47,7 @@ import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/e import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { runWhenWindowIdle } from '../../../../base/browser/dom.js'; +import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; function getLocalUserConfigurationScopes(userDataProfile: IUserDataProfile, hasRemote: boolean): ConfigurationScope[] | undefined { const isDefaultProfile = userDataProfile.isDefault || userDataProfile.useDefaultFlags?.settings; @@ -1151,6 +1152,17 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } +/** + * Rewrites VS Code's custom `#settingId#` syntax to standard markdown links + * so that {@link renderAsPlaintext} can properly extract the setting key as plain text. + */ +function rewriteSettingLinks(text: string): string { + return text.replace(/`#([^#\s`]+)#`|'#([^#\s']+)#'/g, (_, backtickGroup, quoteGroup) => { + const settingKey: string = backtickGroup ?? quoteGroup; + return `[${settingKey}](#${settingKey})`; + }); +} + class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution { constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -1172,6 +1184,15 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo } private registerConfigurationSchemas(): void { + // Ensure deprecationMessage is plain text for properties where it was derived from + // markdownDeprecationMessage, since the JSON editor diagnostics don't support markdown. + for (const key of Object.keys(allSettings.properties)) { + const prop = allSettings.properties[key]; + if (prop.markdownDeprecationMessage && prop.deprecationMessage === prop.markdownDeprecationMessage) { + prop.deprecationMessage = renderAsPlaintext({ value: rewriteSettingLinks(prop.markdownDeprecationMessage) }); + } + } + const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, From 8ea255612fc6b28c8e734d8cb1427f1c8e05506e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:15:26 +0000 Subject: [PATCH 013/448] Reuse fixSettingLinks from preferencesModels.ts instead of custom rewriteSettingLinks Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- .../configuration/browser/configurationService.ts | 14 ++------------ .../preferences/common/preferencesModels.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 29ffc197e14..654e9c2585c 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -48,6 +48,7 @@ import { workbenchConfigurationNodeBase } from '../../../common/configuration.js import { mainWindow } from '../../../../base/browser/window.js'; import { runWhenWindowIdle } from '../../../../base/browser/dom.js'; import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { fixSettingLinks } from '../../preferences/common/preferencesModels.js'; function getLocalUserConfigurationScopes(userDataProfile: IUserDataProfile, hasRemote: boolean): ConfigurationScope[] | undefined { const isDefaultProfile = userDataProfile.isDefault || userDataProfile.useDefaultFlags?.settings; @@ -1152,17 +1153,6 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } -/** - * Rewrites VS Code's custom `#settingId#` syntax to standard markdown links - * so that {@link renderAsPlaintext} can properly extract the setting key as plain text. - */ -function rewriteSettingLinks(text: string): string { - return text.replace(/`#([^#\s`]+)#`|'#([^#\s']+)#'/g, (_, backtickGroup, quoteGroup) => { - const settingKey: string = backtickGroup ?? quoteGroup; - return `[${settingKey}](#${settingKey})`; - }); -} - class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution { constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -1189,7 +1179,7 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo for (const key of Object.keys(allSettings.properties)) { const prop = allSettings.properties[key]; if (prop.markdownDeprecationMessage && prop.deprecationMessage === prop.markdownDeprecationMessage) { - prop.deprecationMessage = renderAsPlaintext({ value: rewriteSettingLinks(prop.markdownDeprecationMessage) }); + prop.deprecationMessage = renderAsPlaintext({ value: fixSettingLinks(prop.markdownDeprecationMessage) }); } } diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 84fa12bc44d..1d399cefc95 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -28,6 +28,14 @@ import { isString } from '../../../../base/common/types.js'; export const nullRange: IRange = { startLineNumber: -1, startColumn: -1, endLineNumber: -1, endColumn: -1 }; function isNullRange(range: IRange): boolean { return range.startLineNumber === -1 && range.startColumn === -1 && range.endLineNumber === -1 && range.endColumn === -1; } +/** + * Strips VS Code's custom `#settingId#` link syntax from a markdown string so the setting key + * remains as inline code (e.g. `` `settingId` ``). Useful for contexts that don't render markdown links. + */ +export function fixSettingLinks(text: string): string { + return text.replace(/`#([^#`]*)#`/g, (_, settingName) => `\`${settingName}\``); +} + abstract class AbstractSettingsModel extends EditorModel { protected _currentResultGroups = new Map(); @@ -1072,13 +1080,11 @@ class SettingsContentBuilder { } private pushSettingDescription(setting: ISetting, indent: string): void { - const fixSettingLink = (line: string) => line.replace(/`#(.*)#`/g, (match, settingName) => `\`${settingName}\``); - setting.descriptionRanges = []; const descriptionPreValue = indent + '// '; const deprecationMessageLines = setting.deprecationMessage?.split(/\n/g) ?? []; for (let line of [...deprecationMessageLines, ...setting.description]) { - line = fixSettingLink(line); + line = fixSettingLinks(line); this._contentByLines.push(descriptionPreValue + line); setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); @@ -1088,7 +1094,7 @@ class SettingsContentBuilder { setting.enumDescriptions.forEach((desc, i) => { const displayEnum = escapeInvisibleChars(String(setting.enum![i])); const line = desc ? - `${displayEnum}: ${fixSettingLink(desc)}` : + `${displayEnum}: ${fixSettingLinks(desc)}` : displayEnum; const lines = line.split(/\n/g); From 6e417b3ff7e98ca2ac9773709bef3dd4cc43816f Mon Sep 17 00:00:00 2001 From: RajeshKumar11 Date: Thu, 26 Feb 2026 22:25:56 +0530 Subject: [PATCH 014/448] MCP Gateway: avoid blocking list calls on startup --- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 88a1da8b4b6..4d039b3afe3 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -124,10 +124,13 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private async _listTools(): Promise { const mcpTools: MCP.Tool[] = []; const servers = this._mcpService.servers.get(); - await Promise.all(servers.map(server => this._ensureServerReady(server))); for (const server of servers) { const cacheState = server.cacheState.get(); + if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { + // Avoid blocking the tool list on slow server startup; refresh in the background. + void this._ensureServerReady(server); + } if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { continue; } @@ -163,7 +166,14 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh const results: IGatewayServerResources[] = []; const servers = this._mcpService.servers.get(); await Promise.all(servers.map(async server => { - await this._ensureServerReady(server); + const cacheState = server.cacheState.get(); + if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { + // Avoid blocking on slow server startup; refresh in the background. + void this._ensureServerReady(server); + } + if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { + return; + } const capabilities = server.capabilities.get(); if (!capabilities || !(capabilities & McpCapability.Resources)) { @@ -195,7 +205,14 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh const servers = this._mcpService.servers.get(); await Promise.all(servers.map(async server => { - await this._ensureServerReady(server); + const cacheState = server.cacheState.get(); + if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { + // Avoid blocking on slow server startup; refresh in the background. + void this._ensureServerReady(server); + } + if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { + return; + } const capabilities = server.capabilities.get(); if (!capabilities || !(capabilities & McpCapability.Resources)) { From 820760f09815dd0957d250af1c2c47547a170254 Mon Sep 17 00:00:00 2001 From: RajeshKumar11 Date: Fri, 27 Feb 2026 14:50:19 +0530 Subject: [PATCH 015/448] MCP Gateway: add 5 s per-server startup grace period Instead of immediately returning empty results for Unknown-state servers and refreshing in the background, wait up to 5 seconds for the server to become live on the first list call. Subsequent calls find the already- resolved promise in _startupGrace and return immediately, so only the first message pays the startup cost. - _waitForStartup: races _ensureServerReady against the configurable grace period (default 5 000 ms); result is cached per server - _shouldUseCachedData: awaits the grace period for Unknown servers, falls back to background-refresh for Outdated servers - _listTools: refactored to Promise.all to parallelise per-server waits - Tests: updated 'starts server when cache state is unknown' to assert tools are returned after the grace period; added new test for the timeout path using createNeverStartingServer --- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 82 +++++++++++-------- .../mcpGatewayToolBrokerChannel.test.ts | 64 ++++++++++++++- 2 files changed, 112 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 4d039b3afe3..58bc42e61ba 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -30,8 +30,16 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private readonly _serverIdMap = new Map(); private _nextServerIndex = 0; + /** + * Per-server promise that races server startup against the grace period timeout. + * Once set for a server, subsequent list calls await the already-resolved promise + * and return immediately instead of waiting again. + */ + private readonly _startupGrace = new Map>(); + constructor( private readonly _mcpService: IMcpService, + private readonly _startupGracePeriodMs = 5000, ) { super(); @@ -81,6 +89,37 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh return undefined; } + private _waitForStartup(server: IMcpServer): Promise { + const id = server.definition.id; + if (!this._startupGrace.has(id)) { + this._startupGrace.set(id, Promise.race([ + this._ensureServerReady(server), + new Promise(resolve => setTimeout(() => resolve(false), this._startupGracePeriodMs)), + ])); + } + return this._startupGrace.get(id)!; + } + + private async _shouldUseCachedData(server: IMcpServer): Promise { + const cacheState = server.cacheState.get(); + if (cacheState === McpServerCacheState.Unknown) { + // On first list call: wait up to the grace period for the server to start. + // On subsequent calls: the stored promise is already resolved, returns immediately. + await this._waitForStartup(server); + const newState = server.cacheState.get(); + return newState === McpServerCacheState.Live + || newState === McpServerCacheState.Cached + || newState === McpServerCacheState.RefreshingFromCached; + } + if (cacheState === McpServerCacheState.Outdated) { + // Has cached data — refresh in the background without blocking. + void this._ensureServerReady(server); + } + return cacheState === McpServerCacheState.Live + || cacheState === McpServerCacheState.Cached + || cacheState === McpServerCacheState.RefreshingFromCached; + } + listen(_ctx: unknown, event: string): Event { switch (event) { case 'onDidChangeTools': @@ -122,29 +161,16 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } private async _listTools(): Promise { - const mcpTools: MCP.Tool[] = []; const servers = this._mcpService.servers.get(); - - for (const server of servers) { - const cacheState = server.cacheState.get(); - if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { - // Avoid blocking the tool list on slow server startup; refresh in the background. - void this._ensureServerReady(server); + const perServer = await Promise.all(servers.map(async server => { + if (!await this._shouldUseCachedData(server)) { + return [] as MCP.Tool[]; } - if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { - continue; - } - - for (const tool of server.tools.get()) { - if (!(tool.visibility & McpToolVisibility.Model)) { - continue; - } - - mcpTools.push(tool.definition); - } - } - - return mcpTools; + return server.tools.get() + .filter(t => t.visibility & McpToolVisibility.Model) + .map(t => t.definition); + })); + return perServer.flat(); } private async _callTool(name: string, args: Record, token: CancellationToken = CancellationToken.None): Promise { @@ -166,12 +192,7 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh const results: IGatewayServerResources[] = []; const servers = this._mcpService.servers.get(); await Promise.all(servers.map(async server => { - const cacheState = server.cacheState.get(); - if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { - // Avoid blocking on slow server startup; refresh in the background. - void this._ensureServerReady(server); - } - if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { + if (!await this._shouldUseCachedData(server)) { return; } @@ -205,12 +226,7 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh const servers = this._mcpService.servers.get(); await Promise.all(servers.map(async server => { - const cacheState = server.cacheState.get(); - if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { - // Avoid blocking on slow server startup; refresh in the background. - void this._ensureServerReady(server); - } - if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { + if (!await this._shouldUseCachedData(server)) { return; } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index 53124c8acc3..55d2cbb2c0a 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -132,9 +132,35 @@ suite('McpGatewayToolBrokerChannel', () => { ); mcpService.servers.set([server], undefined); - await channel.call(undefined, 'listTools'); + const tools = await channel.call(undefined, 'listTools'); + // Server started during the grace period; tools are now available. assert.strictEqual(server.startCalls, 1); + assert.deepStrictEqual(tools.map(t => t.name), ['echo']); + channel.dispose(); + }); + + test('returns empty tools and does not re-wait if server does not start within grace period', async () => { + const mcpService = new TestMcpService(); + // Use a very short grace period so the test does not take 5 s. + const channel = new McpGatewayToolBrokerChannel(mcpService, 10); + + const server = createNeverStartingServer( + 'collectionA', + 'serverA', + [createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] }))], + ); + + mcpService.servers.set([server], undefined); + + // First call: waits up to 10 ms, server never starts → empty result. + const tools = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools, []); + + // Second call: grace-period promise already resolved; returns immediately without re-waiting. + const tools2 = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools2, []); + channel.dispose(); }); }); @@ -177,6 +203,42 @@ function createServer( }; } +function createNeverStartingServer( + collectionId: string, + definitionId: string, + initialTools: readonly IMcpTool[], +): IMcpServer & { startCalls: number } { + const owner = {}; + const tools = observableValue(owner, initialTools); + const connectionState = observableValue(owner, { state: McpConnectionState.Kind.Running }); + const cacheState = observableValue(owner, McpServerCacheState.Unknown); + let startCalls = 0; + + return { + collection: { id: collectionId, label: collectionId }, + definition: { id: definitionId, label: definitionId }, + connection: observableValue(owner, undefined), + connectionState, + serverMetadata: observableValue(owner, undefined), + readDefinitions: () => observableValue(owner, { server: undefined, collection: undefined }), + showOutput: async () => { }, + start: async () => { + startCalls++; + // Never resolves — simulates a server that hangs on startup. + return new Promise<{ state: McpConnectionState.Kind }>(() => { }); + }, + stop: async () => { }, + cacheState, + tools, + prompts: observableValue(owner, []), + capabilities: observableValue(owner, undefined), + resources: () => (async function* () { })(), + resourceTemplates: async () => [], + dispose: () => { }, + get startCalls() { return startCalls; }, + }; +} + function createTool(name: string, call: (params: Record) => Promise, visibility: McpToolVisibility = McpToolVisibility.Model): IMcpTool { const definition: MCP.Tool = { name, From 7325d2b95aff00ec28479d6ac089c7e2b2118494 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 27 Feb 2026 12:02:02 +0000 Subject: [PATCH 016/448] update syntax colors for 2026 theme in dark and light modes Co-authored-by: Copilot --- extensions/theme-2026/themes/2026-dark.json | 538 ++++++++++--------- extensions/theme-2026/themes/2026-light.json | 538 ++++++++++--------- 2 files changed, 564 insertions(+), 512 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8d3f082600d..d953a731a53 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -268,369 +268,395 @@ "tokenColors": [ { "scope": [ - "comment" + "comment", + "punctuation.definition.comment", + "string.comment" ], "settings": { - "foreground": "#6F9B60" + "foreground": "#8b949e" } }, { "scope": [ - "keyword", - "storage.modifier", - "storage.type", - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.cast", - "keyword.operator.sizeof", - "keyword.operator.instanceof" + "constant.other.placeholder", + "constant.character" ], "settings": { - "foreground": "#4F8FDD" + "foreground": "#ff7b72" } }, { "scope": [ - "string" + "constant", + "entity.name.constant", + "variable.other.constant", + "variable.other.enummember", + "variable.language", + "entity" ], "settings": { - "foreground": "#C48081" + "foreground": "#79c0ff" } }, { - "name": "Language constants", "scope": [ - "constant.language" + "entity.name", + "meta.export.default", + "meta.definition.variable" ], "settings": { - "foreground": "#4F8FDD" + "foreground": "#ffa657" + } + }, + { + "scope": [ + "variable.parameter.function", + "meta.jsx.children", + "meta.block", + "meta.tag.attributes", + "entity.name.constant", + "meta.object.member", + "meta.embedded.expression" + ], + "settings": { + "foreground": "#e6edf3" + } + }, + { + "scope": "entity.name.function", + "settings": { + "foreground": "#d2a8ff" } }, { - "name": "HTML/XML tags", "scope": [ "entity.name.tag", - "meta.tag.sgml", - "markup.deleted.git_gutter" + "support.class.component" ], "settings": { - "foreground": "#4F9BDD" + "foreground": "#7ee787" } }, { - "name": "HTML/XML tag punctuation", - "scope": [ - "punctuation.definition.tag.html", - "punctuation.definition.tag.begin.html", - "punctuation.definition.tag.end.html" - ], + "scope": "keyword", "settings": { - "foreground": "#7A828B" - } - }, - { - "name": "HTML/XML attribute names", - "scope": [ - "entity.other.attribute-name" - ], - "settings": { - "foreground": "#90D5FF" - } - }, - { - "name": "Operators", - "scope": [ - "keyword.operator" - ], - "settings": { - "foreground": "#C5CCD6" - } - }, - { - "name": "Function declarations", - "scope": [ - "entity.name.function", - "support.function", - "support.constant.handlebars", - "source.powershell variable.other.member", - "entity.name.operator.custom-literal" - ], - "settings": { - "foreground": "#D1D6AE" - } - }, - { - "name": "Types declaration and references", - "scope": [ - "support.class", - "support.type", - "entity.name.type", - "entity.name.namespace", - "entity.other.attribute", - "entity.name.scope-resolution", - "entity.name.class", - "storage.type.numeric.go", - "storage.type.byte.go", - "storage.type.boolean.go", - "storage.type.string.go", - "storage.type.uintptr.go", - "storage.type.error.go", - "storage.type.rune.go", - "storage.type.cs", - "storage.type.generic.cs", - "storage.type.modifier.cs", - "storage.type.variable.cs", - "storage.type.annotation.java", - "storage.type.generic.java", - "storage.type.java", - "storage.type.object.array.java", - "storage.type.primitive.array.java", - "storage.type.primitive.java", - "storage.type.token.java", - "storage.type.groovy", - "storage.type.annotation.groovy", - "storage.type.parameters.groovy", - "storage.type.generic.groovy", - "storage.type.object.array.groovy", - "storage.type.primitive.array.groovy", - "storage.type.primitive.groovy" - ], - "settings": { - "foreground": "#48C9C4" - } - }, - { - "name": "Types declaration and references, TS grammar specific", - "scope": [ - "meta.type.cast.expr", - "meta.type.new.expr", - "support.constant.math", - "support.constant.dom", - "support.constant.json", - "entity.other.inherited-class", - "punctuation.separator.namespace.ruby" - ], - "settings": { - "foreground": "#48C9B9" - } - }, - { - "name": "Control flow / Special keywords", - "scope": [ - "keyword.control", - "source.cpp keyword.operator.new", - "keyword.operator.delete", - "keyword.other.using", - "keyword.other.directive.using", - "keyword.other.operator", - "entity.name.operator" - ], - "settings": { - "foreground": "#C184C6" - } - }, - { - "name": "Variable and parameter name", - "scope": [ - "variable", - "meta.definition.variable.name", - "support.variable", - "entity.name.variable", - "constant.other.placeholder" - ], - "settings": { - "foreground": "#90D5FF" - } - }, - { - "name": "Constants and enums", - "scope": [ - "variable.other.constant", - "variable.other.enummember" - ], - "settings": { - "foreground": "#4CBDFF" - } - }, - { - "name": "Object keys, TS grammar specific", - "scope": [ - "meta.object-literal.key" - ], - "settings": { - "foreground": "#90D5FF" - } - }, - { - "name": "CSS property value", - "scope": [ - "support.constant.property-value", - "support.constant.font-name", - "support.constant.media-type", - "support.constant.media", - "constant.other.color.rgb-value", - "constant.other.rgb-value", - "support.constant.color" - ], - "settings": { - "foreground": "#C48F80" - } - }, - { - "name": "Regular expression groups", - "scope": [ - "punctuation.definition.group.regexp", - "punctuation.definition.group.assertion.regexp", - "punctuation.definition.character-class.regexp", - "punctuation.character.set.begin.regexp", - "punctuation.character.set.end.regexp", - "keyword.operator.negation.regexp", - "support.other.parenthesis.regexp" - ], - "settings": { - "foreground": "#C49580" + "foreground": "#ff7b72" } }, { "scope": [ - "constant.character.character-class.regexp", - "constant.other.character-class.set.regexp", - "constant.other.character-class.regexp", - "constant.character.set.regexp" + "storage", + "storage.type" ], "settings": { - "foreground": "#C86971" + "foreground": "#ff7b72" } }, { "scope": [ - "keyword.operator.or.regexp", - "keyword.control.anchor.regexp" + "storage.modifier.package", + "storage.modifier.import", + "storage.type.java" ], "settings": { - "foreground": "#CBD6AE" - } - }, - { - "scope": "keyword.operator.quantifier.regexp", - "settings": { - "foreground": "#CCBD84" + "foreground": "#e6edf3" } }, { "scope": [ - "constant.character", - "constant.other.option" + "string", + "string punctuation.section.embedded source" ], "settings": { - "foreground": "#4F9BDD" + "foreground": "#a5d6ff" } }, { - "scope": "constant.character.escape", + "scope": "support", "settings": { - "foreground": "#CCB784" + "foreground": "#79c0ff" } }, { - "scope": "entity.name.label", + "scope": "meta.property-name", "settings": { - "foreground": "#BAC2CC" + "foreground": "#79c0ff" + } + }, + { + "scope": "variable", + "settings": { + "foreground": "#ffa657" + } + }, + { + "scope": "variable.other", + "settings": { + "foreground": "#e6edf3" + } + }, + { + "scope": "invalid.broken", + "settings": { + "fontStyle": "italic", + "foreground": "#ffa198" + } + }, + { + "scope": "invalid.deprecated", + "settings": { + "fontStyle": "italic", + "foreground": "#ffa198" + } + }, + { + "scope": "invalid.illegal", + "settings": { + "fontStyle": "italic", + "foreground": "#ffa198" + } + }, + { + "scope": "invalid.unimplemented", + "settings": { + "fontStyle": "italic", + "foreground": "#ffa198" + } + }, + { + "scope": "carriage-return", + "settings": { + "fontStyle": "italic underline", + "background": "#ff7b72", + "foreground": "#f0f6fc", + "content": "^M" + } + }, + { + "scope": "message.error", + "settings": { + "foreground": "#ffa198" + } + }, + { + "scope": "string variable", + "settings": { + "foreground": "#79c0ff" } }, { - "name": "Numbers", "scope": [ - "constant.numeric" + "source.regexp", + "string.regexp" ], "settings": { - "foreground": "#A8CAAD" + "foreground": "#a5d6ff" } }, { - "name": "Markup Heading", - "scope": "markup.heading", + "scope": [ + "string.regexp.character-class", + "string.regexp constant.character.escape", + "string.regexp source.ruby.embedded", + "string.regexp string.regexp.arbitrary-repitition" + ], "settings": { - "foreground": "#64b0df", - "fontStyle": "bold" + "foreground": "#a5d6ff" } }, { - "name": "Markup Bold", - "scope": "markup.bold", + "scope": "string.regexp constant.character.escape", "settings": { - "foreground": "#C48081", - "fontStyle": "bold" + "fontStyle": "bold", + "foreground": "#7ee787" + } + }, + { + "scope": "support.constant", + "settings": { + "foreground": "#79c0ff" + } + }, + { + "scope": "support.variable", + "settings": { + "foreground": "#79c0ff" + } + }, + { + "scope": "support.type.property-name.json", + "settings": { + "foreground": "#7ee787" + } + }, + { + "scope": "meta.module-reference", + "settings": { + "foreground": "#79c0ff" + } + }, + { + "scope": "punctuation.definition.list.begin.markdown", + "settings": { + "foreground": "#ffa657" + } + }, + { + "scope": [ + "markup.heading", + "markup.heading entity.name" + ], + "settings": { + "fontStyle": "bold", + "foreground": "#79c0ff" + } + }, + { + "scope": "markup.quote", + "settings": { + "foreground": "#7ee787" } }, { - "name": "Markup Italic", "scope": "markup.italic", "settings": { - "fontStyle": "italic" + "fontStyle": "italic", + "foreground": "#e6edf3" } }, { - "name": "Markup Strikethrough", - "scope": "markup.strikethrough", + "scope": "markup.bold", "settings": { - "fontStyle": "strikethrough" + "fontStyle": "bold", + "foreground": "#e6edf3" } }, { - "name": "Markup Underline", - "scope": "markup.underline", + "scope": [ + "markup.underline" + ], "settings": { "fontStyle": "underline" } }, { - "name": "Markup Quote", - "scope": "markup.quote", + "scope": [ + "markup.strikethrough" + ], "settings": { - "foreground": "#C184C6" + "fontStyle": "strikethrough" } }, { - "name": "Markup List", - "scope": "markup.list", - "settings": { - "foreground": "#48C9C4" - } - }, - { - "name": "Markup Inline Raw", "scope": "markup.inline.raw", "settings": { - "foreground": "#D1D6AE" + "foreground": "#79c0ff" } }, { - "name": "Markup Raw/Fenced Code Block", "scope": [ - "markup.raw", - "markup.fenced_code" + "markup.deleted", + "meta.diff.header.from-file", + "punctuation.definition.deleted" ], "settings": { - "foreground": "#8C8C8C" + "background": "#490202", + "foreground": "#ffa198" } }, { - "name": "Markup Link", "scope": [ - "meta.link", - "markup.underline.link" + "punctuation.section.embedded" ], "settings": { - "foreground": "#48A0C7" + "foreground": "#ff7b72" + } + }, + { + "scope": [ + "markup.inserted", + "meta.diff.header.to-file", + "punctuation.definition.inserted" + ], + "settings": { + "background": "#04260f", + "foreground": "#7ee787" + } + }, + { + "scope": [ + "markup.changed", + "punctuation.definition.changed" + ], + "settings": { + "background": "#5a1e02", + "foreground": "#ffa657" + } + }, + { + "scope": [ + "markup.ignored", + "markup.untracked" + ], + "settings": { + "foreground": "#161b22", + "background": "#79c0ff" + } + }, + { + "scope": "meta.diff.range", + "settings": { + "foreground": "#d2a8ff", + "fontStyle": "bold" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#79c0ff" + } + }, + { + "scope": "meta.separator", + "settings": { + "fontStyle": "bold", + "foreground": "#79c0ff" + } + }, + { + "scope": "meta.output", + "settings": { + "foreground": "#79c0ff" + } + }, + { + "scope": [ + "brackethighlighter.tag", + "brackethighlighter.curly", + "brackethighlighter.round", + "brackethighlighter.square", + "brackethighlighter.angle", + "brackethighlighter.quote" + ], + "settings": { + "foreground": "#8b949e" + } + }, + { + "scope": "brackethighlighter.unmatched", + "settings": { + "foreground": "#ffa198" + } + }, + { + "scope": [ + "constant.other.reference.link", + "string.other.link" + ], + "settings": { + "foreground": "#a5d6ff" } } ], - "semanticHighlighting": true, - "semanticTokenColors": { - "newOperator": "#C586C0", - "stringLiteral": "#ce9178", - "customLiteral": "#DCDCAA", - "numberLiteral": "#b5cea8" - } + "semanticHighlighting": true } diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f11ffef2905..6c65f1b9454 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -275,369 +275,395 @@ "tokenColors": [ { "scope": [ - "comment" + "comment", + "punctuation.definition.comment", + "string.comment" ], "settings": { - "foreground": "#60984D" + "foreground": "#6e7781" } }, { "scope": [ - "keyword", - "storage.modifier", - "storage.type", - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.cast", - "keyword.operator.sizeof", - "keyword.operator.instanceof" + "constant.other.placeholder", + "constant.character" ], "settings": { - "foreground": "#5460C1" + "foreground": "#cf222e" } }, { "scope": [ - "string" + "constant", + "entity.name.constant", + "variable.other.constant", + "variable.other.enummember", + "variable.language", + "entity" ], "settings": { - "foreground": "#B86855" + "foreground": "#0550ae" } }, { - "name": "Language constants", "scope": [ - "constant.language" + "entity.name", + "meta.export.default", + "meta.definition.variable" ], "settings": { - "foreground": "#5460C1" + "foreground": "#953800" + } + }, + { + "scope": [ + "variable.parameter.function", + "meta.jsx.children", + "meta.block", + "meta.tag.attributes", + "entity.name.constant", + "meta.object.member", + "meta.embedded.expression" + ], + "settings": { + "foreground": "#1f2328" + } + }, + { + "scope": "entity.name.function", + "settings": { + "foreground": "#8250df" } }, { - "name": "HTML/XML tags", "scope": [ "entity.name.tag", - "meta.tag.sgml", - "markup.deleted.git_gutter" + "support.class.component" ], "settings": { - "foreground": "#5751DE" + "foreground": "#116329" } }, { - "name": "HTML/XML tag punctuation", - "scope": [ - "punctuation.definition.tag.html", - "punctuation.definition.tag.begin.html", - "punctuation.definition.tag.end.html" - ], + "scope": "keyword", "settings": { - "foreground": "#93201A" - } - }, - { - "name": "HTML/XML attribute names", - "scope": [ - "entity.other.attribute-name" - ], - "settings": { - "foreground": "#E75854" - } - }, - { - "name": "Operators", - "scope": [ - "keyword.operator" - ], - "settings": { - "foreground": "#573F35" - } - }, - { - "name": "Function declarations", - "scope": [ - "entity.name.function", - "support.function", - "support.constant.handlebars", - "source.powershell variable.other.member", - "entity.name.operator.custom-literal" - ], - "settings": { - "foreground": "#98863B" - } - }, - { - "name": "Types declaration and references", - "scope": [ - "support.class", - "support.type", - "entity.name.type", - "entity.name.namespace", - "entity.other.attribute", - "entity.name.scope-resolution", - "entity.name.class", - "storage.type.numeric.go", - "storage.type.byte.go", - "storage.type.boolean.go", - "storage.type.string.go", - "storage.type.uintptr.go", - "storage.type.error.go", - "storage.type.rune.go", - "storage.type.cs", - "storage.type.generic.cs", - "storage.type.modifier.cs", - "storage.type.variable.cs", - "storage.type.annotation.java", - "storage.type.generic.java", - "storage.type.java", - "storage.type.object.array.java", - "storage.type.primitive.array.java", - "storage.type.primitive.java", - "storage.type.token.java", - "storage.type.groovy", - "storage.type.annotation.groovy", - "storage.type.parameters.groovy", - "storage.type.generic.groovy", - "storage.type.object.array.groovy", - "storage.type.primitive.array.groovy", - "storage.type.primitive.groovy" - ], - "settings": { - "foreground": "#46969A" - } - }, - { - "name": "Types declaration and references, TS grammar specific", - "scope": [ - "meta.type.cast.expr", - "meta.type.new.expr", - "support.constant.math", - "support.constant.dom", - "support.constant.json", - "entity.other.inherited-class", - "punctuation.separator.namespace.ruby" - ], - "settings": { - "foreground": "#419BB3" - } - }, - { - "name": "Control flow / Special keywords", - "scope": [ - "keyword.control", - "source.cpp keyword.operator.new", - "source.cpp keyword.operator.delete", - "keyword.other.using", - "keyword.other.directive.using", - "keyword.other.operator", - "entity.name.operator" - ], - "settings": { - "foreground": "#8F41AD" - } - }, - { - "name": "Variable and parameter name", - "scope": [ - "variable", - "meta.definition.variable.name", - "support.variable", - "entity.name.variable", - "constant.other.placeholder" - ], - "settings": { - "foreground": "#282D85" - } - }, - { - "name": "Constants and enums", - "scope": [ - "variable.other.constant", - "variable.other.enummember" - ], - "settings": { - "foreground": "#3086C5" - } - }, - { - "name": "Object keys, TS grammar specific", - "scope": [ - "meta.object-literal.key" - ], - "settings": { - "foreground": "#282D85" - } - }, - { - "name": "CSS property value", - "scope": [ - "support.constant.property-value", - "support.constant.font-name", - "support.constant.media-type", - "support.constant.media", - "constant.other.color.rgb-value", - "constant.other.rgb-value", - "support.constant.color" - ], - "settings": { - "foreground": "#2D6AAE" - } - }, - { - "name": "Regular expression groups", - "scope": [ - "punctuation.definition.group.regexp", - "punctuation.definition.group.assertion.regexp", - "punctuation.definition.character-class.regexp", - "punctuation.character.set.begin.regexp", - "punctuation.character.set.end.regexp", - "keyword.operator.negation.regexp", - "support.other.parenthesis.regexp" - ], - "settings": { - "foreground": "#D68490" + "foreground": "#cf222e" } }, { "scope": [ - "constant.character.character-class.regexp", - "constant.other.character-class.set.regexp", - "constant.other.character-class.regexp", - "constant.character.set.regexp" + "storage", + "storage.type" ], "settings": { - "foreground": "#A63350" - } - }, - { - "scope": "keyword.operator.quantifier.regexp", - "settings": { - "foreground": "#573F35" + "foreground": "#cf222e" } }, { "scope": [ - "keyword.operator.or.regexp", - "keyword.control.anchor.regexp" + "storage.modifier.package", + "storage.modifier.import", + "storage.type.java" ], "settings": { - "foreground": "#C54C5B" + "foreground": "#1f2328" } }, { "scope": [ - "constant.character", - "constant.other.option" + "string", + "string punctuation.section.embedded source" ], "settings": { - "foreground": "#5751DE" + "foreground": "#0a3069" } }, { - "scope": "constant.character.escape", + "scope": "support", "settings": { - "foreground": "#E14A46" + "foreground": "#0550ae" } }, { - "scope": "entity.name.label", + "scope": "meta.property-name", "settings": { - "foreground": "#5C3923" + "foreground": "#0550ae" + } + }, + { + "scope": "variable", + "settings": { + "foreground": "#953800" + } + }, + { + "scope": "variable.other", + "settings": { + "foreground": "#1f2328" + } + }, + { + "scope": "invalid.broken", + "settings": { + "fontStyle": "italic", + "foreground": "#82071e" + } + }, + { + "scope": "invalid.deprecated", + "settings": { + "fontStyle": "italic", + "foreground": "#82071e" + } + }, + { + "scope": "invalid.illegal", + "settings": { + "fontStyle": "italic", + "foreground": "#82071e" + } + }, + { + "scope": "invalid.unimplemented", + "settings": { + "fontStyle": "italic", + "foreground": "#82071e" + } + }, + { + "scope": "carriage-return", + "settings": { + "fontStyle": "italic underline", + "background": "#cf222e", + "foreground": "#f6f8fa", + "content": "^M" + } + }, + { + "scope": "message.error", + "settings": { + "foreground": "#82071e" + } + }, + { + "scope": "string variable", + "settings": { + "foreground": "#0550ae" } }, { - "name": "Numbers", "scope": [ - "constant.numeric" + "source.regexp", + "string.regexp" ], "settings": { - "foreground": "#2B9A69" + "foreground": "#0a3069" } }, { - "name": "Markup Heading", - "scope": "markup.heading", + "scope": [ + "string.regexp.character-class", + "string.regexp constant.character.escape", + "string.regexp source.ruby.embedded", + "string.regexp string.regexp.arbitrary-repitition" + ], "settings": { - "foreground": "#5460C1", - "fontStyle": "bold" + "foreground": "#0a3069" } }, { - "name": "Markup Bold", - "scope": "markup.bold", + "scope": "string.regexp constant.character.escape", "settings": { - "foreground": "#B86855", - "fontStyle": "bold" + "fontStyle": "bold", + "foreground": "#116329" + } + }, + { + "scope": "support.constant", + "settings": { + "foreground": "#0550ae" + } + }, + { + "scope": "support.variable", + "settings": { + "foreground": "#0550ae" + } + }, + { + "scope": "support.type.property-name.json", + "settings": { + "foreground": "#116329" + } + }, + { + "scope": "meta.module-reference", + "settings": { + "foreground": "#0550ae" + } + }, + { + "scope": "punctuation.definition.list.begin.markdown", + "settings": { + "foreground": "#953800" + } + }, + { + "scope": [ + "markup.heading", + "markup.heading entity.name" + ], + "settings": { + "fontStyle": "bold", + "foreground": "#0550ae" + } + }, + { + "scope": "markup.quote", + "settings": { + "foreground": "#116329" } }, { - "name": "Markup Italic", "scope": "markup.italic", "settings": { - "fontStyle": "italic" + "fontStyle": "italic", + "foreground": "#1f2328" } }, { - "name": "Markup Strikethrough", - "scope": "markup.strikethrough", + "scope": "markup.bold", "settings": { - "fontStyle": "strikethrough" + "fontStyle": "bold", + "foreground": "#1f2328" } }, { - "name": "Markup Underline", - "scope": "markup.underline", + "scope": [ + "markup.underline" + ], "settings": { "fontStyle": "underline" } }, { - "name": "Markup Quote", - "scope": "markup.quote", + "scope": [ + "markup.strikethrough" + ], "settings": { - "foreground": "#8F41AD" + "fontStyle": "strikethrough" } }, { - "name": "Markup List", - "scope": "markup.list", - "settings": { - "foreground": "#46969A" - } - }, - { - "name": "Markup Inline Raw", "scope": "markup.inline.raw", "settings": { - "foreground": "#98863B" + "foreground": "#0550ae" } }, { - "name": "Markup Raw/Fenced Code Block", "scope": [ - "markup.raw", - "markup.fenced_code" + "markup.deleted", + "meta.diff.header.from-file", + "punctuation.definition.deleted" ], "settings": { - "foreground": "#606060" + "background": "#ffebe9", + "foreground": "#82071e" } }, { - "name": "Markup Link", "scope": [ - "meta.link", - "markup.underline.link" + "punctuation.section.embedded" ], "settings": { - "foreground": "#0069CC" + "foreground": "#cf222e" + } + }, + { + "scope": [ + "markup.inserted", + "meta.diff.header.to-file", + "punctuation.definition.inserted" + ], + "settings": { + "background": "#dafbe1", + "foreground": "#116329" + } + }, + { + "scope": [ + "markup.changed", + "punctuation.definition.changed" + ], + "settings": { + "background": "#ffd8b5", + "foreground": "#953800" + } + }, + { + "scope": [ + "markup.ignored", + "markup.untracked" + ], + "settings": { + "foreground": "#eaeef2", + "background": "#0550ae" + } + }, + { + "scope": "meta.diff.range", + "settings": { + "foreground": "#8250df", + "fontStyle": "bold" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#0550ae" + } + }, + { + "scope": "meta.separator", + "settings": { + "fontStyle": "bold", + "foreground": "#0550ae" + } + }, + { + "scope": "meta.output", + "settings": { + "foreground": "#0550ae" + } + }, + { + "scope": [ + "brackethighlighter.tag", + "brackethighlighter.curly", + "brackethighlighter.round", + "brackethighlighter.square", + "brackethighlighter.angle", + "brackethighlighter.quote" + ], + "settings": { + "foreground": "#57606a" + } + }, + { + "scope": "brackethighlighter.unmatched", + "settings": { + "foreground": "#82071e" + } + }, + { + "scope": [ + "constant.other.reference.link", + "string.other.link" + ], + "settings": { + "foreground": "#0a3069" } } ], - "semanticHighlighting": true, - "semanticTokenColors": { - "newOperator": "#AF00DB", - "stringLiteral": "#a31515", - "customLiteral": "#795E26", - "numberLiteral": "#098658" - } + "semanticHighlighting": true } From c30f1117b451443377e1dcf1f0a298cc0259a09f Mon Sep 17 00:00:00 2001 From: RajeshKumar11 Date: Sun, 1 Mar 2026 10:20:29 +0530 Subject: [PATCH 017/448] MCP gateway: apply grace period to Outdated servers same as Unknown Per review feedback: Outdated servers should use the same per-server startup grace period as Unknown servers. A prior fast startup does not guarantee a fast restart, so both states now race startup against the 5s timeout before returning results. --- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 8 +++----- .../mcpGatewayToolBrokerChannel.test.ts | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 58bc42e61ba..264d67d0a8c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -102,19 +102,17 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private async _shouldUseCachedData(server: IMcpServer): Promise { const cacheState = server.cacheState.get(); - if (cacheState === McpServerCacheState.Unknown) { + if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { // On first list call: wait up to the grace period for the server to start. // On subsequent calls: the stored promise is already resolved, returns immediately. + // Outdated servers get the same grace period as Unknown — a prior fast startup + // does not guarantee a fast restart. await this._waitForStartup(server); const newState = server.cacheState.get(); return newState === McpServerCacheState.Live || newState === McpServerCacheState.Cached || newState === McpServerCacheState.RefreshingFromCached; } - if (cacheState === McpServerCacheState.Outdated) { - // Has cached data — refresh in the background without blocking. - void this._ensureServerReady(server); - } return cacheState === McpServerCacheState.Live || cacheState === McpServerCacheState.Cached || cacheState === McpServerCacheState.RefreshingFromCached; diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index 55d2cbb2c0a..c2e92dd5dd3 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -140,6 +140,26 @@ suite('McpGatewayToolBrokerChannel', () => { channel.dispose(); }); + test('starts server and waits within grace period when cache state is outdated', async () => { + const mcpService = new TestMcpService(); + const channel = new McpGatewayToolBrokerChannel(mcpService); + + const server = createServer( + 'collectionA', + 'serverA', + [createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] }))], + McpServerCacheState.Outdated, + ); + + mcpService.servers.set([server], undefined); + const tools = await channel.call(undefined, 'listTools'); + + // Outdated server gets the same grace period as Unknown — started and tools returned. + assert.strictEqual(server.startCalls, 1); + assert.deepStrictEqual(tools.map(t => t.name), ['echo']); + channel.dispose(); + }); + test('returns empty tools and does not re-wait if server does not start within grace period', async () => { const mcpService = new TestMcpService(); // Use a very short grace period so the test does not take 5 s. From 101a6c7dd516e0133111135770d7bc88baf042fb Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:19:40 -0800 Subject: [PATCH 018/448] Use Dark Dimmed tokens for syntax highlighting and diff colors --- extensions/theme-2026/themes/2026-dark.json | 155 ++++++++++++-------- 1 file changed, 90 insertions(+), 65 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index d953a731a53..e16509b8628 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -153,8 +153,10 @@ "editorGutter.background": "#121314", "editorGutter.addedBackground": "#72C892", "editorGutter.deletedBackground": "#F28772", - "diffEditor.insertedTextBackground": "#72C89233", - "diffEditor.removedTextBackground": "#F2877233", + "diffEditor.insertedLineBackground": "#347d3926", + "diffEditor.insertedTextBackground": "#57ab5a4d", + "diffEditor.removedLineBackground": "#c93c3726", + "diffEditor.removedTextBackground": "#f470674d", "editorOverviewRuler.border": "#2A2B2CFF", "editorOverviewRuler.findMatchForeground": "#3a94bc99", "editorOverviewRuler.modifiedForeground": "#6ab890", @@ -273,7 +275,7 @@ "string.comment" ], "settings": { - "foreground": "#8b949e" + "foreground": "#768390" } }, { @@ -282,7 +284,7 @@ "constant.character" ], "settings": { - "foreground": "#ff7b72" + "foreground": "#F47067" } }, { @@ -295,7 +297,7 @@ "entity" ], "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { @@ -305,7 +307,7 @@ "meta.definition.variable" ], "settings": { - "foreground": "#ffa657" + "foreground": "#F69D50" } }, { @@ -319,13 +321,13 @@ "meta.embedded.expression" ], "settings": { - "foreground": "#e6edf3" + "foreground": "#ADBAC7" } }, { "scope": "entity.name.function", "settings": { - "foreground": "#d2a8ff" + "foreground": "#DCBDFB" } }, { @@ -334,13 +336,13 @@ "support.class.component" ], "settings": { - "foreground": "#7ee787" + "foreground": "#8DDB8C" } }, { "scope": "keyword", "settings": { - "foreground": "#ff7b72" + "foreground": "#F47067" } }, { @@ -349,7 +351,7 @@ "storage.type" ], "settings": { - "foreground": "#ff7b72" + "foreground": "#F47067" } }, { @@ -359,7 +361,7 @@ "storage.type.java" ], "settings": { - "foreground": "#e6edf3" + "foreground": "#ADBAC7" } }, { @@ -368,80 +370,79 @@ "string punctuation.section.embedded source" ], "settings": { - "foreground": "#a5d6ff" + "foreground": "#96D0FF" } }, { "scope": "support", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { "scope": "meta.property-name", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { "scope": "variable", "settings": { - "foreground": "#ffa657" + "foreground": "#F69D50" } }, { "scope": "variable.other", "settings": { - "foreground": "#e6edf3" + "foreground": "#ADBAC7" } }, { "scope": "invalid.broken", "settings": { - "fontStyle": "italic", - "foreground": "#ffa198" + "foreground": "#FF938A", + "fontStyle": "italic" } }, { "scope": "invalid.deprecated", "settings": { - "fontStyle": "italic", - "foreground": "#ffa198" + "foreground": "#FF938A", + "fontStyle": "italic" } }, { "scope": "invalid.illegal", "settings": { - "fontStyle": "italic", - "foreground": "#ffa198" + "foreground": "#FF938A", + "fontStyle": "italic" } }, { "scope": "invalid.unimplemented", "settings": { - "fontStyle": "italic", - "foreground": "#ffa198" + "foreground": "#FF938A", + "fontStyle": "italic" } }, { "scope": "carriage-return", "settings": { - "fontStyle": "italic underline", - "background": "#ff7b72", - "foreground": "#f0f6fc", - "content": "^M" + "foreground": "#CDD9E5", + "background": "#F47067", + "fontStyle": "italic underline" } }, { "scope": "message.error", "settings": { - "foreground": "#ffa198" + "foreground": "#FF938A" } }, { "scope": "string variable", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { @@ -450,7 +451,7 @@ "string.regexp" ], "settings": { - "foreground": "#a5d6ff" + "foreground": "#96D0FF" } }, { @@ -461,44 +462,44 @@ "string.regexp string.regexp.arbitrary-repitition" ], "settings": { - "foreground": "#a5d6ff" + "foreground": "#96D0FF" } }, { "scope": "string.regexp constant.character.escape", "settings": { - "fontStyle": "bold", - "foreground": "#7ee787" + "foreground": "#8DDB8C", + "fontStyle": "bold" } }, { "scope": "support.constant", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { "scope": "support.variable", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { "scope": "support.type.property-name.json", "settings": { - "foreground": "#7ee787" + "foreground": "#8DDB8C" } }, { "scope": "meta.module-reference", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { "scope": "punctuation.definition.list.begin.markdown", "settings": { - "foreground": "#ffa657" + "foreground": "#F69D50" } }, { @@ -507,28 +508,28 @@ "markup.heading entity.name" ], "settings": { - "fontStyle": "bold", - "foreground": "#79c0ff" + "foreground": "#6CB6FF", + "fontStyle": "bold" } }, { "scope": "markup.quote", "settings": { - "foreground": "#7ee787" + "foreground": "#8DDB8C" } }, { "scope": "markup.italic", "settings": { - "fontStyle": "italic", - "foreground": "#e6edf3" + "foreground": "#ADBAC7", + "fontStyle": "italic" } }, { "scope": "markup.bold", "settings": { - "fontStyle": "bold", - "foreground": "#e6edf3" + "foreground": "#ADBAC7", + "fontStyle": "bold" } }, { @@ -550,7 +551,7 @@ { "scope": "markup.inline.raw", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { @@ -560,8 +561,8 @@ "punctuation.definition.deleted" ], "settings": { - "background": "#490202", - "foreground": "#ffa198" + "foreground": "#FF938A", + "background": "#5D0F12" } }, { @@ -569,7 +570,7 @@ "punctuation.section.embedded" ], "settings": { - "foreground": "#ff7b72" + "foreground": "#F47067" } }, { @@ -579,8 +580,8 @@ "punctuation.definition.inserted" ], "settings": { - "background": "#04260f", - "foreground": "#7ee787" + "foreground": "#8DDB8C", + "background": "#113417" } }, { @@ -589,8 +590,8 @@ "punctuation.definition.changed" ], "settings": { - "background": "#5a1e02", - "foreground": "#ffa657" + "foreground": "#F69D50", + "background": "#682D0F" } }, { @@ -599,34 +600,34 @@ "markup.untracked" ], "settings": { - "foreground": "#161b22", - "background": "#79c0ff" + "foreground": "#2D333B", + "background": "#6CB6FF" } }, { "scope": "meta.diff.range", "settings": { - "foreground": "#d2a8ff", + "foreground": "#DCBDFB", "fontStyle": "bold" } }, { "scope": "meta.diff.header", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { "scope": "meta.separator", "settings": { - "fontStyle": "bold", - "foreground": "#79c0ff" + "foreground": "#6CB6FF", + "fontStyle": "bold" } }, { "scope": "meta.output", "settings": { - "foreground": "#79c0ff" + "foreground": "#6CB6FF" } }, { @@ -639,13 +640,13 @@ "brackethighlighter.quote" ], "settings": { - "foreground": "#8b949e" + "foreground": "#768390" } }, { "scope": "brackethighlighter.unmatched", "settings": { - "foreground": "#ffa198" + "foreground": "#FF938A" } }, { @@ -654,7 +655,31 @@ "string.other.link" ], "settings": { - "foreground": "#a5d6ff" + "foreground": "#96D0FF" + } + }, + { + "scope": "token.info-token", + "settings": { + "foreground": "#6796E6" + } + }, + { + "scope": "token.warn-token", + "settings": { + "foreground": "#CD9731" + } + }, + { + "scope": "token.error-token", + "settings": { + "foreground": "#F44747" + } + }, + { + "scope": "token.debug-token", + "settings": { + "foreground": "#B267E6" } } ], From 17e938946e06326658d2a1b884a57b75f0d4fd2f Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:45:45 -0800 Subject: [PATCH 019/448] sessions: mcp protocol negotiation, gateway output channel, and customization ui improvements (#298852) * sessions: always show Logs panel in sessions window The Logs/Output panel was previously gated behind dev mode only. Make it always visible so MCP gateway negotiation logs and other output channels are accessible for debugging. * sessions: mcp protocol negotiation, gateway output channel, and customizations UI improvements MCP Gateway Protocol Negotiation: - Gateway now negotiates protocol version with clients instead of hardcoding '2025-11-25', fixing compatibility with older SDK versions - Adds MCP_SUPPORTED_PROTOCOL_VERSIONS covering all known MCP spec versions - Responds with client's requested version if supported, falls back to latest - Logs client info and negotiated version for diagnostics - 7 new unit tests covering all negotiation scenarios MCP Gateway Output Channel: - Dedicated 'MCP Gateway' output channel via ILoggerService (logLevel: always) - Gateway service and sessions now use ILogger instead of ILogService - All gateway lifecycle events visible in Output panel Sessions Window MCP Integration: - Re-enable MCP Servers section in sessions management editor - Add MCP Servers to sessions sidebar toolbar with total count - Add MCP Servers link item in sessions tree view (navigates to editor) - Add MCP Servers to sessions overview view with count from IMcpService - Add chat.experimentalSessionsWindowOverride setting for sessions-specific extension behavior (overridden to true in sessions defaults) MCP List Widget Polish: - Add 'Built-in' group showing extension-provided servers (e.g. GitHub MCP) - Remove per-item server icons, aligning with other customization sections - Hide running/stopped status indicators in sessions window - Match item height (36px), padding, and font styling to other sections - Hide empty description lines to tighten layout Customizations UI Cleanup: - Remove git status badges and SCM service dependency from list widget - Remove per-item storage badge icons (workspace/user/extension) - Remove 'Developer: Customizations Debug' command (replaced by output channel) - Simplify sidebar counts to single total number (no category icon badges) - Remove group separator borders, use spacing only - Fix list container overflow (hidden -> auto) and add min-height: 0 for scroll - Fix layout() fallback from 100px to 0px with requestAnimationFrame re-measure Customizations Debug Output Channel: - New sessions-only 'Customizations Debug' output channel - Streams snapshot on every change: summary table + search paths + file details - Includes MCP server listing with connection states Hooks Count Fix: - Toolbar hook counts now match management editor (per-hook, not per-file) - Uses IFileService to parse hook JSON files and count individual hooks The MCP gateway now negotiates the protocol version with connecting clients instead of hardcoding 2025-11-25. This fixes compatibility with clients using older @modelcontextprotocol/sdk versions that do not support the latest protocol version. * address PR review feedback - Fix stale MCP count in overview: use autorun to watch mcpService.servers - Guard rAF layout callbacks against widget disposal - Make built-in MCP items non-interactive (no pointer cursor, no hover) - Fix _logSnapshot dropping events: re-run if dirty during snapshot - Add CSS for mcp-builtin-readonly items --- src/vs/platform/mcp/node/mcpGatewayService.ts | 37 ++-- src/vs/platform/mcp/node/mcpGatewaySession.ts | 29 ++- .../mcp/test/node/mcpGatewaySession.test.ts | 139 ++++++++++++++ .../browser/aiCustomizationOverviewView.ts | 15 +- .../browser/aiCustomizationTreeViewViews.ts | 49 ++++- .../aiCustomizationWorkspaceService.ts | 3 +- .../customizationsDebugLog.contribution.ts | 179 ++++++++++++++++++ .../browser/configuration.contribution.ts | 1 + .../contrib/logs/browser/logs.contribution.ts | 4 +- .../sessions/browser/customizationCounts.ts | 26 +++ .../customizationsToolbar.contribution.ts | 63 ++---- src/vs/sessions/sessions.desktop.main.ts | 1 + .../aiCustomization/aiCustomizationIcons.ts | 5 + .../aiCustomizationListWidget.ts | 110 ++--------- .../aiCustomizationManagement.contribution.ts | 21 -- .../aiCustomizationManagement.ts | 1 - .../browser/aiCustomization/mcpListWidget.ts | 139 +++++++++++--- .../media/aiCustomizationManagement.css | 23 ++- .../contrib/chat/browser/chat.contribution.ts | 6 + 19 files changed, 624 insertions(+), 227 deletions(-) create mode 100644 src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts diff --git a/src/vs/platform/mcp/node/mcpGatewayService.ts b/src/vs/platform/mcp/node/mcpGatewayService.ts index 8225b3fffe8..c86b23f21db 100644 --- a/src/vs/platform/mcp/node/mcpGatewayService.ts +++ b/src/vs/platform/mcp/node/mcpGatewayService.ts @@ -9,7 +9,7 @@ import { JsonRpcMessage, JsonRpcProtocol } from '../../../base/common/jsonRpcPro import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; -import { ILogService } from '../../log/common/log.js'; +import { ILogger, ILoggerService } from '../../log/common/log.js'; import { IMcpGatewayInfo, IMcpGatewayService, IMcpGatewayToolInvoker } from '../common/mcpGateway.js'; import { isInitializeMessage, McpGatewaySession } from './mcpGatewaySession.js'; @@ -28,11 +28,14 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService /** Maps gatewayId to clientId for tracking ownership */ private readonly _gatewayToClient = new Map(); private _serverStartPromise: Promise | undefined; + private readonly _logger: ILogger; constructor( - @ILogService private readonly _logService: ILogService, + @ILoggerService loggerService: ILoggerService, ) { super(); + this._logger = this._register(loggerService.createLogger('mcpGateway', { name: 'MCP Gateway', logLevel: 'always' })); + this._logger.info('[McpGatewayService] Initialized'); } async createGateway(clientId: unknown, toolInvoker?: IMcpGatewayToolInvoker): Promise { @@ -51,15 +54,15 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService throw new Error('[McpGatewayService] Tool invoker is required to create gateway'); } - const gateway = new McpGatewayRoute(gatewayId, this._logService, toolInvoker); + const gateway = new McpGatewayRoute(gatewayId, this._logger, toolInvoker); this._gateways.set(gatewayId, gateway); // Track client ownership if clientId provided (for cleanup on disconnect) if (clientId) { this._gatewayToClient.set(gatewayId, clientId); - this._logService.info(`[McpGatewayService] Created gateway at http://127.0.0.1:${this._port}/gateway/${gatewayId} for client ${clientId}`); + this._logger.info(`[McpGatewayService] Created gateway at http://127.0.0.1:${this._port}/gateway/${gatewayId} for client ${clientId}`); } else { - this._logService.warn(`[McpGatewayService] Created gateway without client tracking at http://127.0.0.1:${this._port}/gateway/${gatewayId}`); + this._logger.warn(`[McpGatewayService] Created gateway without client tracking at http://127.0.0.1:${this._port}/gateway/${gatewayId}`); } const address = URI.parse(`http://127.0.0.1:${this._port}/gateway/${gatewayId}`); @@ -73,14 +76,14 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService async disposeGateway(gatewayId: string): Promise { const gateway = this._gateways.get(gatewayId); if (!gateway) { - this._logService.warn(`[McpGatewayService] Attempted to dispose unknown gateway: ${gatewayId}`); + this._logger.warn(`[McpGatewayService] Attempted to dispose unknown gateway: ${gatewayId}`); return; } gateway.dispose(); this._gateways.delete(gatewayId); this._gatewayToClient.delete(gatewayId); - this._logService.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`); + this._logger.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`); // If no more gateways, shut down the server if (this._gateways.size === 0) { @@ -98,7 +101,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService } if (gatewaysToDispose.length > 0) { - this._logService.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`); + this._logger.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`); for (const gatewayId of gatewaysToDispose) { this._gateways.get(gatewayId)?.dispose(); @@ -156,19 +159,19 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService } clearTimeout(portTimeout); - this._logService.info(`[McpGatewayService] Server started on port ${this._port}`); + this._logger.info(`[McpGatewayService] Server started on port ${this._port}`); deferredPromise.complete(); }); this._server.on('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EADDRINUSE') { - this._logService.warn('[McpGatewayService] Port in use, retrying with random port...'); + this._logger.warn('[McpGatewayService] Port in use, retrying with random port...'); // Try with a random port this._server!.listen(0, '127.0.0.1'); return; } clearTimeout(portTimeout); - this._logService.error(`[McpGatewayService] Server error: ${err}`); + this._logger.error(`[McpGatewayService] Server error: ${err}`); deferredPromise.error(err); }); @@ -183,13 +186,13 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService return; } - this._logService.info('[McpGatewayService] Stopping server (no more gateways)'); + this._logger.info('[McpGatewayService] Stopping server (no more gateways)'); this._server.close(err => { if (err) { - this._logService.error(`[McpGatewayService] Error closing server: ${err}`); + this._logger.error(`[McpGatewayService] Error closing server: ${err}`); } else { - this._logService.info('[McpGatewayService] Server stopped'); + this._logger.info('[McpGatewayService] Server stopped'); } }); @@ -237,7 +240,7 @@ class McpGatewayRoute extends Disposable { constructor( public readonly gatewayId: string, - private readonly _logService: ILogService, + private readonly _logger: ILogger, private readonly _toolInvoker: IMcpGatewayToolInvoker, ) { super(); @@ -344,7 +347,7 @@ class McpGatewayRoute extends Disposable { res.writeHead(200, headers); res.end(JSON.stringify(Array.isArray(message) ? responses : responses[0])); } catch (error) { - this._logService.error('[McpGatewayService] Failed handling gateway request', error); + this._logger.error('[McpGatewayService] Failed handling gateway request', error); this._respondHttpError(res, 500, 'Internal server error'); } } @@ -366,7 +369,7 @@ class McpGatewayRoute extends Disposable { } const sessionId = generateUuid(); - const session = new McpGatewaySession(sessionId, this._logService, () => { + const session = new McpGatewaySession(sessionId, this._logger, () => { this._sessions.delete(sessionId); }, this._toolInvoker); this._sessions.set(sessionId, session); diff --git a/src/vs/platform/mcp/node/mcpGatewaySession.ts b/src/vs/platform/mcp/node/mcpGatewaySession.ts index 836d6571e3b..f35223a15a3 100644 --- a/src/vs/platform/mcp/node/mcpGatewaySession.ts +++ b/src/vs/platform/mcp/node/mcpGatewaySession.ts @@ -10,11 +10,18 @@ import { } from '../../../base/common/jsonRpcProtocol.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { hasKey } from '../../../base/common/types.js'; -import { ILogService } from '../../log/common/log.js'; +import { ILogger } from '../../log/common/log.js'; import { IMcpGatewayToolInvoker } from '../common/mcpGateway.js'; import { MCP } from '../common/modelContextProtocol.js'; const MCP_LATEST_PROTOCOL_VERSION = '2025-11-25'; +const MCP_SUPPORTED_PROTOCOL_VERSIONS = [ + '2025-11-25', + '2025-06-18', + '2025-03-26', + '2024-11-05', + '2024-10-07', +]; const MCP_INVALID_REQUEST = -32600; const MCP_METHOD_NOT_FOUND = -32601; const MCP_INVALID_PARAMS = -32602; @@ -79,7 +86,7 @@ export class McpGatewaySession extends Disposable { constructor( public readonly id: string, - private readonly _logService: ILogService, + private readonly _logService: ILogger, private readonly _onDidDispose: () => void, private readonly _toolInvoker: IMcpGatewayToolInvoker, ) { @@ -192,7 +199,7 @@ export class McpGatewaySession extends Disposable { private async _handleRequest(request: IJsonRpcRequest): Promise { if (request.method === 'initialize') { - return this._handleInitialize(); + return this._handleInitialize(request); } if (!this._isInitialized) { @@ -225,9 +232,21 @@ export class McpGatewaySession extends Disposable { } } - private _handleInitialize(): MCP.InitializeResult { + private _handleInitialize(request: IJsonRpcRequest): MCP.InitializeResult { + const params = typeof request.params === 'object' && request.params ? request.params as Record : undefined; + const clientVersion = typeof params?.protocolVersion === 'string' ? params.protocolVersion : undefined; + const clientInfo = params?.clientInfo as { name?: string; version?: string } | undefined; + const negotiatedVersion = clientVersion && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion) + ? clientVersion + : MCP_LATEST_PROTOCOL_VERSION; + + this._logService.info(`[McpGateway] Initialize: client=${clientInfo?.name ?? 'unknown'}/${clientInfo?.version ?? '?'}, clientProtocol=${clientVersion ?? '(none)'}, negotiated=${negotiatedVersion}`); + if (clientVersion && clientVersion !== negotiatedVersion) { + this._logService.warn(`[McpGateway] Client requested unsupported protocol version '${clientVersion}', falling back to '${negotiatedVersion}'`); + } + return { - protocolVersion: MCP_LATEST_PROTOCOL_VERSION, + protocolVersion: negotiatedVersion, capabilities: { tools: { listChanged: true, diff --git a/src/vs/platform/mcp/test/node/mcpGatewaySession.test.ts b/src/vs/platform/mcp/test/node/mcpGatewaySession.test.ts index 98712bb9681..d30d166d09f 100644 --- a/src/vs/platform/mcp/test/node/mcpGatewaySession.test.ts +++ b/src/vs/platform/mcp/test/node/mcpGatewaySession.test.ts @@ -112,6 +112,145 @@ suite('McpGatewaySession', () => { onDidChangeResources.dispose(); }); + test('negotiates to older protocol version when client requests it', async () => { + const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); + const session = new McpGatewaySession('session-negotiate-1', new NullLogService(), () => { }, invoker); + + const responses = await session.handleIncoming({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test-client', version: '1.0.0' }, + }, + }); + + assert.strictEqual(responses.length, 1); + const response = responses[0] as IJsonRpcSuccessResponse; + assert.strictEqual((response.result as { protocolVersion: string }).protocolVersion, '2025-03-26'); + session.dispose(); + onDidChangeTools.dispose(); + onDidChangeResources.dispose(); + }); + + test('negotiates to each supported protocol version', async () => { + const supportedVersions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']; + for (const version of supportedVersions) { + const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); + const session = new McpGatewaySession(`session-ver-${version}`, new NullLogService(), () => { }, invoker); + + const responses = await session.handleIncoming({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: version, capabilities: {} }, + }); + + const response = responses[0] as IJsonRpcSuccessResponse; + assert.strictEqual( + (response.result as { protocolVersion: string }).protocolVersion, + version, + `Expected server to negotiate to ${version}` + ); + session.dispose(); + onDidChangeTools.dispose(); + onDidChangeResources.dispose(); + } + }); + + test('falls back to latest version for unsupported client version', async () => { + const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); + const session = new McpGatewaySession('session-negotiate-2', new NullLogService(), () => { }, invoker); + + const responses = await session.handleIncoming({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2099-01-01', + capabilities: {}, + clientInfo: { name: 'test-client', version: '1.0.0' }, + }, + }); + + assert.strictEqual(responses.length, 1); + const response = responses[0] as IJsonRpcSuccessResponse; + assert.strictEqual((response.result as { protocolVersion: string }).protocolVersion, '2025-11-25'); + session.dispose(); + onDidChangeTools.dispose(); + onDidChangeResources.dispose(); + }); + + test('falls back to latest version when no params provided', async () => { + const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); + const session = new McpGatewaySession('session-negotiate-3', new NullLogService(), () => { }, invoker); + + const responses = await session.handleIncoming({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + }); + + assert.strictEqual(responses.length, 1); + const response = responses[0] as IJsonRpcSuccessResponse; + assert.strictEqual((response.result as { protocolVersion: string }).protocolVersion, '2025-11-25'); + session.dispose(); + onDidChangeTools.dispose(); + onDidChangeResources.dispose(); + }); + + test('falls back to latest version when protocolVersion is not a string', async () => { + const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); + const session = new McpGatewaySession('session-negotiate-4', new NullLogService(), () => { }, invoker); + + const responses = await session.handleIncoming({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: 42, + capabilities: {}, + }, + }); + + assert.strictEqual(responses.length, 1); + const response = responses[0] as IJsonRpcSuccessResponse; + assert.strictEqual((response.result as { protocolVersion: string }).protocolVersion, '2025-11-25'); + session.dispose(); + onDidChangeTools.dispose(); + onDidChangeResources.dispose(); + }); + + test('initialize response includes server info and capabilities', async () => { + const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); + const session = new McpGatewaySession('session-init-caps', new NullLogService(), () => { }, invoker); + + const responses = await session.handleIncoming({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2025-03-26', capabilities: {} }, + }); + + const result = (responses[0] as IJsonRpcSuccessResponse).result as MCP.InitializeResult; + assert.deepStrictEqual(result, { + protocolVersion: '2025-03-26', + capabilities: { + tools: { listChanged: true }, + resources: { listChanged: true }, + }, + serverInfo: { + name: 'VS Code MCP Gateway', + version: '1.0.0', + }, + }); + session.dispose(); + onDidChangeTools.dispose(); + onDidChangeResources.dispose(); + }); + test('rejects non-initialize requests before initialized notification', async () => { const { invoker, onDidChangeTools, onDidChangeResources } = createInvoker(); const session = new McpGatewaySession('session-2', new NullLogService(), () => { }, invoker); diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index bf959abffad..d644c5122b5 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -24,10 +24,11 @@ import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyn import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, mcpServerIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; +import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; const $ = DOM.$; @@ -67,6 +68,7 @@ export class AICustomizationOverviewView extends ViewPane { @IPromptsService private readonly promptsService: IPromptsService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, + @IMcpService private readonly mcpService: IMcpService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -76,6 +78,7 @@ export class AICustomizationOverviewView extends ViewPane { { id: AICustomizationManagementSection.Skills, label: localize('skills', "Skills"), icon: skillIcon, count: 0 }, { id: AICustomizationManagementSection.Instructions, label: localize('instructions', "Instructions"), icon: instructionsIcon, count: 0 }, { id: AICustomizationManagementSection.Prompts, label: localize('prompts', "Prompts"), icon: promptIcon, count: 0 }, + { id: AICustomizationManagementSection.McpServers, label: localize('mcpServers', "MCP Servers"), icon: mcpServerIcon, count: 0 }, ); // Listen to changes @@ -173,6 +176,16 @@ export class AICustomizationOverviewView extends ViewPane { } })); + // Update MCP server count reactively + const mcpSection = this.sections.find(s => s.id === AICustomizationManagementSection.McpServers); + if (mcpSection) { + this._register(autorun(reader => { + const servers = this.mcpService.servers.read(reader); + mcpSection.count = servers.length; + this.updateCountElements(); + })); + } + this.updateCountElements(); } diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts index ef3874912b6..7e85b4dd447 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts @@ -27,12 +27,15 @@ import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/ import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IPromptsService, PromptsStorage, IAgentSkill, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { agentIcon, extensionIcon, instructionsIcon, pluginIcon, promptIcon, skillIcon, userIcon, workspaceIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, extensionIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon, userIcon, workspaceIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { AICustomizationItemMenuId } from './aiCustomizationTreeView.js'; +import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; +import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; @@ -95,7 +98,18 @@ interface IAICustomizationFileItem { readonly promptType: PromptsType; } -type AICustomizationTreeItem = IAICustomizationTypeItem | IAICustomizationGroupItem | IAICustomizationFileItem; +/** + * Represents a link item that navigates to the management editor. + */ +interface IAICustomizationLinkItem { + readonly type: 'link'; + readonly id: string; + readonly label: string; + readonly icon: ThemeIcon; + readonly section: AICustomizationManagementSection; +} + +type AICustomizationTreeItem = IAICustomizationTypeItem | IAICustomizationGroupItem | IAICustomizationFileItem | IAICustomizationLinkItem; //#endregion @@ -109,6 +123,7 @@ class AICustomizationTreeDelegate implements IListVirtualDelegate { +class AICustomizationCategoryRenderer implements ITreeRenderer { readonly templateId = 'category'; renderTemplate(container: HTMLElement): ICategoryTemplateData { @@ -145,7 +160,7 @@ class AICustomizationCategoryRenderer implements ITreeRenderer, _index: number, templateData: ICategoryTemplateData): void { + renderElement(node: ITreeNode, _index: number, templateData: ICategoryTemplateData): void { templateData.icon.className = 'icon'; templateData.icon.classList.add(...ThemeIcon.asClassNameArray(node.element.icon)); templateData.label.textContent = node.element.label; @@ -248,6 +263,9 @@ class UnifiedAICustomizationDataSource implements IAsyncDataSource { - if (element.type === 'category') { + if (element.type === 'category' || element.type === 'link') { return element.label; } if (element.type === 'group') { @@ -573,12 +598,18 @@ export class AICustomizationViewPane extends ViewPane { } )); - // Handle double-click to open file - this.treeDisposables.add(this.tree.onDidOpen(e => { + // Handle double-click to open file or navigate to section + this.treeDisposables.add(this.tree.onDidOpen(async e => { if (e.element && e.element.type === 'file') { this.editorService.openEditor({ resource: e.element.uri }); + } else if (e.element && e.element.type === 'link') { + const input = AICustomizationManagementEditorInput.getOrCreate(); + const editor = await this.editorService.openEditor(input, { pinned: true }, MODAL_GROUP); + if (editor instanceof AICustomizationManagementEditor) { + editor.selectSectionById(e.element.section); + } } })); diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 2f1b25038ee..194d9ceb84b 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -96,8 +96,7 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization AICustomizationManagementSection.Instructions, AICustomizationManagementSection.Prompts, AICustomizationManagementSection.Hooks, - // TODO: Re-enable MCP Servers once CLI MCP configuration is unified with VS Code - // AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.McpServers, ]; private static readonly _hooksFilter: IStorageSourceFilter = { diff --git a/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts b/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts new file mode 100644 index 00000000000..fcda250ebc9 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/customizationsDebugLog.contribution.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IPromptsService, PromptsStorage, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; +import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; + +const PROMPT_SECTIONS: { section: AICustomizationManagementSection; type: PromptsType }[] = [ + { section: AICustomizationManagementSection.Agents, type: PromptsType.agent }, + { section: AICustomizationManagementSection.Skills, type: PromptsType.skill }, + { section: AICustomizationManagementSection.Instructions, type: PromptsType.instructions }, + { section: AICustomizationManagementSection.Prompts, type: PromptsType.prompt }, + { section: AICustomizationManagementSection.Hooks, type: PromptsType.hook }, +]; + +class CustomizationsDebugLogContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'sessions.customizationsDebugLog'; + + private readonly _logger: ILogger; + + constructor( + @ILoggerService loggerService: ILoggerService, + @IPromptsService private readonly _promptsService: IPromptsService, + @IAICustomizationWorkspaceService private readonly _workspaceService: IAICustomizationWorkspaceService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IMcpService private readonly _mcpService: IMcpService, + ) { + super(); + this._logger = this._register(loggerService.createLogger('customizationsDebug', { name: 'Customizations Debug' })); + + this._register(this._promptsService.onDidChangeCustomAgents(() => this._logSnapshot())); + this._register(this._promptsService.onDidChangeSlashCommands(() => this._logSnapshot())); + this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._logSnapshot())); + this._register(autorun(reader => { + this._workspaceService.activeProjectRoot.read(reader); + this._logSnapshot(); + })); + this._register(autorun(reader => { + this._mcpService.servers.read(reader); + this._logSnapshot(); + })); + } + + private _pendingSnapshot: Promise | undefined; + private _snapshotDirty = false; + + private _logSnapshot(): void { + if (this._pendingSnapshot) { + this._snapshotDirty = true; + return; + } + this._pendingSnapshot = this._doLogSnapshot().finally(() => { + this._pendingSnapshot = undefined; + if (this._snapshotDirty) { + this._snapshotDirty = false; + this._logSnapshot(); + } + }); + } + + private async _doLogSnapshot(): Promise { + const root = this._workspaceService.getActiveProjectRoot()?.fsPath ?? '(none)'; + + this._logger.info(''); + this._logger.info('=== Customizations Snapshot ==='); + this._logger.info(` Root: ${root}`); + this._logger.info(` Sections: ${this._workspaceService.managementSections.join(', ')}`); + this._logger.info(''); + + // Header + this._logger.info(` ${'Section'.padEnd(16)} ${'Local'.padStart(6)} ${'User'.padStart(6)} ${'Ext'.padStart(6)} ${'Total'.padStart(7)}`); + this._logger.info(` ${'--------'.padEnd(16)} ${'-----'.padStart(6)} ${'----'.padStart(6)} ${'---'.padStart(6)} ${'-----'.padStart(7)}`); + + for (const { section, type } of PROMPT_SECTIONS) { + const filter = this._workspaceService.getStorageSourceFilter(type); + await this._logSectionRow(section, type, filter); + } + + this._logger.info(''); + + // Details per section + for (const { section, type } of PROMPT_SECTIONS) { + const filter = this._workspaceService.getStorageSourceFilter(type); + await this._logSectionDetails(section, type, filter); + } + + // MCP Servers + this._logMcpServers(); + } + + private _logMcpServers(): void { + const servers = this._mcpService.servers.get(); + this._logger.info(` -- MCP Servers (${servers.length}) --`); + if (servers.length === 0) { + this._logger.info(' (none registered)'); + } + for (const server of servers) { + const state = server.connectionState.get(); + const stateStr = state?.state ?? 'unknown'; + this._logger.info(` ${server.definition.label} [${stateStr}] id=${server.definition.id}`); + } + this._logger.info(''); + } + + private async _logSectionRow(section: AICustomizationManagementSection, type: PromptsType, filter: IStorageSourceFilter): Promise { + try { + const [localFiles, userFiles, extensionFiles] = await Promise.all([ + this._promptsService.listPromptFilesForStorage(type, PromptsStorage.local, CancellationToken.None), + this._promptsService.listPromptFilesForStorage(type, PromptsStorage.user, CancellationToken.None), + this._promptsService.listPromptFilesForStorage(type, PromptsStorage.extension, CancellationToken.None), + ]); + const all: IPromptPath[] = [...localFiles, ...userFiles, ...extensionFiles]; + const filtered = applyStorageSourceFilter(all, filter); + const local = filtered.filter(f => f.storage === PromptsStorage.local).length; + const user = filtered.filter(f => f.storage === PromptsStorage.user).length; + const ext = filtered.filter(f => f.storage === PromptsStorage.extension).length; + + this._logger.info(` ${section.padEnd(16)} ${String(local).padStart(6)} ${String(user).padStart(6)} ${String(ext).padStart(6)} ${String(filtered.length).padStart(7)}`); + } catch { + this._logger.info(` ${section.padEnd(16)} (error)`); + } + } + + private async _logSectionDetails(section: AICustomizationManagementSection, type: PromptsType, filter: IStorageSourceFilter): Promise { + try { + // Source folders - where we look for files + const sourceFolders = await this._promptsService.getSourceFolders(type); + if (sourceFolders.length > 0) { + this._logger.info(` -- ${section} --`); + this._logger.info(` Search paths:`); + for (const sf of sourceFolders) { + this._logger.info(` [${sf.storage}] ${sf.uri.fsPath}`); + } + } + + const [localFiles, userFiles, extensionFiles] = await Promise.all([ + this._promptsService.listPromptFilesForStorage(type, PromptsStorage.local, CancellationToken.None), + this._promptsService.listPromptFilesForStorage(type, PromptsStorage.user, CancellationToken.None), + this._promptsService.listPromptFilesForStorage(type, PromptsStorage.extension, CancellationToken.None), + ]); + const all: IPromptPath[] = [...localFiles, ...userFiles, ...extensionFiles]; + const filtered = applyStorageSourceFilter(all, filter); + + if (filtered.length > 0) { + if (sourceFolders.length === 0) { + this._logger.info(` -- ${section} --`); + } + this._logger.info(` Filter: sources=[${filter.sources.join(', ')}]${filter.includedUserFileRoots ? `, roots=[${filter.includedUserFileRoots.map(r => r.fsPath).join(', ')}]` : ''}`); + this._logger.info(` Found ${filtered.length} item(s):`); + for (const f of filtered) { + this._logger.info(` [${f.storage}] ${f.uri.fsPath}`); + } + } + + if (sourceFolders.length > 0 || filtered.length > 0) { + this._logger.info(''); + } + } catch { + // already logged in row + } + } +} + +registerWorkbenchContribution2( + CustomizationsDebugLogContribution.ID, + CustomizationsDebugLogContribution, + WorkbenchPhase.AfterRestored, +); diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index dc83de217d1..e4474e06784 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -8,6 +8,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; Registry.as(Extensions.Configuration).registerDefaultConfigurations([{ overrides: { + 'chat.experimentalSessionsWindowOverride': true, 'chat.agent.maxRequests': 1000, 'chat.customizationsMenu.userStoragePath': '~/.copilot', 'chat.viewSessions.enabled': false, diff --git a/src/vs/sessions/contrib/logs/browser/logs.contribution.ts b/src/vs/sessions/contrib/logs/browser/logs.contribution.ts index b401f4e70ee..14b41083f7d 100644 --- a/src/vs/sessions/contrib/logs/browser/logs.contribution.ts +++ b/src/vs/sessions/contrib/logs/browser/logs.contribution.ts @@ -18,7 +18,6 @@ import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensi import { OutputViewPane } from '../../../../workbench/contrib/output/browser/outputView.js'; import { OUTPUT_VIEW_ID } from '../../../../workbench/services/output/common/output.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; const SESSIONS_LOGS_CONTAINER_ID = 'workbench.sessions.panel.logsContainer'; @@ -32,9 +31,8 @@ class RegisterLogsViewContainerContribution implements IWorkbenchContribution { constructor( @IContextKeyService contextKeyService: IContextKeyService, - @IEnvironmentService environmentService: IEnvironmentService, ) { - CONTEXT_SESSIONS_SHOW_LOGS.bindTo(contextKeyService).set(!environmentService.isBuilt); + CONTEXT_SESSIONS_SHOW_LOGS.bindTo(contextKeyService).set(true); const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index fac485cf511..682c73edc6b 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -7,10 +7,13 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { isEqualOrParent } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { parseHooksFromFile } from '../../../../workbench/contrib/chat/common/promptSyntax/hookCompatibility.js'; +import { parse as parseJSONC } from '../../../../base/common/jsonc.js'; export interface ISourceCounts { readonly workspace: number; @@ -45,6 +48,7 @@ export async function getSourceCounts( filter: IStorageSourceFilter, workspaceContextService: IWorkspaceContextService, workspaceService: IAICustomizationWorkspaceService, + fileService?: IFileService, ): Promise { const items: { storage: PromptsStorage; uri: URI }[] = []; @@ -88,6 +92,28 @@ export async function getSourceCounts( uri: file.uri, }); } + } else if (promptType === PromptsType.hook && fileService) { + // Must match loadItems: parse individual hooks from each file + const hookFiles = await promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None); + const activeRoot = workspaceService.getActiveProjectRoot(); + for (const hookFile of hookFiles) { + try { + const content = await fileService.readFile(hookFile.uri); + const json = parseJSONC(content.value.toString()); + const { hooks } = parseHooksFromFile(hookFile.uri, json, activeRoot, ''); + if (hooks.size > 0) { + for (const [, entry] of hooks) { + for (let i = 0; i < entry.hooks.length; i++) { + items.push({ storage: hookFile.storage, uri: hookFile.uri }); + } + } + } else { + items.push({ storage: hookFile.storage, uri: hookFile.uri }); + } + } catch { + items.push({ storage: hookFile.storage, uri: hookFile.uri }); + } + } } else { // hooks and anything else: uses listPromptFiles const files = await promptsService.listPromptFiles(promptType, CancellationToken.None); diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index 382c1b38051..afbbd572247 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -15,21 +15,22 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; -import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { Menus } from '../../../browser/menus.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, workspaceIcon, userIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, mcpServerIcon, promptIcon, skillIcon, hookIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../base/common/actions.js'; import { $, append } from '../../../../base/browser/dom.js'; import { autorun } from '../../../../base/common/observable.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { ISessionsManagementService } from './sessionsManagementService.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { getSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js'; +import { getSourceCounts, getSourceCountsTotal } from './customizationCounts.js'; import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; @@ -39,7 +40,7 @@ interface ICustomizationItemConfig { readonly icon: ThemeIcon; readonly section: AICustomizationManagementSection; readonly promptType?: PromptsType; - readonly getCount?: (languageModelsService: ILanguageModelsService, mcpService: IMcpService) => Promise; + readonly isMcp?: boolean; } const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ @@ -78,7 +79,13 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ section: AICustomizationManagementSection.Hooks, promptType: PromptsType.hook, }, - // TODO: Re-enable MCP Servers once CLI MCP configuration is unified with VS Code + { + id: 'sessions.customization.mcpServers', + label: localize('mcpServers', "MCP Servers"), + icon: mcpServerIcon, + section: AICustomizationManagementSection.McpServers, + isMcp: true, + }, ]; /** @@ -101,6 +108,7 @@ class CustomizationLinkViewItem extends ActionViewItem { @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, @IAICustomizationWorkspaceService private readonly _workspaceService: IAICustomizationWorkspaceService, + @IFileService private readonly _fileService: IFileService, ) { super(undefined, action, { ...options, icon: false, label: false }); this._viewItemDisposables = this._register(new DisposableStore()); @@ -166,50 +174,19 @@ class CustomizationLinkViewItem extends ActionViewItem { if (this._config.promptType) { const type = this._config.promptType; const filter = this._workspaceService.getStorageSourceFilter(type); - const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService); + const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService, this._fileService); if (requestId !== this._updateCountsRequestId) { return; } - this._renderSourceCounts(this._countContainer, counts); - } else if (this._config.getCount) { - const count = await this._config.getCount(this._languageModelsService, this._mcpService); - if (requestId !== this._updateCountsRequestId) { - return; - } - this._renderSimpleCount(this._countContainer, count); + const total = getSourceCountsTotal(counts, filter); + this._renderTotalCount(this._countContainer, total); + } else if (this._config.isMcp) { + const total = this._mcpService.servers.get().length; + this._renderTotalCount(this._countContainer, total); } } - private _renderSourceCounts(container: HTMLElement, counts: ISourceCounts): void { - container.textContent = ''; - const type = this._config.promptType; - const filter = type ? this._workspaceService.getStorageSourceFilter(type) : this._workspaceService.getStorageSourceFilter(PromptsType.prompt); - const total = getSourceCountsTotal(counts, filter); - container.classList.toggle('hidden', total === 0); - if (total === 0) { - return; - } - - const visibleSourcesSet = new Set(filter.sources); - const sources: { storage: PromptsStorage; count: number; icon: ThemeIcon; title: string }[] = [ - { storage: PromptsStorage.local, count: counts.workspace, icon: workspaceIcon, title: localize('workspaceCount', "{0} from workspace", counts.workspace) }, - { storage: PromptsStorage.user, count: counts.user, icon: userIcon, title: localize('userCount', "{0} from user", counts.user) }, - ]; - - for (const source of sources) { - if (source.count === 0 || !visibleSourcesSet.has(source.storage)) { - continue; - } - const badge = append(container, $('span.source-count-badge')); - badge.title = source.title; - const icon = append(badge, $('span.source-count-icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(source.icon)); - const num = append(badge, $('span.source-count-num')); - num.textContent = `${source.count}`; - } - } - - private _renderSimpleCount(container: HTMLElement, count: number): void { + private _renderTotalCount(container: HTMLElement, count: number): void { container.textContent = ''; container.classList.toggle('hidden', count === 0); if (count > 0) { diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 406ea03e508..a040bb09492 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -203,6 +203,7 @@ import './browser/layoutActions.js'; import './contrib/accountMenu/browser/account.contribution.js'; import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.js'; import './contrib/chat/browser/chat.contribution.js'; +import './contrib/chat/browser/customizationsDebugLog.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts index 4d2fd7a57eb..f6937026881 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts @@ -66,3 +66,8 @@ export const extensionIcon = registerIcon('ai-customization-extension', Codicon. * Icon for plugin storage. */ export const pluginIcon = registerIcon('ai-customization-plugin', Codicon.plug, localize('aiCustomizationPluginIcon', "Icon for plugin-contributed items.")); + +/** + * Icon for MCP servers. + */ +export const mcpServerIcon = registerIcon('ai-customization-mcp-server', Codicon.server, localize('aiCustomizationMcpServerIcon', "Icon for MCP servers.")); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 9f3f4492db7..1e9cccddcf8 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -37,7 +37,6 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IAICustomizationWorkspaceService, applyStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; import { Action, Separator } from '../../../../../base/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { ISCMService } from '../../../scm/common/scm.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IPathService } from '../../../../services/path/common/pathService.js'; @@ -51,7 +50,7 @@ import { OS } from '../../../../../base/common/platform.js'; const $ = DOM.$; const ITEM_HEIGHT = 44; -const GROUP_HEADER_HEIGHT = 32; +const GROUP_HEADER_HEIGHT = 36; const GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; /** @@ -65,7 +64,6 @@ export interface IAICustomizationListItem { readonly description?: string; readonly storage: PromptsStorage; readonly promptType: PromptsType; - gitStatus?: 'uncommitted' | 'committed'; nameMatches?: IMatch[]; descriptionMatches?: IMatch[]; } @@ -116,8 +114,6 @@ interface IAICustomizationItemTemplateData { readonly actionsContainer: HTMLElement; readonly nameLabel: HighlightedLabel; readonly description: HighlightedLabel; - readonly storageBadge: HTMLElement; - readonly gitStatusBadge: HTMLElement; readonly disposables: DisposableStore; readonly elementDisposables: DisposableStore; } @@ -213,15 +209,10 @@ class AICustomizationItemRenderer implements IListRenderer } }) => { - this._register(repo.provider.onDidChangeResources(() => { - this.updateGitStatus(this.allItems); - this.filterItems(); - })); - }; - for (const repo of [...this.scmService.repositories]) { - trackRepoChanges(repo); - } - this._register(this.scmService.onDidAddRepository(repo => trackRepoChanges(repo))); - } private create(): void { @@ -948,36 +885,11 @@ export class AICustomizationListWidget extends Disposable { // Sort items by name items.sort((a, b) => a.name.localeCompare(b.name)); - // Set git status for workspace (local) items - this.updateGitStatus(items); - this.allItems = items; this.filterItems(); this._onDidChangeItemCount.fire(items.length); } - /** - * Updates git status on local workspace items by checking SCM resource groups. - * Files found in resource groups have uncommitted changes; others are committed. - */ - private updateGitStatus(items: IAICustomizationListItem[]): void { - // Build a set of URIs that have uncommitted changes in SCM - const uncommittedUris = new Set(); - for (const repo of [...this.scmService.repositories]) { - for (const group of repo.provider.groups) { - for (const resource of group.resources) { - uncommittedUris.add(resource.sourceUri.toString()); - } - } - } - - for (const item of items) { - if (item.storage === PromptsStorage.local) { - item.gitStatus = uncommittedUris.has(item.uri.toString()) ? 'uncommitted' : 'committed'; - } - } - } - /** * Derives a friendly name from a filename by removing extension suffixes. */ @@ -1201,14 +1113,28 @@ export class AICustomizationListWidget extends Disposable { * Layouts the widget. */ layout(height: number, width: number): void { - const sectionFooterHeight = this.sectionHeader.offsetHeight || 100; + const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; - const margins = 12; // search margin (6+6), not included in offsetHeight - const listHeight = height - sectionFooterHeight - searchBarHeight - margins; + const listHeight = height - sectionFooterHeight - searchBarHeight; this.searchInput.layout(); this.listContainer.style.height = `${Math.max(0, listHeight)}px`; this.list.layout(Math.max(0, listHeight), width); + + // Re-layout once after footer renders if we used a zero fallback + if (sectionFooterHeight === 0) { + DOM.getWindow(this.listContainer).requestAnimationFrame(() => { + if (this._store.isDisposed) { + return; + } + const actualFooterHeight = this.sectionHeader.offsetHeight; + if (actualFooterHeight > 0) { + const correctedHeight = height - actualFooterHeight - searchBarHeight; + this.listContainer.style.height = `${Math.max(0, correctedHeight)}px`; + this.list.layout(Math.max(0, correctedHeight), width); + } + }); + } } /** diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts index 7cdad4e6440..6e0924c3c46 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts @@ -6,7 +6,6 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -308,26 +307,6 @@ class AICustomizationManagementActionsContribution extends Disposable implements } })); - // Toggle Debug Panel in AI Customizations Editor - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: AICustomizationManagementCommands.ToggleDebug, - title: localize2('toggleDebugPanel', "Customizations Debug"), - category: Categories.Developer, - f1: true, - }); - } - - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const pane = editorService.activeEditorPane; - if (pane instanceof AICustomizationManagementEditor) { - const report = await (pane as AICustomizationManagementEditor).generateDebugReport(); - await editorService.openEditor({ resource: undefined, contents: report, options: { pinned: false } }); - } - } - })); } } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts index 55da7d7eb79..6aa4d4ba4d4 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts @@ -26,7 +26,6 @@ export const AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID = 'workbench.input.aiCu */ export const AICustomizationManagementCommands = { OpenEditor: 'aiCustomization.openManagementEditor', - ToggleDebug: 'aiCustomization.toggleDebugPanel', CreateNewAgent: 'aiCustomization.createNewAgent', CreateNewSkill: 'aiCustomization.createNewSkill', CreateNewInstructions: 'aiCustomization.createNewInstructions', diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index 6e6ed17aabe..e92639f9b95 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -28,13 +28,14 @@ import { Delayer } from '../../../../../base/common/async.js'; import { IAction, Separator } from '../../../../../base/common/actions.js'; import { getContextMenuActions } from '../../../../contrib/mcp/browser/mcpServerActions.js'; import { LocalMcpServerScope } from '../../../../services/mcp/common/mcpWorkbenchManagementService.js'; -import { workspaceIcon, userIcon } from './aiCustomizationIcons.js'; +import { workspaceIcon, userIcon, extensionIcon } from './aiCustomizationIcons.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; const $ = DOM.$; -const MCP_ITEM_HEIGHT = 60; -const MCP_GROUP_HEADER_HEIGHT = 32; +const MCP_ITEM_HEIGHT = 36; +const MCP_GROUP_HEADER_HEIGHT = 36; const MCP_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; /** @@ -43,7 +44,7 @@ const MCP_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; interface IMcpGroupHeaderEntry { readonly type: 'group-header'; readonly id: string; - readonly scope: LocalMcpServerScope; + readonly scope: LocalMcpServerScope | 'builtin'; readonly label: string; readonly icon: ThemeIcon; readonly count: number; @@ -60,7 +61,17 @@ interface IMcpServerItemEntry { readonly server: IWorkbenchMcpServer; } -type IMcpListEntry = IMcpGroupHeaderEntry | IMcpServerItemEntry; +/** + * Represents a built-in MCP server provided by an extension. + */ +interface IMcpBuiltinItemEntry { + readonly type: 'builtin-item'; + readonly id: string; + readonly label: string; + readonly description: string; +} + +type IMcpListEntry = IMcpGroupHeaderEntry | IMcpServerItemEntry | IMcpBuiltinItemEntry; /** * Delegate for the MCP server list. @@ -77,6 +88,9 @@ class McpServerItemDelegate implements IListVirtualDelegate { if (element.type === 'group-header') { return 'mcpGroupHeader'; } + if (element.type === 'builtin-item') { + return 'mcpServerItem'; + } const server = element.server; return server.gallery && !server.local ? 'mcpGalleryItem' : 'mcpServerItem'; } @@ -151,7 +165,6 @@ class McpGroupHeaderRenderer implements IListRenderer { +class McpServerItemRenderer implements IListRenderer { readonly templateId = 'mcpServerItem'; constructor( @IMcpService private readonly mcpService: IMcpService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, ) { } renderTemplate(container: HTMLElement): IMcpServerItemTemplateData { container.classList.add('mcp-server-item'); - const icon = DOM.append(container, $('.mcp-server-icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.server)); - const details = DOM.append(container, $('.mcp-server-details')); const name = DOM.append(details, $('.mcp-server-name')); const description = DOM.append(details, $('.mcp-server-description')); const status = DOM.append(container, $('.mcp-server-status')); - return { container, icon, name, description, status, disposables: new DisposableStore() }; + return { container, name, description, status, disposables: new DisposableStore() }; } - renderElement(element: IMcpServerItemEntry, index: number, templateData: IMcpServerItemTemplateData): void { + renderElement(element: IMcpServerItemEntry | IMcpBuiltinItemEntry, index: number, templateData: IMcpServerItemTemplateData): void { templateData.disposables.clear(); + if (element.type === 'builtin-item') { + templateData.container.classList.add('builtin'); + templateData.name.textContent = element.label; + if (element.description) { + templateData.description.textContent = element.description; + templateData.description.style.display = ''; + } else { + templateData.description.style.display = 'none'; + } + templateData.status.style.display = 'none'; + return; + } + + templateData.container.classList.remove('builtin'); templateData.name.textContent = element.server.label; - templateData.description.textContent = element.server.description || ''; + if (element.server.description) { + templateData.description.textContent = element.server.description; + templateData.description.style.display = ''; + } else { + templateData.description.style.display = 'none'; + } // Find the server from IMcpService to get connection state const server = this.mcpService.servers.get().find(s => s.definition.id === element.server.id); @@ -200,6 +230,13 @@ class McpServerItemRenderer implements IListRenderer(); + private readonly collapsedGroups = new Set(); private galleryCts: CancellationTokenSource | undefined; private readonly delayedFilter = new Delayer(200); private readonly delayedGallerySearch = new Delayer(400); @@ -477,6 +510,9 @@ export class McpListWidget extends Disposable { if (element.type === 'group-header') { return localize('mcpGroupAriaLabel', "{0}, {1} items, {2}", element.label, element.count, element.collapsed ? localize('collapsed', "collapsed") : localize('expanded', "expanded")); } + if (element.type === 'builtin-item') { + return element.label; + } return element.server.label; }, getWidgetAriaLabel() { @@ -486,7 +522,13 @@ export class McpListWidget extends Disposable { openOnSingleClick: true, identityProvider: { getId(element: IMcpListEntry) { - return element.type === 'group-header' ? element.id : element.server.id; + if (element.type === 'group-header') { + return element.id; + } + if (element.type === 'builtin-item') { + return element.id; + } + return element.server.id; } } } @@ -496,9 +538,10 @@ export class McpListWidget extends Disposable { if (e.element) { if (e.element.type === 'group-header') { this.toggleGroup(e.element); - } else { + } else if (e.element.type === 'server-item') { this._onDidSelectServer.fire(e.element.server); } + // builtin-item: no action on click (read-only) } })); @@ -619,8 +662,14 @@ export class McpListWidget extends Disposable { this.filteredServers = [...this.mcpWorkbenchService.local]; } + // Find extension-provided servers not in the local list (e.g. GitHub MCP) + const localIds = new Set(this.filteredServers.map(s => s.id)); + const builtinServers = this.mcpService.servers.get() + .filter(s => !localIds.has(s.definition.id)) + .filter(s => !query || s.definition.label.toLowerCase().includes(query)); + // Show empty state only when there are no servers at all (not when filtered to empty) - if (this.filteredServers.length === 0) { + if (this.filteredServers.length === 0 && builtinServers.length === 0) { this.emptyContainer.style.display = 'flex'; this.listContainer.style.display = 'none'; @@ -681,6 +730,32 @@ export class McpListWidget extends Disposable { isFirst = false; } + // Add built-in / extension-provided servers + if (builtinServers.length > 0) { + const collapsed = this.collapsedGroups.has('builtin'); + entries.push({ + type: 'group-header', + id: 'mcp-group-builtin', + scope: 'builtin', + label: localize('builtInGroup', "Built-in"), + icon: extensionIcon, + count: builtinServers.length, + isFirst, + description: localize('builtInGroupDescription', "MCP servers built into VS Code. These are available automatically."), + collapsed, + }); + if (!collapsed) { + for (const server of builtinServers) { + entries.push({ + type: 'builtin-item', + id: `builtin-${server.definition.id}`, + label: server.definition.label, + description: '', + }); + } + } + } + this.displayEntries = entries; this.list.splice(0, this.list.length, this.displayEntries); } @@ -701,14 +776,28 @@ export class McpListWidget extends Disposable { * Layouts the widget. */ layout(height: number, width: number): void { - const sectionFooterHeight = this.sectionHeader.offsetHeight || 100; + const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; - const margins = 12; - const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight - margins; + const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight; this.listContainer.style.height = `${Math.max(0, listHeight)}px`; this.list.layout(Math.max(0, listHeight), width); + + // Re-layout once after footer renders if we used a zero fallback + if (sectionFooterHeight === 0) { + DOM.getWindow(this.listContainer).requestAnimationFrame(() => { + if (this._store.isDisposed) { + return; + } + const actualFooterHeight = this.sectionHeader.offsetHeight; + if (actualFooterHeight > 0) { + const correctedHeight = height - actualFooterHeight - searchBarHeight - backLinkHeight; + this.listContainer.style.height = `${Math.max(0, correctedHeight)}px`; + this.list.layout(Math.max(0, correctedHeight), width); + } + }); + } } /** diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index af43b382e18..dd08e6fb380 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -195,7 +195,8 @@ .ai-customization-list-widget .list-container { flex: 1; - overflow: hidden; + min-height: 0; + overflow: auto; } .ai-customization-list-widget .list-empty-message { @@ -251,11 +252,9 @@ border-radius: 4px; } -/* Separator line above non-first group headers */ +/* Spacing above non-first group headers */ .ai-customization-group-header.has-previous-group { - border-top: 1px solid var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border)); margin-top: 4px; - padding-top: 12px; } .ai-customization-group-header:hover { @@ -648,7 +647,8 @@ .mcp-list-widget .mcp-list-container { flex: 1; - overflow: hidden; + min-height: 0; + overflow: auto; } /* MCP Empty State */ @@ -690,17 +690,23 @@ .mcp-server-item { display: flex; align-items: center; - padding: 8px 12px; + padding: 6px 8px 6px 24px; cursor: pointer; border-radius: 4px; margin: 2px 0; - gap: 12px; + min-height: 32px; + gap: 10px; } .mcp-server-item:hover { background-color: var(--vscode-list-hoverBackground); } +.mcp-server-item.builtin { + cursor: default; + opacity: 0.85; +} + .mcp-server-item .mcp-server-icon { flex-shrink: 0; width: 24px; @@ -721,10 +727,10 @@ .mcp-server-item .mcp-server-name { font-size: 13px; - font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + line-height: 18px; } .mcp-server-item .mcp-server-description { @@ -733,6 +739,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + line-height: 14px; } .mcp-server-item .mcp-server-status { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 09612521caf..c1cb465e781 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -184,6 +184,12 @@ configurationRegistry.registerConfiguration({ title: nls.localize('interactiveSessionConfigurationTitle', "Chat"), type: 'object', properties: { + 'chat.experimentalSessionsWindowOverride': { + type: 'boolean', + description: nls.localize('chat.experimentalSessionsWindowOverride', "When true, enables sessions-window-specific behavior for extensions."), + default: false, + tags: ['experimental'], + }, 'chat.fontSize': { type: 'number', description: nls.localize('chat.fontSize', "Controls the font size in pixels in chat messages."), From 9c46bcc75f3b4788a79e05f938ba4d70852150d4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 2 Mar 2026 20:58:44 -0800 Subject: [PATCH 020/448] Add scrollbar to chat input --- .../chat/browser/widget/input/chatInputPart.ts | 12 +++++++++--- .../contrib/chat/browser/widget/media/chat.css | 11 +++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 627dbaea7a7..05ed2a8a75f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2075,7 +2075,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge showStatusBar: false, insertMode: 'insert', }; - options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' }; + options.scrollbar = this.options.renderStyle === 'compact' + ? { ...(options.scrollbar ?? {}), vertical: 'hidden' } + : { + ...(options.scrollbar ?? {}), + vertical: 'auto', + verticalScrollbarSize: 7, + }; options.stickyScroll = { enabled: false }; this._inputEditorElement = dom.append(editorContainer, $(chatInputEditorContainerSelector)); @@ -3049,8 +3055,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return { editorBorder: 2, - inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, - inputPartHorizontalPaddingInside: 12, + inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 24, + inputPartHorizontalPaddingInside: this.options.renderStyle === 'compact' ? 12 : 10, toolbarsWidth: this.options.renderStyle === 'compact' ? getToolbarsWidthCompact() : 0, sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0, }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index ea94c7c0a09..455e1671b77 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -814,7 +814,7 @@ have to be updated for changes to the rules above, or to support more deeply nes background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); border-radius: var(--vscode-cornerRadius-large); - padding: 0 6px 6px 6px; + padding: 0 0px 6px 6px; /* top padding is inside the editor widget */ width: 100%; position: relative; @@ -1265,6 +1265,8 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; justify-content: space-between; padding-bottom: 0; + /* no scrollbar */ + padding-right: 6px; border-radius: var(--vscode-cornerRadius-small); } @@ -1280,7 +1282,12 @@ have to be updated for changes to the rules above, or to support more deeply nes } .chat-editor-container { - padding: 0 4px; + padding: 0 0 0 4px; +} + +.interactive-session .interactive-input-part.compact .chat-editor-container { + /* No scrollbar */ + padding-right: 4px; } .chat-editor-container .monaco-editor .mtk1 { From 397d3e1b17fb7a861eb25e84d29033a9aa02148b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 3 Mar 2026 16:19:05 +1100 Subject: [PATCH 021/448] Check language model session target (#298862) --- src/vs/workbench/api/common/extHostLanguageModels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 9325afc0185..b6bcbfdbb52 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -364,7 +364,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } for (const [modelIdentifier, modelData] of this._localModels) { - if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat]) { + if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat] && !modelData.metadata.targetChatSessionType) { defaultModelId = modelIdentifier; break; } From 8e445caeffff66b8920466770121e4b53343ebea Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:42:15 -0800 Subject: [PATCH 022/448] Revert "Remove remaining webpack references for building extensions" This reverts commit 3d7cf10fd11065589daa64bdb9c054da1344f37a. --- .eslint-ignore | 2 + build/gulpfile.extensions.ts | 12 + build/lib/extensions.ts | 208 +- .../server/build/javaScriptLibraryLoader.js | 132 ++ .../json-language-features/server/.npmignore | 1 + extensions/mangle-loader.js | 66 + extensions/media-preview/.vscodeignore | 1 + .../mermaid-chat-features/.vscodeignore | 2 + extensions/shared.webpack.config.mjs | 209 ++ package-lock.json | 1445 +++++++++++++ package.json | 9 + test/monaco/package-lock.json | 1893 +---------------- test/monaco/package.json | 9 +- 13 files changed, 2086 insertions(+), 1903 deletions(-) create mode 100644 extensions/html-language-features/server/build/javaScriptLibraryLoader.js create mode 100644 extensions/mangle-loader.js create mode 100644 extensions/shared.webpack.config.mjs diff --git a/.eslint-ignore b/.eslint-ignore index 8b8cdd1c2c7..4736eb5621d 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -18,6 +18,8 @@ **/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts **/extensions/terminal-suggest/third_party/** **/extensions/typescript-language-features/test-workspace/** +**/extensions/typescript-language-features/extension.webpack.config.js +**/extensions/typescript-language-features/extension-browser.webpack.config.js **/extensions/typescript-language-features/package-manager/node-maintainer/** **/extensions/vscode-api-tests/testWorkspace/** **/extensions/vscode-api-tests/testWorkspace2/** diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index e0137816c8c..8f9ac9b2b21 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -309,6 +309,13 @@ async function buildWebExtensions(isWatch: boolean): Promise { { ignore: ['**/node_modules'] } ); + // Find all webpack configs, excluding those that will be esbuilt + const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p))); + const webpackConfigLocations = (await nodeUtil.promisify(glob)( + path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), + { ignore: ['**/node_modules'] } + )).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath))); + const promises: Promise[] = []; // Esbuild for extensions @@ -323,5 +330,10 @@ async function buildWebExtensions(isWatch: boolean): Promise { ); } + // Run webpack for remaining extensions + if (webpackConfigLocations.length > 0) { + promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })))); + } + await Promise.all(promises); } diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index aacf25cbbc1..5710f4d6919 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -20,8 +20,10 @@ import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; +import webpack from 'webpack'; import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; +import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; @@ -30,8 +32,8 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const root = path.dirname(path.dirname(import.meta.dirname)); -// const commit = getVersion(root); -// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +const commit = getVersion(root); +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -63,24 +65,32 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream { +function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb ? 'esbuild.browser.mts' : 'esbuild.mts'; + const webpackConfigFileName = forWeb + ? `extension-browser.webpack.config.js` + : `extension.webpack.config.js`; + const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); + const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - // Esbuild only does bundling so we still want to run a separate type check step + // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), ...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)), ); isBundled = true; + } else if (hasWebpack) { + input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); + isBundled = true; } else { input = fromLocalNormal(extensionPath); } @@ -112,6 +122,132 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean) return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } +function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { + const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); + const webpack = require('webpack'); + const webpackGulp = require('webpack-stream'); + const result = es.through(); + + const packagedDependencies: string[] = []; + const stripOutSourceMaps: string[] = []; + const packageJsonConfig = require(path.join(extensionPath, 'package.json')); + if (packageJsonConfig.dependencies) { + const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = webpackConfig.default; + for (const key in webpackRootConfig.externals) { + if (key in packageJsonConfig.dependencies) { + packagedDependencies.push(key); + } + } + + if (webpackConfig.StripOutSourceMaps) { + for (const filePath of webpackConfig.StripOutSourceMaps) { + stripOutSourceMaps.push(filePath); + } + } + } + + // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar + // to vsce.PackageManager.Yarn. + // A static analysis showed there are no webpack externals that are dependencies of the current + // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list + // as a temporary workaround. + vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { + const files = fileNames + .map(fileName => path.join(extensionPath, fileName)) + .map(filePath => new File({ + path: filePath, + stat: fs.statSync(filePath), + base: extensionPath, + contents: fs.createReadStream(filePath) + })); + + // check for a webpack configuration files, then invoke webpack + // and merge its output with the files stream. + const webpackConfigLocations = (glob.sync( + path.join(extensionPath, '**', webpackConfigFileName), + { ignore: ['**/node_modules'] } + ) as string[]); + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { + + const webpackDone = (err: Error | undefined, stats: any) => { + fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); + if (err) { + result.emit('error', err); + } + const { compilation } = stats; + if (compilation.errors.length > 0) { + result.emit('error', compilation.errors.join('\n')); + } + if (compilation.warnings.length > 0) { + result.emit('error', compilation.warnings.join('\n')); + } + }; + + const exportedConfig = require(webpackConfigPath).default; + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + if (disableMangle) { + if (Array.isArray(config.module.rules)) { + for (const rule of config.module.rules) { + if (Array.isArray(rule.use)) { + for (const use of rule.use) { + if (String(use.loader).endsWith('mangle-loader.js')) { + use.options.disabled = true; + } + } + } + } + } + } + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data: File) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + if (path.extname(data.basename) === '.js') { + if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); + } else { + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + } + } + + this.emit('data', data); + })); + }); + }); + + es.merge(...webpackStreams, es.readArray(files)) + // .pipe(es.through(function (data) { + // // debug + // console.log('out', data.path, data.contents.length); + // this.emit('data', data); + // })) + .pipe(result); + + }).catch(err => { + console.error(extensionPath); + console.error(packagedDependencies); + result.emit('error', err); + }); + + return result.pipe(createStatsStream(path.basename(extensionPath))); +} function fromLocalNormal(extensionPath: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -513,6 +649,70 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); +export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { + const webpack = require('webpack') as typeof import('webpack'); + + const webpackConfigs: webpack.Configuration[] = []; + + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath).default; + function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { + for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { + const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; + if (outputRoot) { + config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!)); + } + webpackConfigs.push(config); + } + } + addConfig(configOrFnOrArray); + } + + function reporter(fullStats: any) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error: any) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning: any) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } else { + reporter(stats?.toJson()); + } + }); + } else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } else { + reporter(stats?.toJson()); + resolve(); + } + }); + } + }); +} + export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise { function reporter(stdError: string, script: string) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); diff --git a/extensions/html-language-features/server/build/javaScriptLibraryLoader.js b/extensions/html-language-features/server/build/javaScriptLibraryLoader.js new file mode 100644 index 00000000000..b8b0f8c4eb6 --- /dev/null +++ b/extensions/html-language-features/server/build/javaScriptLibraryLoader.js @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// a webpack loader that bundles all library definitions (d.ts) for the embedded JavaScript engine. + +const path = require('path'); +const fs = require('fs'); + +const TYPESCRIPT_LIB_SOURCE = path.join(__dirname, '../../../node_modules/typescript/lib'); +const JQUERY_DTS = path.join(__dirname, '../lib/jquery.d.ts'); + +module.exports = function () { + function getFileName(name) { + return (name === '' ? 'lib.d.ts' : `lib.${name}.d.ts`); + } + function readLibFile(name) { + var srcPath = path.join(TYPESCRIPT_LIB_SOURCE, getFileName(name)); + return fs.readFileSync(srcPath).toString(); + } + + var queue = []; + var in_queue = {}; + + var enqueue = function (name) { + if (in_queue[name]) { + return; + } + in_queue[name] = true; + queue.push(name); + }; + + enqueue('es2020.full'); + + var result = []; + while (queue.length > 0) { + var name = queue.shift(); + var contents = readLibFile(name); + var lines = contents.split(/\r\n|\r|\n/); + + var outputLines = []; + for (let i = 0; i < lines.length; i++) { + let m = lines[i].match(/\/\/\/\s*= 0; i--) { + strResult += `"${result[i].name}": ${result[i].output},\n`; + } + strResult += `\n};` + + strResult += `export function loadLibrary(name: string) : string {\n return libs[name] || ''; \n}`; + + return strResult; +} + +/** + * Escape text such that it can be used in a javascript string enclosed by double quotes (") + */ +function escapeText(text) { + // See http://www.javascriptkit.com/jsref/escapesequence.shtml + var _backspace = '\b'.charCodeAt(0); + var _formFeed = '\f'.charCodeAt(0); + var _newLine = '\n'.charCodeAt(0); + var _nullChar = 0; + var _carriageReturn = '\r'.charCodeAt(0); + var _tab = '\t'.charCodeAt(0); + var _verticalTab = '\v'.charCodeAt(0); + var _backslash = '\\'.charCodeAt(0); + var _doubleQuote = '"'.charCodeAt(0); + + var startPos = 0, chrCode, replaceWith = null, resultPieces = []; + + for (var i = 0, len = text.length; i < len; i++) { + chrCode = text.charCodeAt(i); + switch (chrCode) { + case _backspace: + replaceWith = '\\b'; + break; + case _formFeed: + replaceWith = '\\f'; + break; + case _newLine: + replaceWith = '\\n'; + break; + case _nullChar: + replaceWith = '\\0'; + break; + case _carriageReturn: + replaceWith = '\\r'; + break; + case _tab: + replaceWith = '\\t'; + break; + case _verticalTab: + replaceWith = '\\v'; + break; + case _backslash: + replaceWith = '\\\\'; + break; + case _doubleQuote: + replaceWith = '\\"'; + break; + } + if (replaceWith !== null) { + resultPieces.push(text.substring(startPos, i)); + resultPieces.push(replaceWith); + startPos = i + 1; + replaceWith = null; + } + } + resultPieces.push(text.substring(startPos, len)); + return resultPieces.join(''); +} diff --git a/extensions/json-language-features/server/.npmignore b/extensions/json-language-features/server/.npmignore index f85ce05804a..960a01cc7b5 100644 --- a/extensions/json-language-features/server/.npmignore +++ b/extensions/json-language-features/server/.npmignore @@ -6,4 +6,5 @@ test/ tsconfig.json .gitignore package-lock.json +extension.webpack.config.js vscode-json-languageserver-*.tgz diff --git a/extensions/mangle-loader.js b/extensions/mangle-loader.js new file mode 100644 index 00000000000..ed32a85e633 --- /dev/null +++ b/extensions/mangle-loader.js @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check + +const fs = require('fs'); +const webpack = require('webpack'); +const fancyLog = require('fancy-log'); +const ansiColors = require('ansi-colors'); +const { Mangler } = require('../build/lib/mangle/index.js'); + +/** + * Map of project paths to mangled file contents + * + * @type {Map>>} + */ +const mangleMap = new Map(); + +/** + * @param {string} projectPath + */ +function getMangledFileContents(projectPath) { + let entry = mangleMap.get(projectPath); + if (!entry) { + const log = (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data); + log(`Mangling ${projectPath}`); + const ts2tsMangler = new Mangler(projectPath, log, { mangleExports: true, manglePrivateFields: true }); + entry = ts2tsMangler.computeNewFileContents(); + mangleMap.set(projectPath, entry); + } + + return entry; +} + +/** + * @type {webpack.LoaderDefinitionFunction} + */ +module.exports = async function (source, sourceMap, meta) { + if (this.mode !== 'production') { + // Only enable mangling in production builds + return source; + } + if (true) { + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + return source; + } + const options = this.getOptions(); + if (options.disabled) { + // Dynamically disabled + return source; + } + + if (source !== fs.readFileSync(this.resourcePath).toString()) { + // File content has changed by previous webpack steps. + // Skip mangling. + return source; + } + + const callback = this.async(); + + const fileContentsMap = await getMangledFileContents(options.configFile); + + const newContents = fileContentsMap.get(this.resourcePath); + callback(null, newContents?.out ?? source, sourceMap, meta); +}; diff --git a/extensions/media-preview/.vscodeignore b/extensions/media-preview/.vscodeignore index ca6d6ff79d7..8621eb9e9f4 100644 --- a/extensions/media-preview/.vscodeignore +++ b/extensions/media-preview/.vscodeignore @@ -7,3 +7,4 @@ out/** cgmanifest.json package-lock.json preview-src/** +webpack.config.js diff --git a/extensions/mermaid-chat-features/.vscodeignore b/extensions/mermaid-chat-features/.vscodeignore index 485bbd8df38..4722e586990 100644 --- a/extensions/mermaid-chat-features/.vscodeignore +++ b/extensions/mermaid-chat-features/.vscodeignore @@ -1,6 +1,8 @@ src/** +extension.webpack.config.js esbuild.* cgmanifest.json package-lock.json +webpack.config.js tsconfig*.json .gitignore diff --git a/extensions/shared.webpack.config.mjs b/extensions/shared.webpack.config.mjs new file mode 100644 index 00000000000..12b1ea522a4 --- /dev/null +++ b/extensions/shared.webpack.config.mjs @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +import path from 'node:path'; +import fs from 'node:fs'; +import merge from 'merge-options'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import webpack from 'webpack'; +import { createRequire } from 'node:module'; + +/** @typedef {import('webpack').Configuration} WebpackConfig **/ + +const require = createRequire(import.meta.url); + +const tsLoaderOptions = { + compilerOptions: { + 'sourceMap': true, + }, + onlyCompileBundledFiles: true, +}; + +function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfig) { + const defaultConfig = { + mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + target: 'node', // extensions run in a node context + node: { + __dirname: false // leave the __dirname-behaviour intact + }, + + resolve: { + conditionNames: ['import', 'require', 'node-addons', 'node'], + mainFields: ['module', 'main'], + extensions: ['.ts', '.js'], // support ts-files and js-files + extensionAlias: { + // this is needed to resolve dynamic imports that now require the .js extension + '.js': ['.js', '.ts'], + } + }, + module: { + rules: [{ + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + // configure TypeScript loader: + // * enable sources maps for end-to-end source maps + loader: 'ts-loader', + options: tsLoaderOptions + }, + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + // { + // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), + // options: { + // configFile: path.join(extConfig.context, 'tsconfig.json') + // }, + // }, + ] + }] + }, + externals: { + 'electron': 'commonjs electron', // ignored to avoid bundling from node_modules + 'vscode': 'commonjs vscode', // ignored because it doesn't exist, + 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module + '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use + '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module + '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module + '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module + }, + output: { + // all output goes into `dist`. + // packaging depends on that and this must always be like it + filename: '[name].js', + path: path.join(extConfig.context, 'dist'), + libraryTarget: 'commonjs', + }, + // yes, really source maps + devtool: 'source-map', + plugins: nodePlugins(extConfig.context), + }; + + return merge(defaultConfig, extConfig); +} + +/** + * + * @param {string} context + */ +function nodePlugins(context) { + // Need to find the top-most `package.json` file + const folderName = path.relative(import.meta.dirname, context).split(/[\\\/]/)[0]; + const pkgPath = path.join(import.meta.dirname, folderName, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const id = `${pkg.publisher}.${pkg.name}`; + return [ + new CopyWebpackPlugin({ + patterns: [ + { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } + ] + }) + ]; +} +/** + * @typedef {{ + * configFile?: string + * }} AdditionalBrowserConfig + */ + +function withBrowserDefaults(/**@type WebpackConfig & { context: string }*/extConfig, /** @type AdditionalBrowserConfig */ additionalOptions = {}) { + /** @type WebpackConfig */ + const defaultConfig = { + mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + target: 'webworker', // extensions run in a webworker context + resolve: { + mainFields: ['browser', 'module', 'main'], + extensions: ['.ts', '.js'], // support ts-files and js-files + fallback: { + 'path': require.resolve('path-browserify'), + 'os': require.resolve('os-browserify'), + 'util': require.resolve('util') + }, + extensionAlias: { + // this is needed to resolve dynamic imports that now require the .js extension + '.js': ['.js', '.ts'], + }, + }, + module: { + rules: [{ + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + // configure TypeScript loader: + // * enable sources maps for end-to-end source maps + loader: 'ts-loader', + options: { + ...tsLoaderOptions, + // ...(additionalOptions ? {} : { configFile: additionalOptions.configFile }), + } + }, + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + // { + // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), + // options: { + // configFile: path.join(extConfig.context, additionalOptions?.configFile ?? 'tsconfig.json') + // }, + // }, + ] + }, { + test: /\.wasm$/, + type: 'asset/inline' + }] + }, + externals: { + 'vscode': 'commonjs vscode', // ignored because it doesn't exist, + 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module + '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use + '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module + '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module + '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module + }, + performance: { + hints: false + }, + output: { + // all output goes into `dist`. + // packaging depends on that and this must always be like it + filename: '[name].js', + path: path.join(extConfig.context, 'dist', 'browser'), + libraryTarget: 'commonjs', + }, + // yes, really source maps + devtool: 'source-map', + plugins: browserPlugins(extConfig.context) + }; + + return merge(defaultConfig, extConfig); +} + +/** + * + * @param {string} context + */ +function browserPlugins(context) { + // Need to find the top-most `package.json` file + // const folderName = path.relative(__dirname, context).split(/[\\\/]/)[0]; + // const pkgPath = path.join(__dirname, folderName, 'package.json'); + // const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + // const id = `${pkg.publisher}.${pkg.name}`; + return [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + }), + new CopyWebpackPlugin({ + patterns: [ + { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } + ] + }), + new webpack.DefinePlugin({ + 'process.platform': JSON.stringify('web'), + 'process.env': JSON.stringify({}), + 'process.env.BROWSER_ENV': JSON.stringify('true') + }) + ]; +} + +export default withNodeDefaults; +export { withNodeDefaults as node, withBrowserDefaults as browser, nodePlugins, browserPlugins }; diff --git a/package-lock.json b/package-lock.json index fe2fa5c4e3f..bdd35906813 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", + "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -98,6 +99,8 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.6.0", @@ -107,6 +110,7 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", + "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -147,12 +151,17 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", + "style-loader": "^3.3.2", "tar": "^7.5.9", + "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4", + "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, @@ -814,6 +823,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", + "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@electron/get": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", @@ -1356,6 +1374,28 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", @@ -2331,6 +2371,17 @@ "@types/json-schema": "*" } }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2557,6 +2608,17 @@ "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", "dev": true }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.7", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", @@ -3754,6 +3816,167 @@ "hasInstallScript": true, "license": "MIT" }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", @@ -3761,6 +3984,50 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -3889,6 +4156,20 @@ "addons/*" ] }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3922,6 +4203,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", + "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3956,6 +4250,54 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -4579,6 +4921,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5067,6 +5418,24 @@ } } }, + "node_modules/chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chrome-trace-event/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -5265,6 +5634,41 @@ "node": ">= 0.10" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -5376,6 +5780,12 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5576,6 +5986,73 @@ "is-plain-object": "^5.0.0" } }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", + "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -5688,6 +6165,32 @@ "source-map-resolve": "^0.6.0" } }, + "node_modules/css-loader": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz", + "integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -5729,6 +6232,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -6097,6 +6612,18 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -6311,6 +6838,15 @@ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -6364,6 +6900,30 @@ "node": ">=6" } }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7330,6 +7890,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, "node_modules/fastq": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", @@ -7377,6 +7943,44 @@ "node": ">=16.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -8109,6 +8713,13 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/glob-watcher": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", @@ -10249,6 +10860,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -10300,6 +10923,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -10931,6 +11570,37 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -11058,6 +11728,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11513,6 +12189,34 @@ "uc.micro": "^2.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11548,6 +12252,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "dev": true + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -11566,6 +12276,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11958,6 +12674,34 @@ "timers-ext": "^0.1.7" } }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/memory-fs/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -11992,6 +12736,13 @@ "node": ">=4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -12452,6 +13203,25 @@ "dev": true, "optional": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -12518,6 +13288,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -13537,6 +14313,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -13637,6 +14422,70 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/playwright": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", @@ -13732,6 +14581,114 @@ "node": ">=0.10.0" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -13865,6 +14822,12 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY= sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true + }, "node_modules/pseudo-localization": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/pseudo-localization/-/pseudo-localization-2.4.0.tgz", @@ -14454,6 +15417,27 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", @@ -14752,6 +15736,60 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -14989,6 +16027,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15462,6 +16521,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-resolve": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", @@ -15935,6 +17004,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-loader": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", + "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -16221,6 +17306,78 @@ "node": ">=6.0.0" } }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -16548,6 +17705,35 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/ts-morph": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz", @@ -17319,6 +18505,20 @@ "dev": true, "license": "MIT" }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -17331,6 +18531,245 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", + "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", + "dev": true, + "dependencies": { + "fancy-log": "^1.3.3", + "lodash.clone": "^4.3.2", + "lodash.some": "^4.2.2", + "memory-fs": "^0.5.0", + "plugin-error": "^1.0.1", + "supports-color": "^8.1.1", + "through": "^2.3.8", + "vinyl": "^2.2.1" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "webpack": "^5.21.2" + } + }, + "node_modules/webpack-stream/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-stream/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/webpack-stream/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -17381,6 +18820,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, "node_modules/windows-foreground-love": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/windows-foreground-love/-/windows-foreground-love-0.6.1.tgz", diff --git a/package.json b/package.json index 42f49e9b34d..faf767b7e34 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", + "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -167,6 +168,8 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.6.0", @@ -176,6 +179,7 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", + "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -216,12 +220,17 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", + "style-loader": "^3.3.2", "tar": "^7.5.9", + "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4", + "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 088444ad194..513d33eeb34 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -12,72 +12,7 @@ "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "css-loader": "^6.9.1", - "file-loader": "^6.2.0", - "style-loader": "^3.3.2", - "warnings-to-errors-webpack-plugin": "^2.3.0", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "warnings-to-errors-webpack-plugin": "^2.3.0" } }, "node_modules/@types/chai": { @@ -86,42 +21,6 @@ "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", "dev": true }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/junit-report-builder": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", @@ -129,312 +28,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", - "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -487,91 +80,6 @@ "playwright": ">1.0.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001775", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", - "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -598,122 +106,6 @@ "node": "*" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -726,273 +118,6 @@ "node": ">=0.12" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -1003,174 +128,6 @@ "node": "*" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "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/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/junit-report-builder": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz", @@ -1186,58 +143,6 @@ "node": ">=16" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -1261,36 +166,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -1301,105 +176,6 @@ "mustache": "bin/mustache" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -1416,260 +192,6 @@ "dev": true, "license": "ISC" }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1680,197 +202,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -1880,61 +211,6 @@ "node": ">=4" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/warnings-to-errors-webpack-plugin": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/warnings-to-errors-webpack-plugin/-/warnings-to-errors-webpack-plugin-2.3.0.tgz", @@ -1944,173 +220,6 @@ "webpack": "^2.2.0-rc || ^3 || ^4 || ^5" } }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.105.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", - "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/test/monaco/package.json b/test/monaco/package.json index d5cfcacffac..c7373919431 100644 --- a/test/monaco/package.json +++ b/test/monaco/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "compile": "node ../../node_modules/typescript/bin/tsc", - "bundle-webpack": "webpack --config ./webpack.config.js --bail", + "bundle-webpack": "node ../../node_modules/webpack/bin/webpack --config ./webpack.config.js --bail", "esm-check": "node esm-check/esm-check.js", "test": "node runner.js" }, @@ -14,11 +14,6 @@ "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "css-loader": "^6.9.1", - "file-loader": "^6.2.0", - "style-loader": "^3.3.2", - "warnings-to-errors-webpack-plugin": "^2.3.0", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4" + "warnings-to-errors-webpack-plugin": "^2.3.0" } } From 5aefa4caeb76874b77ba5b00075b4f4c37b59cf0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 08:45:21 +0100 Subject: [PATCH 023/448] sessions - disable implicit context entirely (#298884) --- .../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 e4474e06784..d533049d110 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -13,6 +13,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'chat.customizationsMenu.userStoragePath': '~/.copilot', 'chat.viewSessions.enabled': false, 'chat.implicitContext.suggestedContext': false, + 'chat.implicitContext.enabled': { 'panel': 'never' }, 'breadcrumbs.enabled': false, From ccbe5ab0742c5845559c057d9a58abea9120c008 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Mar 2026 08:51:36 +0100 Subject: [PATCH 024/448] Enhance source map handling in NLS plugin and related components - Introduced adjustments for source maps in the NLS plugin to ensure accurate mapping after placeholder replacements. - Implemented deferred processing for source maps to handle edits more effectively, preserving unmapped segments. - Updated tests to validate column mappings and ensure correctness in both minified and non-minified builds. - Improved documentation to reflect changes in source map generation and adjustments. --- build/gulpfile.vscode.ts | 22 ++- build/next/index.ts | 47 ++++-- build/next/nls-plugin.ts | 132 +++++++++------- build/next/private-to-property.ts | 163 +++++++++++++++----- build/next/test/nls-sourcemap.test.ts | 93 ++++++++++- build/next/test/private-to-property.test.ts | 35 +++++ build/next/working.md | 71 ++++++++- 7 files changed, 453 insertions(+), 110 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 0dfb90f264b..686028110b5 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -237,6 +237,8 @@ function runTsGoTypeCheck(): Promise { } const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +const useCdnSourceMapsForPackagingTasks = !!process.env['CI']; +const stripSourceMapsInPackagingTasks = !!process.env['CI']; const minifyVSCodeTask = task.define('minify-vscode', task.series( bundleVSCodeTask, util.rimraf('out-vscode-min'), @@ -349,8 +351,11 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const extensions = gulp.src(['.build/extensions/**', ...platformSpecificBuiltInExtensionsExclusions], { base: '.build', dot: true }); + const sourceFilterPattern = stripSourceMapsInPackagingTasks + ? ['**', '!**/*.{js,css}.map'] + : ['**']; const sources = es.merge(src, extensions) - .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); + .pipe(filter(sourceFilterPattern, { dot: true })); let version = packageJson.version; const quality = (product as { quality?: string }).quality; @@ -420,8 +425,13 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const productionDependencies = getProductionDependencies(root); const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat().concat('!**/*.mk'); + const depFilterPattern = ['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock']; + if (stripSourceMapsInPackagingTasks) { + depFilterPattern.push('!**/*.{js,css}.map'); + } + const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) - .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.{js,css}.map'])) + .pipe(filter(depFilterPattern)) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) .pipe(jsFilter) @@ -696,7 +706,13 @@ BUILD_TARGETS.forEach(buildTarget => { if (useEsbuildTranspile) { const esbuildBundleTask = task.define( `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, - () => runEsbuildBundle(sourceFolderName, !!minified, true, 'desktop', minified ? `${sourceMappingURLBase}/core` : undefined) + () => runEsbuildBundle( + sourceFolderName, + !!minified, + true, + 'desktop', + minified && useCdnSourceMapsForPackagingTasks ? `${sourceMappingURLBase}/core` : undefined + ) ); vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( copyCodiconsTask, diff --git a/build/next/index.ts b/build/next/index.ts index 77886ad43a9..f3043f0fa1f 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -897,6 +897,13 @@ ${tslib}`, const mangleStats: { file: string; result: ConvertPrivateFieldsResult }[] = []; // Map from JS file path to pre-mangle content + edits, for source map adjustment const mangleEdits = new Map(); + // Map from JS file path to pre-NLS content + edits, for source map adjustment + const nlsEdits = new Map(); + // Defer .map files until all .js files are processed, because esbuild may + // emit the .map file in a different build result than the .js file (e.g. + // code-split chunks), and we need the NLS/mangle edits from the .js pass + // to be available when adjusting the .map. + const deferredMaps: { path: string; text: string; contents: Uint8Array }[] = []; for (const { result } of buildResults) { if (!result.outputFiles) { continue; @@ -925,7 +932,12 @@ ${tslib}`, // Apply NLS post-processing if enabled (JS only) if (file.path.endsWith('.js') && doNls && indexMap.size > 0) { - content = postProcessNLS(content, indexMap, preserveEnglish); + const preNLSCode = content; + const nlsResult = postProcessNLS(content, indexMap, preserveEnglish); + content = nlsResult.code; + if (nlsResult.edits.length > 0) { + nlsEdits.set(file.path, { preNLSCode, edits: nlsResult.edits }); + } } // Rewrite sourceMappingURL to CDN URL if configured @@ -943,16 +955,8 @@ ${tslib}`, await fs.promises.writeFile(file.path, content); } else if (file.path.endsWith('.map')) { - // Source maps may need adjustment if private fields were mangled - const jsPath = file.path.replace(/\.map$/, ''); - const editInfo = mangleEdits.get(jsPath); - if (editInfo) { - const mapJson = JSON.parse(file.text); - const adjusted = adjustSourceMap(mapJson, editInfo.preMangleCode, editInfo.edits); - await fs.promises.writeFile(file.path, JSON.stringify(adjusted)); - } else { - await fs.promises.writeFile(file.path, file.contents); - } + // Defer .map processing until all .js files have been handled + deferredMaps.push({ path: file.path, text: file.text, contents: file.contents }); } else { // Write other files (assets, etc.) as-is await fs.promises.writeFile(file.path, file.contents); @@ -961,6 +965,27 @@ ${tslib}`, bundled++; } + // Second pass: process deferred .map files now that all mangle/NLS edits + // have been collected from .js processing above. + for (const mapFile of deferredMaps) { + const jsPath = mapFile.path.replace(/\.map$/, ''); + const mangle = mangleEdits.get(jsPath); + const nls = nlsEdits.get(jsPath); + + if (mangle || nls) { + let mapJson = JSON.parse(mapFile.text); + if (mangle) { + mapJson = adjustSourceMap(mapJson, mangle.preMangleCode, mangle.edits); + } + if (nls) { + mapJson = adjustSourceMap(mapJson, nls.preNLSCode, nls.edits); + } + await fs.promises.writeFile(mapFile.path, JSON.stringify(mapJson)); + } else { + await fs.promises.writeFile(mapFile.path, mapFile.contents); + } + } + // Log mangle-privates stats if (doManglePrivates && mangleStats.length > 0) { let totalClasses = 0, totalFields = 0, totalEdits = 0, totalElapsed = 0; diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 7be3faccf24..9f3bfa01e35 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -12,6 +12,7 @@ import { analyzeLocalizeCalls, parseLocalizeKeyOrValue } from '../lib/nls-analysis.ts'; +import type { TextEdit } from './private-to-property.ts'; // ============================================================================ // Types @@ -148,12 +149,13 @@ export async function finalizeNLS( /** * Post-processes a JavaScript file to replace NLS placeholders with indices. + * Returns the transformed code and the edits applied (for source map adjustment). */ export function postProcessNLS( content: string, indexMap: Map, preserveEnglish: boolean -): string { +): { code: string; edits: readonly TextEdit[] } { return replaceInOutput(content, indexMap, preserveEnglish); } @@ -244,7 +246,7 @@ function generateNLSSourceMap( const generator = new SourceMapGenerator(); generator.setSourceContent(filePath, originalSource); - const lineCount = originalSource.split('\n').length; + const lines = originalSource.split('\n'); // Group edits by line const editsByLine = new Map(); @@ -257,7 +259,7 @@ function generateNLSSourceMap( arr.push(edit); } - for (let line = 0; line < lineCount; line++) { + for (let line = 0; line < lines.length; line++) { const smLine = line + 1; // source maps use 1-based lines // Always map start of line @@ -273,7 +275,8 @@ function generateNLSSourceMap( let cumulativeShift = 0; - for (const edit of lineEdits) { + for (let i = 0; i < lineEdits.length; i++) { + const edit = lineEdits[i]; const origLen = edit.endCol - edit.startCol; // Map start of edit: the replacement begins at the same original position @@ -285,12 +288,20 @@ function generateNLSSourceMap( cumulativeShift += edit.newLength - origLen; - // Map content after edit: columns resume with the shift applied - generator.addMapping({ - generated: { line: smLine, column: edit.endCol + cumulativeShift }, - original: { line: smLine, column: edit.endCol }, - source: filePath, - }); + // Source maps don't interpolate columns — each query resolves to the + // last segment with generatedColumn <= queryColumn. A single mapping + // at edit-end would cause every subsequent column on this line to + // collapse to that one original position. Add per-column identity + // mappings from edit-end to the next edit (or end of line) so that + // esbuild's source-map composition preserves fine-grained accuracy. + const nextBound = i + 1 < lineEdits.length ? lineEdits[i + 1].startCol : lines[line].length; + for (let origCol = edit.endCol; origCol < nextBound; origCol++) { + generator.addMapping({ + generated: { line: smLine, column: origCol + cumulativeShift }, + original: { line: smLine, column: origCol }, + source: filePath, + }); + } } } } @@ -302,17 +313,19 @@ function replaceInOutput( content: string, indexMap: Map, preserveEnglish: boolean -): string { - // Replace all placeholders in a single pass using regex - // Two types of placeholders: - // - %%NLS:moduleId#key%% for localize() - message replaced with null - // - %%NLS2:moduleId#key%% for localize2() - message preserved - // Note: esbuild may use single or double quotes, so we handle both +): { code: string; edits: readonly TextEdit[] } { + // Collect all matches first, then apply from back to front so that byte + // offsets remain valid. Each match becomes a TextEdit in terms of the + // ORIGINAL content offsets, which is what adjustSourceMap expects. + + interface PendingEdit { start: number; end: number; replacement: string } + const pending: PendingEdit[] = []; if (preserveEnglish) { - // Just replace the placeholder with the index (both NLS and NLS2) - return content.replace(/["']%%NLS2?:([^%]+)%%["']/g, (match, inner) => { - // Try NLS first, then NLS2 + const re = /["']%%NLS2?:([^%]+)%%["']/g; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const inner = m[1]; let placeholder = `%%NLS:${inner}%%`; let index = indexMap.get(placeholder); if (index === undefined) { @@ -320,45 +333,60 @@ function replaceInOutput( index = indexMap.get(placeholder); } if (index !== undefined) { - return String(index); + pending.push({ start: m.index, end: m.index + m[0].length, replacement: String(index) }); } - // Placeholder not found in map, leave as-is (shouldn't happen) - return match; - }); + } } else { - // For NLS (localize): replace placeholder with index AND replace message with null - // For NLS2 (localize2): replace placeholder with index, keep message - // Note: Use (?:[^"\\]|\\.)* to properly handle escaped quotes like \" or \\ - // Note: esbuild may use single or double quotes, so we handle both - - // First handle NLS (localize) - replace both key and message - content = content.replace( - /["']%%NLS:([^%]+)%%["'](\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, - (match, inner, comma) => { - const placeholder = `%%NLS:${inner}%%`; - const index = indexMap.get(placeholder); - if (index !== undefined) { - return `${index}${comma}null`; - } - return match; + // NLS (localize): replace placeholder with index AND replace message with null + const reNLS = /["']%%NLS:([^%]+)%%["'](\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g; + let m: RegExpExecArray | null; + while ((m = reNLS.exec(content)) !== null) { + const inner = m[1]; + const comma = m[2]; + const placeholder = `%%NLS:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + pending.push({ start: m.index, end: m.index + m[0].length, replacement: `${index}${comma}null` }); } - ); + } - // Then handle NLS2 (localize2) - replace only key, keep message - content = content.replace( - /["']%%NLS2:([^%]+)%%["']/g, - (match, inner) => { - const placeholder = `%%NLS2:${inner}%%`; - const index = indexMap.get(placeholder); - if (index !== undefined) { - return String(index); - } - return match; + // NLS2 (localize2): replace only key, keep message + const reNLS2 = /["']%%NLS2:([^%]+)%%["']/g; + while ((m = reNLS2.exec(content)) !== null) { + const inner = m[1]; + const placeholder = `%%NLS2:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + pending.push({ start: m.index, end: m.index + m[0].length, replacement: String(index) }); } - ); - - return content; + } } + + if (pending.length === 0) { + return { code: content, edits: [] }; + } + + // Sort by offset ascending, then apply back-to-front to keep offsets valid + pending.sort((a, b) => a.start - b.start); + + // Build TextEdit[] (in original-content coordinates) and apply edits + const edits: TextEdit[] = []; + for (const p of pending) { + edits.push({ start: p.start, end: p.end, newText: p.replacement }); + } + + // Apply edits using forward-scanning parts array — O(N+K) instead of + // O(N*K) from repeated substring concatenation on large strings. + const parts: string[] = []; + let lastEnd = 0; + for (const p of pending) { + parts.push(content.substring(lastEnd, p.start)); + parts.push(p.replacement); + lastEnd = p.end; + } + parts.push(content.substring(lastEnd)); + + return { code: parts.join(''), edits }; } // ============================================================================ diff --git a/build/next/private-to-property.ts b/build/next/private-to-property.ts index 11f977774a5..98ff98a6440 100644 --- a/build/next/private-to-property.ts +++ b/build/next/private-to-property.ts @@ -220,15 +220,53 @@ export function adjustSourceMap( return sourceMapJson; } - // Build a line-offset table for the original code to convert byte offsets to line/column - const lineStarts: number[] = [0]; - for (let i = 0; i < originalCode.length; i++) { - if (originalCode.charCodeAt(i) === 10 /* \n */) { - lineStarts.push(i + 1); - } + // Build line-offset tables for the original code and the code after edits. + // When edits span newlines (e.g. NLS replacing a multi-line template literal + // with `null`), subsequent lines shift up and columns change. We handle this + // by converting each mapping's old generated (line, col) to a byte offset, + // adjusting the offset for the edits, then converting back to (line, col) in + // the post-edit coordinate system. + + const oldLineStarts = buildLineStarts(originalCode); + const newLineStarts = buildLineStartsAfterEdits(originalCode, edits); + + // Precompute cumulative byte-shift after each edit for binary search + const n = edits.length; + const editStarts: number[] = new Array(n); + const editEnds: number[] = new Array(n); + const cumShifts: number[] = new Array(n); // cumulative shift *after* edit[i] + let cumShift = 0; + for (let i = 0; i < n; i++) { + editStarts[i] = edits[i].start; + editEnds[i] = edits[i].end; + cumShift += edits[i].newText.length - (edits[i].end - edits[i].start); + cumShifts[i] = cumShift; } - function offsetToLineCol(offset: number): { line: number; col: number } { + function adjustOffset(oldOff: number): number { + // Binary search: find last edit with start <= oldOff + let lo = 0, hi = n - 1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (editStarts[mid] <= oldOff) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + // hi = index of last edit where start <= oldOff, or -1 if none + if (hi < 0) { + return oldOff; + } + if (oldOff < editEnds[hi]) { + // Inside edit range — clamp to edit start in new coordinates + const prevShift = hi > 0 ? cumShifts[hi - 1] : 0; + return editStarts[hi] + prevShift; + } + return oldOff + cumShifts[hi]; + } + + function offsetToLineCol(lineStarts: readonly number[], offset: number): { line: number; col: number } { let lo = 0, hi = lineStarts.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; @@ -241,23 +279,9 @@ export function adjustSourceMap( return { line: lo, col: offset - lineStarts[lo] }; } - // Convert edits from byte offsets to per-line column shifts - interface LineEdit { col: number; origLen: number; newLen: number } - const editsByLine = new Map(); - for (const edit of edits) { - const pos = offsetToLineCol(edit.start); - const origLen = edit.end - edit.start; - let arr = editsByLine.get(pos.line); - if (!arr) { - arr = []; - editsByLine.set(pos.line, arr); - } - arr.push({ col: pos.col, origLen, newLen: edit.newText.length }); - } - // Use source-map library to read, adjust, and write const consumer = new SourceMapConsumer(sourceMapJson); - const generator = new SourceMapGenerator({ file: sourceMapJson.file }); + const generator = new SourceMapGenerator({ file: sourceMapJson.file, sourceRoot: sourceMapJson.sourceRoot }); // Copy sourcesContent for (let i = 0; i < sourceMapJson.sources.length; i++) { @@ -267,15 +291,19 @@ export function adjustSourceMap( } } - // Walk every mapping, adjust the generated column, and add to the new generator + // Walk every mapping, convert old generated position → byte offset → adjust → new position consumer.eachMapping(mapping => { - const lineEdits = editsByLine.get(mapping.generatedLine - 1); // 0-based for our data - const adjustedCol = adjustColumn(mapping.generatedColumn, lineEdits); + const oldLine0 = mapping.generatedLine - 1; // 0-based + const oldOff = (oldLine0 < oldLineStarts.length + ? oldLineStarts[oldLine0] + : oldLineStarts[oldLineStarts.length - 1]) + mapping.generatedColumn; + + const newOff = adjustOffset(oldOff); + const newPos = offsetToLineCol(newLineStarts, newOff); - // Some mappings may be unmapped (no original position/source) - skip those. if (mapping.source !== null && mapping.originalLine !== null && mapping.originalColumn !== null) { const newMapping: Mapping = { - generated: { line: mapping.generatedLine, column: adjustedCol }, + generated: { line: newPos.line + 1, column: newPos.col }, original: { line: mapping.originalLine, column: mapping.originalColumn }, source: mapping.source, }; @@ -283,25 +311,82 @@ export function adjustSourceMap( newMapping.name = mapping.name; } generator.addMapping(newMapping); + } else { + // Preserve unmapped segments (generated-only mappings with no original + // position). These create essential "gaps" that prevent + // originalPositionFor() from wrongly interpolating between distant + // valid mappings on the same line in minified output. + // eslint-disable-next-line local/code-no-dangerous-type-assertions + generator.addMapping({ + generated: { line: newPos.line + 1, column: newPos.col }, + } as Mapping); } }); return JSON.parse(generator.toString()); } -function adjustColumn(col: number, lineEdits: { col: number; origLen: number; newLen: number }[] | undefined): number { - if (!lineEdits) { - return col; - } - let shift = 0; - for (const edit of lineEdits) { - if (edit.col + edit.origLen <= col) { - shift += edit.newLen - edit.origLen; - } else if (edit.col < col) { - return edit.col + shift; - } else { +function buildLineStarts(text: string): number[] { + const starts: number[] = [0]; + let pos = 0; + while (true) { + const nl = text.indexOf('\n', pos); + if (nl === -1) { break; } + starts.push(nl + 1); + pos = nl + 1; } - return col + shift; + return starts; +} + +/** + * Compute line starts for the code that results from applying `edits` to + * `originalCode`, without materialising the full new string. + */ +function buildLineStartsAfterEdits(originalCode: string, edits: readonly TextEdit[]): number[] { + const starts: number[] = [0]; + let oldPos = 0; + let newPos = 0; + + for (const edit of edits) { + // Scan unchanged region [oldPos, edit.start) for newlines + let from = oldPos; + while (true) { + const nl = originalCode.indexOf('\n', from); + if (nl === -1 || nl >= edit.start) { + break; + } + starts.push(newPos + (nl - oldPos) + 1); + from = nl + 1; + } + newPos += edit.start - oldPos; + + // Scan replacement text for newlines + let replFrom = 0; + while (true) { + const nl = edit.newText.indexOf('\n', replFrom); + if (nl === -1) { + break; + } + starts.push(newPos + nl + 1); + replFrom = nl + 1; + } + newPos += edit.newText.length; + + oldPos = edit.end; + } + + // Scan remaining unchanged text after last edit + let from = oldPos; + while (true) { + const nl = originalCode.indexOf('\n', from); + if (nl === -1) { + break; + } + starts.push(newPos + (nl - oldPos) + 1); + from = nl + 1; + } + + return starts; } diff --git a/build/next/test/nls-sourcemap.test.ts b/build/next/test/nls-sourcemap.test.ts index fd732b86802..09bad2f5c27 100644 --- a/build/next/test/nls-sourcemap.test.ts +++ b/build/next/test/nls-sourcemap.test.ts @@ -10,6 +10,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { type RawSourceMap, SourceMapConsumer } from 'source-map'; import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from '../nls-plugin.ts'; +import { adjustSourceMap } from '../private-to-property.ts'; // analyzeLocalizeCalls requires the import path to end with `/nls` const NLS_STUB = [ @@ -36,7 +37,7 @@ interface BundleResult { async function bundleWithNLS( files: Record, entryPoint: string, - opts?: { postProcess?: boolean } + opts?: { postProcess?: boolean; minify?: boolean } ): Promise { const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'nls-sm-test-')); const srcDir = path.join(tmpDir, 'src'); @@ -64,6 +65,7 @@ async function bundleWithNLS( packages: 'external', sourcemap: 'linked', sourcesContent: true, + minify: opts?.minify ?? false, write: false, plugins: [ nlsPlugin({ baseDir: srcDir, collector }), @@ -91,7 +93,16 @@ async function bundleWithNLS( // Optionally apply NLS post-processing (replaces placeholders with indices) if (opts?.postProcess) { const nlsResult = await finalizeNLS(collector, outDir); - jsContent = postProcessNLS(jsContent, nlsResult.indexMap, false); + const preNLSCode = jsContent; + const nlsProcessed = postProcessNLS(jsContent, nlsResult.indexMap, false); + jsContent = nlsProcessed.code; + + // Adjust source map for NLS edits + if (nlsProcessed.edits.length > 0) { + const mapJson = JSON.parse(mapContent); + const adjusted = adjustSourceMap(mapJson, preNLSCode, nlsProcessed.edits); + mapContent = JSON.stringify(adjusted); + } } assert.ok(jsContent, 'Expected JS output'); @@ -370,4 +381,82 @@ suite('NLS plugin source maps', () => { cleanup(); } }); + + test('post-processed NLS - column mappings correct after placeholder replacement', async () => { + // NLS placeholders like "%%NLS:test/drift#k%%" are much longer than their + // replacements (e.g. "0"). Without source map adjustment the columns for + // tokens AFTER the replacement drift by the cumulative length delta. + const source = [ + 'import { localize } from "../vs/nls";', // 1 + 'export const a = localize("k1", "Alpha"); export const MARKER = "FINDME";', // 2 + ].join('\n'); + + const { js, map, cleanup } = await bundleWithNLS( + { 'test/drift.ts': source }, + 'test/drift.ts', + { postProcess: true } + ); + + try { + assert.ok(!js.includes('%%NLS:'), 'Placeholders should be replaced'); + + const bundleLine = findLine(js, 'FINDME'); + const bundleCol = findColumn(js, '"FINDME"'); + const pos = map.originalPositionFor({ line: bundleLine, column: bundleCol }); + + assert.ok(pos.source, 'Should have source'); + assert.strictEqual(pos.line, 2, 'Should map to line 2'); + + const originalCol = findColumn(source, '"FINDME"'); + const columnDrift = Math.abs(pos.column! - originalCol); + assert.ok(columnDrift <= 20, + `Column drift after NLS post-processing should be small. ` + + `Expected ~${originalCol}, got ${pos.column} (drift: ${columnDrift}). ` + + `Large drift means postProcessNLS edits were not applied to the source map.`); + } finally { + cleanup(); + } + }); + + test('minified bundle with NLS - end-to-end column mapping', async () => { + // With minification, the entire output is (roughly) on one line. + // Multiple NLS replacements compound their column shifts. A function + // defined after several localize() calls must still map correctly. + const source = [ + 'import { localize } from "../vs/nls";', // 1 + '', // 2 + 'export const a = localize("k1", "Alpha message");', // 3 + 'export const b = localize("k2", "Bravo message that is quite long");', // 4 + 'export const c = localize("k3", "Charlie");', // 5 + 'export const d = localize("k4", "Delta is the fourth letter");', // 6 + '', // 7 + 'export function computeResult(x: number): number {', // 8 + '\treturn x * 42;', // 9 + '}', // 10 + ].join('\n'); + + const { js, map, cleanup } = await bundleWithNLS( + { 'test/minified.ts': source }, + 'test/minified.ts', + { postProcess: true, minify: true } + ); + + try { + assert.ok(!js.includes('%%NLS:'), 'Placeholders should be replaced'); + + // Find the computeResult function in the minified output. + // esbuild minifies `x * 42` and may rename the parameter, so + // search for `*42` which survives both minification and renaming. + const needle = '*42'; + const bundleLine = findLine(js, needle); + const bundleCol = findColumn(js, needle); + const pos = map.originalPositionFor({ line: bundleLine, column: bundleCol }); + + assert.ok(pos.source, 'Should have source for minified mapping'); + assert.strictEqual(pos.line, 9, + `Should map "*42" back to line 9. Got line ${pos.line}.`); + } finally { + cleanup(); + } + }); }); diff --git a/build/next/test/private-to-property.test.ts b/build/next/test/private-to-property.test.ts index aa9da72ce9a..9b976797679 100644 --- a/build/next/test/private-to-property.test.ts +++ b/build/next/test/private-to-property.test.ts @@ -439,6 +439,41 @@ suite('adjustSourceMap', () => { assert.strictEqual(pos.column, origGetValueCol, 'getValue column should match original'); }); + test('multi-line edit: removing newlines shifts subsequent lines up', () => { + // Simulates the NLS scenario: a template literal with embedded newlines + // is replaced with `null`, collapsing 3 lines into 1. + const code = [ + 'var a = "hello";', // line 0 (0-based) + 'var b = `line1', // line 1 + 'line2', // line 2 + 'line3`;', // line 3 + 'var c = "world";', // line 4 + ].join('\n'); + const map = createIdentitySourceMap(code, 'test.js'); + + // Replace the template literal `line1\nline2\nline3` with `null` + // (keeps `var b = ` and `;` intact) + const tplStart = code.indexOf('`line1'); + const tplEnd = code.indexOf('line3`') + 'line3`'.length; + const edits = [{ start: tplStart, end: tplEnd, newText: 'null' }]; + + const result = adjustSourceMap(map, code, edits); + const consumer = new SourceMapConsumer(result); + + // After edit, code is: + // "var a = \"hello\";\nvar b = null;\nvar c = \"world\";" + // "var c" was on line 5 (1-based), now on line 3 (1-based) since 2 newlines removed + + // 'var c' at original line 5, col 0 should now map at generated line 3 + const pos = consumer.originalPositionFor({ line: 3, column: 0 }); + assert.strictEqual(pos.line, 5, 'var c should map to original line 5'); + assert.strictEqual(pos.column, 0, 'var c column should be 0'); + + // 'var a' on line 1 should be unaffected + const posA = consumer.originalPositionFor({ line: 1, column: 0 }); + assert.strictEqual(posA.line, 1, 'var a should still map to original line 1'); + }); + test('brand check: #field in obj -> string replacement adjusts map', () => { const code = 'class C { #x; check(o) { return #x in o; } }'; const map = createIdentitySourceMap(code, 'test.js'); diff --git a/build/next/working.md b/build/next/working.md index 298d1fb8cbd..a7ea64db8b6 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -222,13 +222,13 @@ Two categories of corruption: 2. **`--source-map-base-url` option** - Rewrites `sourceMappingURL` comments to point to CDN URLs. -3. **NLS plugin inline source maps** (`nls-plugin.ts`) - The `onLoad` handler now generates an inline source map (`//# sourceMappingURL=data:...`) mapping from NLS-transformed source back to original. esbuild composes this with its own bundle source map. `SourceMapGenerator.setSourceContent` embeds the original source so `sourcesContent` in the final `.map` has the real TypeScript. Tests in `test/nls-sourcemap.test.ts`. +3. **NLS plugin inline source maps** (`nls-plugin.ts`) - The `onLoad` handler generates an inline source map (`//# sourceMappingURL=data:...`) mapping from NLS-transformed source back to original. esbuild composes this with its own bundle source map. `SourceMapGenerator.setSourceContent` embeds the original source so `sourcesContent` in the final `.map` has the real TypeScript. `generateNLSSourceMap` adds per-column identity mappings after each edit on a line so that esbuild's source-map composition preserves fine-grained column accuracy (source maps don't interpolate columns — they use binary search, so a single boundary mapping would collapse all subsequent columns to the edit-end position). Tests in `test/nls-sourcemap.test.ts`. 4. **`convertPrivateFields` source map adjustment** (`private-to-property.ts`) - `convertPrivateFields` returns its sorted edits as `TextEdit[]`. `adjustSourceMap()` uses `SourceMapConsumer` to walk every mapping, adjusts generated columns based on cumulative edit shifts per line, and rebuilds with `SourceMapGenerator`. The post-processing loop in `index.ts` saves pre-mangle content + edits per JS file, then applies `adjustSourceMap` to the corresponding `.map`. Tests in `test/private-to-property.test.ts`. -### Not Yet Fixed +5. **`postProcessNLS` source map adjustment** (`nls-plugin.ts`, `index.ts`) — `postProcessNLS` now returns `{ code, edits }` where `edits` is a `TextEdit[]` tracking each replacement's byte offset. The bundle loop in `index.ts` chains `adjustSourceMap` calls: first for mangle edits, then for NLS edits, so both transforms are accurately reflected in the final `.map` file. Tests in `test/nls-sourcemap.test.ts`. -**`postProcessNLS` column drift** - Replaces NLS placeholders with short indices in bundled output without updating `.map` files. Shifts columns but never lines, so line-level debugging and crash reporting work correctly. Fixing would require tracking replacement offsets through regex matches and adjusting the source map, similar to `adjustSourceMap`. +6. **`adjustSourceMap` unmapped segment preservation** (`private-to-property.ts`) — Previously, `adjustSourceMap()` silently dropped mappings where `source === null`. These unmapped segments create essential "gaps" that prevent `originalPositionFor()` from wrongly interpolating between distant valid mappings on the same minified line. Now emits them as generated-only mappings. Also preserves `sourceRoot` from the input map. ### Key Technical Details @@ -241,6 +241,71 @@ Two categories of corruption: **Plugin interaction:** Both the NLS plugin and `fileContentMapperPlugin` register `onLoad({ filter: /\.ts$/ })`. In esbuild, the first `onLoad` to return non-`undefined` wins. The NLS plugin is `unshift`ed (runs first), so files with NLS calls skip `fileContentMapperPlugin`. This is safe in practice since `product.ts` (which has `BUILD->INSERT_PRODUCT_CONFIGURATION`) has no localize calls. +### Still Broken — Full Production Build (`npm run gulp vscode-min`) + +**Symptom:** Source maps are totally broken in the minified production build. E.g. a breakpoint at `src/vs/editor/browser/editorExtensions.ts` line 308 resolves to `src/vs/editor/common/cursor/cursorMoveCommands.ts` line 732 — a completely different file. This is **cross-file** mapping corruption, not just column drift. + +**Status of unit tests:** The fixes above pass in isolated unit tests (small 1–2 file bundles via `esbuild.build` with `minify: true`). The tests verify column drift ≤ 20 and correct line mapping for single-file bundles with NLS. **183 tests pass, 0 failing.** But the full production build bundles hundreds of files into huge minified outputs (e.g. `workbench.desktop.main.js` at ~15 MB) and the source maps break at that scale. + +**Suspected root causes (need investigation):** + +1. **`generateNLSSourceMap` per-column identity mappings may overwhelm esbuild's source-map composition.** The fix added one mapping per column from edit-end to end-of-line (or next edit). For a long TypeScript line with a `localize()` call near the beginning, this generates hundreds of identity mappings per line. Across hundreds of files, the inline source maps embedded in `onLoad` responses may be extremely large. esbuild must compose these with its own source maps during bundling — it may hit limits, silently drop mappings, or produce incorrect composed maps at this scale. **Mitigation to try:** Instead of per-column mappings, use sparser "checkpoint" mappings (e.g., every N characters) or rely only on boundary mappings and accept some column drift within the NLS-transformed region. The old boundary-only approach was wrong (collapsed all downstream columns), but per-column may be the other extreme. + +2. **`adjustSourceMap` may corrupt source indices in large minified bundles.** In a minified bundle, the entire output is on one or very few lines. `adjustSourceMap()` walks every mapping via `SourceMapConsumer.eachMapping()` and adjusts `generatedColumn` using `adjustColumn()`. But when thousands of mappings all share `generatedLine: 1` and there are hundreds of NLS edits on that same line, there may be sorting/ordering bugs: `eachMapping()` returns mappings in generated order by default, but `adjustColumn()` binary-searches through edits sorted by column. If edits cover regions that interleave with mappings from different source files, the cumulative shift calculation might produce wrong columns that then resolve to wrong source files. + +3. **Chained `adjustSourceMap` calls (mangle → NLS) may compound errors.** After the first `adjustSourceMap` for mangle edits, the source map's generated columns are updated. The second call for NLS edits uses `nlsEdits` which were computed against `preNLSCode` — but `preNLSCode` is the post-mangle JS, which is what the first `adjustSourceMap` maps from. This chaining _should_ be correct, but needs verification at scale with a real minified bundle. + +4. **The `source-map` v0.6.1 library may have precision issues with very large VLQ-encoded maps.** The bundled outputs have source maps with hundreds of thousands of mappings. The library is old (2017) and there may be numerical precision or sorting issues with very large maps. Consider testing with `source-map` v0.7+ or the Rust-based `@aspect-build/source-map`. + +5. **Alternative approach: skip per-column NLS plugin mappings, fix only `postProcessNLS`.** The NLS plugin `onLoad` replaces `"key"` with `"%%NLS:longPlaceholder%%"` — a length change that only affects columns on affected lines. The subsequent `postProcessNLS` then replaces the long placeholder with a short index. If the `adjustSourceMap` for `postProcessNLS` is correct, it should compensate for both expansions (plugin expansion + post-process contraction). We might not need per-column mappings in `generateNLSSourceMap` at all — just the boundary mapping. The column will drift in the intermediate representation but `adjustSourceMap` for NLS should fix it. **This hypothesis needs testing.** + +6. **Alternative approach: do NLS replacement purely in post-processing.** Skip the `onLoad` two-phase approach (placeholder insertion + post-processing replacement) entirely. Instead, run `postProcessNLS` as a single post-processing step that directly replaces `localize("key", "message")` → `localize(0, null)` in the bundled JS output, with proper source-map adjustment via `adjustSourceMap`. This avoids both the inline source map composition complexity and the two-step replacement. The downside is that post-processing must parse/regex-match real `localize()` calls (not easy placeholders), which is more fragile. + +**Summary of fixes applied vs status:** + +| Bug | Fix | Unit test | Production | +|-----|-----|-----------|------------| +| `generateNLSSourceMap` only had boundary mappings → columns collapsed | Added per-column identity mappings after each edit | Pass (drift: 0) | **Broken** — may overwhelm esbuild composition at scale | +| `postProcessNLS` didn't track edits for source map adjustment | Returns `{ code, edits }`, chained in `index.ts` | Pass | **Broken** — `adjustSourceMap` may corrupt source indices on huge single-line minified output | +| `adjustSourceMap` dropped unmapped segments | Preserves generated-only mappings + `sourceRoot` | Pass (no regressions) | **Broken** — same cross-file mapping issue | + +**Files involved:** +- `build/next/nls-plugin.ts` — `generateNLSSourceMap()` (per-column mappings), `postProcessNLS()` (returns edits), `replaceInOutput()` (regex replacement) +- `build/next/private-to-property.ts` — `adjustSourceMap()` (column adjustment) +- `build/next/index.ts` — bundle post-processing loop (lines ~899–975), chains adjustSourceMap calls +- `build/next/test/nls-sourcemap.test.ts` — unit tests (pass but don't cover production-scale bundles) + +**How to reproduce:** +```bash +npm run gulp vscode-min +# Open out-vscode-min/ in a debugger, set breakpoints in editor files +# Observe breakpoints resolve to wrong files +``` + +**How to debug further:** +```bash +# 1. Build with just --nls (no mangle) to isolate NLS from mangle issues +npx tsx build/next/index.ts bundle --nls --minify --target desktop --out out-debug + +# 2. Build with just --mangle-privates (no NLS) to isolate mangle issues +npx tsx build/next/index.ts bundle --mangle-privates --minify --target desktop --out out-debug + +# 3. Build with neither (baseline — does esbuild's own map work?) +npx tsx build/next/index.ts bundle --minify --target desktop --out out-debug + +# 4. Compare .map files across the three builds to find where mappings diverge + +# 5. Validate a specific mapping in the large bundle: +node -e " +const {SourceMapConsumer} = require('source-map'); +const fs = require('fs'); +const map = JSON.parse(fs.readFileSync('./out-debug/vs/workbench/workbench.desktop.main.js.map','utf8')); +const c = new SourceMapConsumer(map); +// Look up a known position and see which source file it resolves to +console.log(c.originalPositionFor({line: 1, column: XXXX})); +" +``` + --- ## Self-hosting Setup From 1151928683739034bebb6caa397c720549d9776b Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:34:04 +0100 Subject: [PATCH 025/448] =?UTF-8?q?Refactor=20theme=20color=20usage=20in?= =?UTF-8?q?=20AuxiliaryBarPart=20and=20PanelPart=20to=20utili=E2=80=A6=20(?= =?UTF-8?q?#298896)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor theme color usage in AuxiliaryBarPart and PanelPart to utilize sessionsSidebarBorder --- src/vs/sessions/browser/parts/auxiliaryBarPart.ts | 7 ++++--- src/vs/sessions/browser/parts/panelPart.ts | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/browser/parts/auxiliaryBarPart.ts b/src/vs/sessions/browser/parts/auxiliaryBarPart.ts index c7dbddc3e33..f97118b5419 100644 --- a/src/vs/sessions/browser/parts/auxiliaryBarPart.ts +++ b/src/vs/sessions/browser/parts/auxiliaryBarPart.ts @@ -12,8 +12,9 @@ import { INotificationService } from '../../../platform/notification/common/noti import { IStorageService } from '../../../platform/storage/common/storage.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { ActiveAuxiliaryContext, AuxiliaryBarFocusContext } from '../../../workbench/common/contextkeys.js'; -import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_TOP_ACTIVE_BORDER, ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_TOP_FOREGROUND, ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_DRAG_AND_DROP_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, SIDE_BAR_TITLE_BORDER, SIDE_BAR_FOREGROUND } from '../../../workbench/common/theme.js'; +import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_TOP_ACTIVE_BORDER, ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_TOP_FOREGROUND, ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_DRAG_AND_DROP_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_TITLE_BORDER, SIDE_BAR_FOREGROUND } from '../../../workbench/common/theme.js'; import { contrastBorder } from '../../../platform/theme/common/colorRegistry.js'; +import { sessionsSidebarBorder } from '../../common/theme.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../workbench/common/views.js'; import { IExtensionService } from '../../../workbench/services/extensions/common/extensions.js'; import { IWorkbenchLayoutService, Parts } from '../../../workbench/services/layout/browser/layoutService.js'; @@ -105,7 +106,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { { hasTitle: true, trailingSeparator: false, - borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0, + borderWidth: () => (this.getColor(sessionsSidebarBorder) || this.getColor(contrastBorder)) ? 1 : 0, }, AuxiliaryBarPart.activeViewSettingsKey, ActiveAuxiliaryContext.bindTo(contextKeyService), @@ -141,7 +142,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { // Store background and border as CSS variables for the card styling on .part container.style.setProperty('--part-background', this.getColor(SIDE_BAR_BACKGROUND) || ''); - container.style.setProperty('--part-border-color', this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder) || 'transparent'); + container.style.setProperty('--part-border-color', this.getColor(sessionsSidebarBorder) || this.getColor(contrastBorder) || 'transparent'); container.style.backgroundColor = 'transparent'; container.style.color = this.getColor(SIDE_BAR_FOREGROUND) || ''; diff --git a/src/vs/sessions/browser/parts/panelPart.ts b/src/vs/sessions/browser/parts/panelPart.ts index 867760bd112..2fccc865f15 100644 --- a/src/vs/sessions/browser/parts/panelPart.ts +++ b/src/vs/sessions/browser/parts/panelPart.ts @@ -14,8 +14,9 @@ import { IContextMenuService } from '../../../platform/contextview/browser/conte import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; -import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BORDER, PANEL_TITLE_BADGE_BACKGROUND, PANEL_TITLE_BADGE_FOREGROUND } from '../../../workbench/common/theme.js'; +import { PANEL_BACKGROUND, PANEL_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BORDER, PANEL_TITLE_BADGE_BACKGROUND, PANEL_TITLE_BADGE_FOREGROUND } from '../../../workbench/common/theme.js'; import { contrastBorder } from '../../../platform/theme/common/colorRegistry.js'; +import { sessionsSidebarBorder } from '../../common/theme.js'; import { INotificationService } from '../../../platform/notification/common/notification.js'; import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; import { assertReturnsDefined } from '../../../base/common/types.js'; @@ -129,7 +130,7 @@ export class PanelPart extends AbstractPaneCompositePart { // Store background and border as CSS variables for the card styling on .part container.style.setProperty('--part-background', this.getColor(PANEL_BACKGROUND) || ''); - container.style.setProperty('--part-border-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder) || 'transparent'); + container.style.setProperty('--part-border-color', this.getColor(sessionsSidebarBorder) || this.getColor(contrastBorder) || 'transparent'); container.style.backgroundColor = 'transparent'; // Clear inline borders - the card appearance uses CSS border-radius instead From 678444363d7d412c2d6e0d66726881a5153f476a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 3 Mar 2026 19:54:46 +1100 Subject: [PATCH 026/448] Hide debug slash command in Sessions (#298897) --- .../contrib/chat/browser/chatSlashCommands.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 1ffcf9f1cbb..9b379603530 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -33,6 +33,7 @@ import { } from './tools/languageModelToolsService.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; import { Target } from '../common/promptSyntax/service/promptsService.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; export class ChatSlashCommandsContribution extends Disposable { @@ -49,6 +50,7 @@ export class ChatSlashCommandsContribution extends Disposable { @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @IStorageService storageService: IStorageService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ @@ -102,16 +104,18 @@ export class ChatSlashCommandsContribution extends Disposable { }, async () => { await commandService.executeCommand(ManagePluginsAction.ID); })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'debug', - detail: nls.localize('debug', "Show Chat Debug View"), - sortText: 'z3_debug', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat], - }, async () => { - await commandService.executeCommand('github.copilot.debug.showChatLogView'); - })); + if (!this.environmentService.isSessionsWindow) { + this._store.add(slashCommandService.registerSlashCommand({ + command: 'debug', + detail: nls.localize('debug', "Show Chat Debug View"), + sortText: 'z3_debug', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat], + }, async () => { + await commandService.executeCommand('github.copilot.debug.showChatLogView'); + })); + } this._store.add(slashCommandService.registerSlashCommand({ command: 'agents', detail: nls.localize('agents', "Configure custom agents"), From 06bf068dbb8baee0540455c42ecb8667d0b79ae7 Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 3 Mar 2026 18:59:58 +0900 Subject: [PATCH 027/448] fix: support protocol handler for subapp on macOS (#298877) --- build/gulpfile.vscode.ts | 5 +++++ package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 0dfb90f264b..c22758027d1 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -519,6 +519,11 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d darwinMiniAppName: embedded.nameShort, darwinMiniAppBundleIdentifier: embedded.darwinBundleIdentifier, darwinMiniAppIcon: 'resources/darwin/sessions.icns', + darwinMiniAppBundleURLTypes: [{ + role: 'Viewer', + name: embedded.nameLong, + urlSchemes: [embedded.urlProtocol] + }], win32ProxyAppName: embedded.nameShort, win32ProxyIcon: 'resources/win32/sessions.ico', } : {}) diff --git a/package-lock.json b/package-lock.json index bdd35906813..00fc750db9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,7 @@ "@typescript/native-preview": "^7.0.0-dev.20260130", "@vscode/component-explorer": "^0.1.1-16", "@vscode/component-explorer-cli": "^0.1.1-12", - "@vscode/gulp-electron": "1.40.0", + "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", "@vscode/test-cli": "^0.0.6", @@ -3103,9 +3103,9 @@ } }, "node_modules/@vscode/gulp-electron": { - "version": "1.40.0", - "resolved": "git+ssh://git@github.com/microsoft/vscode-gulp-electron.git#580228be384d7942b39aca6466b5a5050e4744a2", - "integrity": "sha512-EfQqw/kFmqiUgBv7WXx3wIrtz9cujAgX2uKQzTq517MbVjlpg7BIAjNC4Iq/wVB4Vgpl/ZGB7/XuSN7LsaLdlA==", + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@vscode/gulp-electron/-/gulp-electron-1.40.1.tgz", + "integrity": "sha512-ERN3Mly+bxicuhSGrF4ksSwr7UNCBcYOcVVClivTzkkEL4gy477V4H8YAURak/W1VPmdmDWn+VZknptRySDWew==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index faf767b7e34..8e5dc8cdf8f 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "@typescript/native-preview": "^7.0.0-dev.20260130", "@vscode/component-explorer": "^0.1.1-16", "@vscode/component-explorer-cli": "^0.1.1-12", - "@vscode/gulp-electron": "1.40.0", + "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", "@vscode/test-cli": "^0.0.6", From d31d0310dfcb0feabed0a140574f4d564ea950e7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 3 Mar 2026 11:16:31 +0100 Subject: [PATCH 028/448] show logs by default (#298916) --- .../contrib/logs/browser/logs.contribution.ts | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/vs/sessions/contrib/logs/browser/logs.contribution.ts b/src/vs/sessions/contrib/logs/browser/logs.contribution.ts index 14b41083f7d..8a562ce4977 100644 --- a/src/vs/sessions/contrib/logs/browser/logs.contribution.ts +++ b/src/vs/sessions/contrib/logs/browser/logs.contribution.ts @@ -5,11 +5,8 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize, localize2 } from '../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { SessionsCategories } from '../../../common/categories.js'; -import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js'; @@ -17,12 +14,9 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, WindowVisibility } from '../../../../workbench/common/views.js'; import { OutputViewPane } from '../../../../workbench/contrib/output/browser/outputView.js'; import { OUTPUT_VIEW_ID } from '../../../../workbench/services/output/common/output.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; const SESSIONS_LOGS_CONTAINER_ID = 'workbench.sessions.panel.logsContainer'; -const CONTEXT_SESSIONS_SHOW_LOGS = new RawContextKey('sessionsShowLogs', false); - const logsViewIcon = registerIcon('sessions-logs-view-icon', Codicon.output, localize('sessionsLogsViewIcon', 'View icon of the logs view in the sessions window.')); class RegisterLogsViewContainerContribution implements IWorkbenchContribution { @@ -32,7 +26,6 @@ class RegisterLogsViewContainerContribution implements IWorkbenchContribution { constructor( @IContextKeyService contextKeyService: IContextKeyService, ) { - CONTEXT_SESSIONS_SHOW_LOGS.bindTo(contextKeyService).set(true); const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -66,28 +59,9 @@ class RegisterLogsViewContainerContribution implements IWorkbenchContribution { ctorDescriptor: new SyncDescriptor(OutputViewPane), canToggleVisibility: true, canMoveView: false, - when: CONTEXT_SESSIONS_SHOW_LOGS, windowVisibility: WindowVisibility.Sessions, }], logsViewContainer); } } registerWorkbenchContribution2(RegisterLogsViewContainerContribution.ID, RegisterLogsViewContainerContribution, WorkbenchPhase.BlockStartup); - -// Command: Sessions: Show Logs -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'workbench.sessions.action.showLogs', - title: localize2('sessionsShowLogs', "Show Logs"), - category: SessionsCategories.Sessions, - f1: true, - }); - } - async run(accessor: ServicesAccessor): Promise { - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - CONTEXT_SESSIONS_SHOW_LOGS.bindTo(contextKeyService).set(true); - await viewsService.openView(OUTPUT_VIEW_ID, true); - } -}); From 5c7e861e70b6eea22b707d6a0404bd38f66d8bcd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 11:40:21 +0100 Subject: [PATCH 029/448] sessions - enable terminal auto approve (#298917) --- .../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 d533049d110..aad4b934137 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -14,6 +14,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'chat.viewSessions.enabled': false, 'chat.implicitContext.suggestedContext': false, 'chat.implicitContext.enabled': { 'panel': 'never' }, + 'chat.tools.terminal.enableAutoApprove': true, 'breadcrumbs.enabled': false, From a6e427ed1f4afb60a276a5b32f5e50bf1c953ad6 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 10:46:54 +0000 Subject: [PATCH 030/448] refactor: update box-shadow styles across various components to use new shadow variables - Replaced hardcoded box-shadow values with new CSS variables for consistency and improved theming. - Updated styles in postEditWidget, findOptionsWidget, findWidget, floatingMenu, hover, parameterHints, peekViewWidget, renameWidget, stickyScroll, suggest, actionWidget, hover, quickInput, agentFeedback components, and more. - Introduced new shadow variables: --vscode-shadow-sm, --vscode-shadow-md, --vscode-shadow-lg, --vscode-shadow-xl, and --vscode-shadow-hover for better control over shadow effects. Co-authored-by: Copilot --- .../lib/stylelint/vscode-known-variables.json | 8 + extensions/theme-2026/themes/styles.css | 199 ------------------ src/vs/base/browser/ui/dialog/dialog.css | 1 + src/vs/base/browser/ui/dropdown/dropdown.css | 1 + src/vs/base/browser/ui/menu/menu.ts | 3 +- .../browser/ui/selectBox/selectBoxCustom.css | 2 +- .../browser/viewParts/minimap/minimap.css | 2 +- .../browser/postEditWidget.css | 2 +- .../find/browser/findOptionsWidget.css | 2 +- .../contrib/find/browser/findWidget.css | 2 +- .../floatingMenu/browser/floatingMenu.css | 2 +- src/vs/editor/contrib/hover/browser/hover.css | 1 + .../parameterHints/browser/parameterHints.css | 1 + .../peekView/browser/media/peekViewWidget.css | 4 + .../contrib/rename/browser/renameWidget.css | 1 + .../contrib/rename/browser/renameWidget.ts | 5 +- .../stickyScroll/browser/stickyScroll.css | 2 +- .../contrib/suggest/browser/media/suggest.css | 1 + .../actionWidget/browser/actionWidget.css | 2 +- src/vs/platform/hover/browser/hover.css | 2 +- .../quickinput/browser/media/quickInput.css | 1 + .../browser/quickInputController.ts | 3 +- .../media/agentFeedbackEditorInput.css | 2 +- .../media/agentFeedbackEditorOverlay.css | 2 +- .../media/agentFeedbackEditorWidget.css | 2 +- src/vs/workbench/browser/media/style.css | 12 ++ .../activitybar/media/activitybarpart.css | 9 + .../browser/parts/editor/breadcrumbsPicker.ts | 4 +- .../parts/editor/media/editorgroupview.css | 46 ++++ .../parts/editor/media/modalEditorPart.css | 2 +- .../editor/media/multieditortabscontrol.css | 8 + .../media/notificationsCenter.css | 1 + .../media/notificationsToasts.css | 2 +- .../notifications/notificationsCenter.ts | 3 - .../notifications/notificationsToasts.ts | 4 - .../parts/titlebar/media/titlebarpart.css | 2 + .../electron-browser/media/browser.css | 2 +- .../media/chatEditingEditorOverlay.css | 8 +- .../media/chatEditingExplanationWidget.css | 2 +- .../media/chatEditorController.css | 2 +- .../browser/accessibility/accessibility.css | 2 +- .../browser/dictation/editorDictation.css | 2 +- .../browser/find/simpleFindWidget.css | 2 +- .../contrib/debug/browser/debugToolBar.ts | 5 +- .../debug/browser/media/debugHover.css | 1 + .../debug/browser/media/debugToolBar.css | 1 + .../browser/media/extensionsViewlet.css | 5 + .../inlineChat/browser/media/inlineChat.css | 2 +- .../media/inlineChatEditorAffordance.css | 2 +- .../browser/media/inlineChatOverlayWidget.css | 4 +- .../find/notebookFindReplaceWidget.css | 2 +- .../notebook/browser/media/notebook.css | 2 + .../preferences/browser/keybindingWidgets.ts | 3 +- .../preferences/browser/media/keybindings.css | 1 + .../browser/media/settingsEditor2.css | 1 + .../contrib/scm/browser/media/scm.css | 1 + 56 files changed, 145 insertions(+), 251 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 9e2eea3b858..2cdd3b0077f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -934,6 +934,14 @@ "--notebook-editor-font-weight", "--outline-element-color", "--separator-border", + "--vscode-shadow-active-tab", + "--vscode-shadow-depth-x", + "--vscode-shadow-depth-y", + "--vscode-shadow-hover", + "--vscode-shadow-lg", + "--vscode-shadow-md", + "--vscode-shadow-sm", + "--vscode-shadow-xl", "--status-border-top-color", "--tab-border-bottom-color", "--tab-border-top-color", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 87790e5e7eb..9e5d90e3a2e 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -6,101 +6,12 @@ :root { --radius-sm: 4px; --radius-lg: 8px; - - --shadow-sm: 0 0 4px rgba(0, 0, 0, 0.08); - --shadow-md: 0 0 6px rgba(0, 0, 0, 0.08); - --shadow-lg: 0 0 12px rgba(0, 0, 0, 0.14); - --shadow-xl: 0 0 20px rgba(0, 0, 0, 0.15); - --shadow-hover: 0 0 8px rgba(0, 0, 0, 0.12); - --shadow-sm-strong: 0 0 4px rgba(0, 0, 0, 0.18); - --shadow-active-tab: 0 8px 12px rgba(0, 0, 0, 0.02); - - /* Panel depth shadows cast onto the editor surface */ - --shadow-depth-x: 5px 0 10px -4px rgba(0, 0, 0, 0.05); - --shadow-depth-y: 0 5px 10px -4px rgba(0, 0, 0, 0.04); -} - -/* Dark theme: add brightness reduction for contrast-safe luminosity blending over bright backgrounds */ -.monaco-workbench.vs-dark { - --shadow-depth-x: 5px 0 12px -4px rgba(0, 0, 0, 0.14); - --shadow-depth-y: 0 5px 12px -4px rgba(0, 0, 0, 0.10); -} - -/* Stealth Shadows - panels appear to float above the editor. - * Instead of z-index on panels (which breaks webviews, iframes, sashes), - * the editor draws its own "received shadow" via a ::after pseudo-element. - * The surrounding panels stay at default stacking — no z-index needed. */ - -/* Activity Bar - only needs shadow when sidebar is hidden */ -.monaco-workbench.nosidebar .part.activitybar { - box-shadow: var(--shadow-md); -} - -.monaco-workbench.activitybar-right .part.activitybar { - box-shadow: var(--shadow-md); } .monaco-pane-view .split-view-view:first-of-type > .pane > .pane-header { border-top: 1px solid var(--vscode-sideBarSectionHeader-border) !important; } -/* Editor - the ::after pseudo-element draws inset shadows on each edge, - * creating the illusion that sidebar, panel, and auxiliarybar float above it. */ -.monaco-workbench.vs .part.editor { - position: relative; -} - -.monaco-workbench.vs .part.editor::after { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - z-index: 10; - box-shadow: - inset var(--shadow-depth-x), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), - inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); -} - -/* When sidebar is on the right, flip the stronger shadow to the right edge */ -.monaco-workbench.sidebar-right.vs .part.editor::after { - box-shadow: - inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05), - inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); -} - -/* Panel positions: strengthen the shadow on whichever edge faces the panel */ -.monaco-workbench.panel-position-left.vs .part.editor::after { - box-shadow: - inset var(--shadow-depth-x), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04); -} - -.monaco-workbench.panel-position-right.vs .part.editor::after { - box-shadow: - inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05); -} - -.monaco-workbench.panel-position-top.vs .part.editor::after { - box-shadow: - inset var(--shadow-depth-x), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), - inset 0 var(--shadow-depth-y); -} -.monaco-workbench.vs .part.editor > .content .editor-group-container > .title { - box-shadow: none; -} - -.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { - box-shadow: inset var(--shadow-active-tab); -} - -.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { - box-shadow: var(--shadow-sm); -} - /* Tab border bottom - make transparent */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { --tabs-border-bottom-color: transparent !important; @@ -110,15 +21,7 @@ --tab-border-bottom-color: transparent !important; } -/* Title Bar */ -.monaco-workbench.vs .part.titlebar { - box-shadow: var(--shadow-md); -} - /* Quick Input (Command Palette) */ -.monaco-workbench .quick-input-widget { - box-shadow: var(--shadow-xl) !important; -} .monaco-workbench.vs-dark .quick-input-widget { border: 1px solid var(--vscode-menu-border) !important; @@ -170,7 +73,6 @@ } .monaco-workbench .quick-input-widget .monaco-inputbox { - box-shadow: none !important; background: transparent !important; } @@ -180,10 +82,6 @@ /* Chat Widget */ -.monaco-workbench.vs .interactive-session .chat-input-container { - box-shadow: inset var(--shadow-sm); -} - .monaco-workbench .part.panel .interactive-session, .monaco-workbench .part.auxiliarybar .interactive-session { position: relative; @@ -207,73 +105,33 @@ /* Context Menus */ .monaco-workbench .context-view .monaco-menu { - box-shadow: var(--shadow-lg); border: none; } -.monaco-workbench .monaco-select-box-dropdown-container { - box-shadow: var(--shadow-lg); -} - -.monaco-workbench .monaco-menu-container > .monaco-scrollable-element { - box-shadow: var(--shadow-lg) !important; -} - .monaco-workbench .action-widget .action-widget-action-bar { background: transparent; } /* Suggest Widget */ -.monaco-workbench .monaco-editor .suggest-widget { - box-shadow: var(--shadow-lg); -} .monaco-workbench.vs-dark .monaco-editor .suggest-widget { border: 1px solid var(--vscode-editorWidget-border); } -/* Find Widget */ -.monaco-workbench .monaco-editor .find-widget { - box-shadow: var(--shadow-lg); -} - -.monaco-workbench .inline-chat-gutter-menu { - box-shadow: var(--shadow-lg); -} - /* Dialog */ .monaco-workbench .monaco-dialog-box { border: 1px solid var(--vscode-dialog-border); - box-shadow: var(--shadow-xl); } /* Peek View */ -.monaco-workbench .monaco-editor .peekview-widget { - box-shadow: var(--shadow-hover); -} .monaco-workbench .monaco-editor .peekview-widget .head, .monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; } -.monaco-editor .monaco-hover { - box-shadow: var(--shadow-sm-strong); -} - -.monaco-workbench .monaco-hover.workbench-hover, -.monaco-hover.workbench-hover { - box-shadow: var(--shadow-sm-strong); -} - .monaco-workbench .defineKeybindingWidget { border: 1px solid var(--vscode-editorWidget-border); - box-shadow: var(--shadow-lg) !important; -} - -.monaco-workbench .chat-editor-overlay-widget, -.monaco-workbench .chat-diff-change-content-widget { - box-shadow: var(--shadow-md); } .monaco-workbench.vs-dark .chat-editor-overlay-widget, @@ -282,9 +140,6 @@ } /* Settings */ -.monaco-workbench .settings-editor .settings-toc-container { - box-shadow: var(--shadow-sm); -} .monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { border-radius: var(--radius-sm); @@ -293,27 +148,11 @@ border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } -/* Welcome Tiles */ -.monaco-workbench .part.editor .welcomePageContainer .tile { - box-shadow: var(--shadow-md); - border: none; - border-radius: var(--radius-lg); -} - -.monaco-workbench .part.editor .welcomePageContainer .tile:hover { - box-shadow: var(--shadow-hover); -} - /* Extensions */ .monaco-workbench .extensions-list .extension-list-item { - box-shadow: var(--shadow-sm); border: none; } -.monaco-workbench .extensions-list .extension-list-item:hover { - box-shadow: var(--shadow-md); -} - /* Breadcrumbs */ .monaco-workbench.vs .breadcrumbs-control { @@ -358,23 +197,7 @@ box-shadow: none; } -/* Dropdowns */ -.monaco-workbench .monaco-dropdown .dropdown-menu { - box-shadow: var(--shadow-lg); -} - -/* SCM */ -.monaco-workbench .scm-view .scm-provider { - box-shadow: var(--shadow-sm); -} - -/* Debug Toolbar */ -.monaco-workbench .debug-toolbar { - box-shadow: var(--shadow-lg); -} - .monaco-workbench .debug-hover-widget { - box-shadow: var(--shadow-lg); color: var(--vscode-editor-foreground) !important; } @@ -382,16 +205,6 @@ background-color: var(--vscode-list-hoverBackground); } -/* Action Widget */ -.monaco-workbench .action-widget { - box-shadow: var(--shadow-lg) !important; -} - -/* Parameter Hints */ -.monaco-workbench .monaco-editor .parameter-hints-widget { - box-shadow: var(--shadow-lg); -} - /* Minimap */ .monaco-workbench .monaco-editor .minimap canvas { @@ -400,7 +213,6 @@ .monaco-workbench.vs-dark .monaco-editor .minimap, .monaco-workbench .monaco-editor .minimap-shadow-visible { - box-shadow: var(--shadow-md); opacity: 0.85; background-color: var(--vscode-editor-background); left: 0; @@ -417,7 +229,6 @@ /* Sticky Scroll */ .monaco-workbench .monaco-editor .sticky-widget { - box-shadow: var(--shadow-md) !important; border-bottom: var(--vscode-editorWidget-border) !important; background: transparent !important; } @@ -447,31 +258,21 @@ } .monaco-editor .rename-box.preview { - box-shadow: var(--shadow-hover) !important; border: 1px solid var(--vscode-editorWidget-border); } /* Notebook */ -.monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { - box-shadow: inset var(--shadow-sm); -} - .notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: var(--vscode-editorWidget-background) !important; - box-shadow: var(--shadow-sm); } /* Inline Chat */ .monaco-workbench .monaco-editor .inline-chat { - box-shadow: var(--shadow-lg); border: none; } /* Command Center */ -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { - box-shadow: inset var(--shadow-sm) !important; -} .monaco-workbench .part.titlebar .command-center .agent-status-pill { border-color: var(--vscode-input-border); diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 844b50bec38..0dd289c9f9e 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -31,6 +31,7 @@ padding: 10px; transform: translate3d(0px, 0px, 0px); border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-xl); } .monaco-dialog-box.align-vertical { diff --git a/src/vs/base/browser/ui/dropdown/dropdown.css b/src/vs/base/browser/ui/dropdown/dropdown.css index 0e2d6bdd9f5..7c70f376b14 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.css +++ b/src/vs/base/browser/ui/dropdown/dropdown.css @@ -22,6 +22,7 @@ .monaco-dropdown .dropdown-menu { border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .monaco-dropdown-with-primary { diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index bd6298d0485..ec712c7cf87 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -322,13 +322,11 @@ export class Menu extends ActionBar { const bgColor = style.backgroundColor ?? ''; const border = style.borderColor ? `1px solid ${style.borderColor}` : ''; const borderRadius = 'var(--vscode-cornerRadius-large)'; - const shadow = style.shadowColor ? `0 2px 8px ${style.shadowColor}` : ''; scrollElement.style.outline = border; scrollElement.style.borderRadius = borderRadius; scrollElement.style.color = fgColor; scrollElement.style.backgroundColor = bgColor; - scrollElement.style.boxShadow = shadow; } override getContainer(): HTMLElement { @@ -1241,6 +1239,7 @@ ${formatRule(Codicon.menuSubmenu)} border: none; animation: fadeIn 0.083s linear; -webkit-app-region: no-drag; + box-shadow: var(--vscode-shadow-lg); } .context-view.monaco-menu-container :focus, diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index 62f42240629..769ba3a08aa 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -7,7 +7,7 @@ display: none; box-sizing: border-box; border-radius: var(--vscode-cornerRadius-large); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); } .monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index 35bb6e3b717..73814f23979 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -25,7 +25,7 @@ background: var(--vscode-minimapSlider-activeBackground); } .monaco-editor .minimap-shadow-visible { - box-shadow: var(--vscode-scrollbar-shadow) -6px 0 6px -6px inset; + box-shadow: var(--vscode-shadow-md); } .monaco-editor .minimap-shadow-hidden { position: absolute; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css index 496b989268e..cce094ae6dc 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .post-edit-widget { - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 1px solid var(--vscode-widget-border, transparent); border-radius: 4px; color: var(--vscode-button-foreground); diff --git a/src/vs/editor/contrib/find/browser/findOptionsWidget.css b/src/vs/editor/contrib/find/browser/findOptionsWidget.css index 2188fa9fa9d..ac9f95c4e34 100644 --- a/src/vs/editor/contrib/find/browser/findOptionsWidget.css +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.css @@ -6,6 +6,6 @@ .monaco-editor .findOptionsWidget { background-color: var(--vscode-editorWidget-background); color: var(--vscode-editorWidget-foreground); - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 2px solid var(--vscode-contrastBorder); } diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index cbe6028775c..42e63375f2c 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -15,7 +15,7 @@ margin-top: 4px; box-sizing: border-box; transform: translateY(calc(-100% - 10px)); /* shadow (10px) */ - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); color: var(--vscode-editorWidget-foreground); border: 1px solid var(--vscode-widget-border); border-radius: var(--vscode-cornerRadius-large); diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 422e073e5e7..6f544cb1212 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -14,7 +14,7 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); overflow: hidden; .actions-container { diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index b33ea5e76bc..5817cd9a3c5 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -23,6 +23,7 @@ border-radius: var(--vscode-cornerRadius-large); color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); + box-shadow: var(--vscode-shadow-hover); } .monaco-editor .monaco-hover a { diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index bf54d22d60e..d613456e162 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -14,6 +14,7 @@ background-color: var(--vscode-editorHoverWidget-background); border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .hc-black .monaco-editor .parameter-hints-widget, diff --git a/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css b/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css index f49973bfea3..eb777d9ac7f 100644 --- a/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css +++ b/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-editor .peekview-widget { + box-shadow: var(--vscode-shadow-hover); +} + .monaco-editor .peekview-widget .head { box-sizing: border-box; display: flex; diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.css b/src/vs/editor/contrib/rename/browser/renameWidget.css index acd375f2afb..b68e37efee9 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.css +++ b/src/vs/editor/contrib/rename/browser/renameWidget.css @@ -11,6 +11,7 @@ .monaco-editor .rename-box.preview { padding: 4px 4px 0 4px; + box-shadow: var(--vscode-shadow-hover); } .monaco-editor .rename-box .rename-input-with-button { diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index b7e32811605..efd2fc16232 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -41,8 +41,7 @@ import { inputForeground, quickInputListFocusBackground, quickInputListFocusForeground, - widgetBorder, - widgetShadow + widgetBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { HoverStyle } from '../../../../base/browser/ui/hover/hover.js'; @@ -243,10 +242,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable return; } - const widgetShadowColor = theme.getColor(widgetShadow); const widgetBorderColor = theme.getColor(widgetBorder); this._domNode.style.backgroundColor = String(theme.getColor(editorWidgetBackground) ?? ''); - this._domNode.style.boxShadow = widgetShadowColor ? ` 0 0 8px 2px ${widgetShadowColor}` : ''; this._domNode.style.border = widgetBorderColor ? `1px solid ${widgetBorderColor}` : ''; this._domNode.style.color = String(theme.getColor(inputForeground) ?? ''); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index ecc59245dec..1cd84683e70 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -7,7 +7,7 @@ overflow: hidden; border-bottom: 1px solid var(--vscode-editorStickyScroll-border); width: 100%; - box-shadow: var(--vscode-editorStickyScroll-shadow) 0 4px 2px -2px; + box-shadow: var(--vscode-shadow-md); z-index: 4; right: initial !important; margin-left: '0px'; diff --git a/src/vs/editor/contrib/suggest/browser/media/suggest.css b/src/vs/editor/contrib/suggest/browser/media/suggest.css index 2d9fd8b1f7c..70f27a8fa86 100644 --- a/src/vs/editor/contrib/suggest/browser/media/suggest.css +++ b/src/vs/editor/contrib/suggest/browser/media/suggest.css @@ -11,6 +11,7 @@ display: flex; flex-direction: column; border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .monaco-editor .suggest-widget.message { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 96fc6cbf506..f6d3dc6133f 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -15,7 +15,7 @@ background-color: var(--vscode-menu-background); color: var(--vscode-menu-foreground); padding: 4px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); } .context-view-block { diff --git a/src/vs/platform/hover/browser/hover.css b/src/vs/platform/hover/browser/hover.css index 597738d3069..af76fdc3702 100644 --- a/src/vs/platform/hover/browser/hover.css +++ b/src/vs/platform/hover/browser/hover.css @@ -17,7 +17,7 @@ border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: 5px; color: var(--vscode-editorHoverWidget-foreground); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-hover); } .monaco-hover.workbench-hover .monaco-action-bar .action-item .codicon { diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index bfad8086272..01831b024b9 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -10,6 +10,7 @@ left: 50%; -webkit-app-region: no-drag; border-radius: var(--vscode-cornerRadius-xLarge); + box-shadow: var(--vscode-shadow-xl); } .quick-input-titlebar { diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 8e5283ef9ad..162d90de81b 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -922,13 +922,12 @@ export class QuickInputController extends Disposable { private updateStyles() { if (this.ui) { const { - quickInputTitleBackground, quickInputBackground, quickInputForeground, widgetBorder, widgetShadow, + quickInputTitleBackground, quickInputBackground, quickInputForeground, widgetBorder, } = this.styles.widget; this.ui.titleBar.style.backgroundColor = quickInputTitleBackground ?? ''; this.ui.container.style.backgroundColor = quickInputBackground ?? ''; this.ui.container.style.color = quickInputForeground ?? ''; this.ui.container.style.border = widgetBorder ? `1px solid ${widgetBorder}` : ''; - this.ui.container.style.boxShadow = widgetShadow ? `0 0 8px 2px ${widgetShadow}` : ''; this.ui.list.style(this.styles.list); this.ui.tree.tree.style(this.styles.list); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css index f2361debfc6..b467ff7f7aa 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css @@ -8,7 +8,7 @@ z-index: 10000; background-color: var(--vscode-panel-background); border: 1px solid var(--vscode-agentFeedbackInputWidget-border, var(--vscode-input-border, var(--vscode-widget-border))); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border-radius: 8px; padding: 4px; display: flex; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css index 1acdbe228ce..0de61925c9e 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css @@ -14,7 +14,7 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); overflow: hidden; } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css index 3ca674d2cf4..34725117ebe 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -11,7 +11,7 @@ background-color: var(--vscode-editorWidget-background); border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); border-radius: 8px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); font-size: 12px; line-height: 1.4; opacity: 0; diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 7537f9316e0..0d6a2da153b 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -53,6 +53,18 @@ body { z-index: 1; overflow: hidden; color: var(--vscode-foreground); + + /* Elevation shadows */ + --vscode-shadow-sm: 0 0 4px rgba(0, 0, 0, 0.08); + --vscode-shadow-md: 0 0 6px rgba(0, 0, 0, 0.08); + --vscode-shadow-lg: 0 0 12px rgba(0, 0, 0, 0.14); + --vscode-shadow-xl: 0 0 20px rgba(0, 0, 0, 0.15); + --vscode-shadow-hover: 0 0 8px rgba(0, 0, 0, 0.12); + --vscode-shadow-active-tab: 0 8px 12px rgba(0, 0, 0, 0.02); + + /* Panel depth shadows cast onto the editor surface */ + --vscode-shadow-depth-x: 5px 0 10px -4px rgba(0, 0, 0, 0.05); + --vscode-shadow-depth-y: 0 5px 10px -4px rgba(0, 0, 0, 0.04); } .monaco-workbench.web { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index d903883d10a..568a7212980 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -8,6 +8,15 @@ height: 100%; } +/* Activity Bar - shadow when sidebar is hidden or on the right */ +.monaco-workbench.nosidebar .part.activitybar { + box-shadow: var(--vscode-shadow-md); +} + +.monaco-workbench.activitybar-right .part.activitybar { + box-shadow: var(--vscode-shadow-md); +} + .monaco-workbench .activitybar.bordered::before { content: ''; float: left; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 198f156d196..db602f7d37a 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -17,7 +17,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { FileKind, FileSystemProviderCapabilities, IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchDataTree, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; -import { breadcrumbsPickerBackground, widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; +import { breadcrumbsPickerBackground, widgetBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from '../../labels.js'; import { BreadcrumbsConfig } from './breadcrumbs.js'; @@ -96,7 +96,7 @@ export abstract class BreadcrumbsPicker { this._treeContainer.style.background = color ? color.toString() : ''; this._treeContainer.style.paddingTop = '2px'; this._treeContainer.style.borderRadius = '3px'; - this._treeContainer.style.boxShadow = `0 0 8px 2px ${this._themeService.getColorTheme().getColor(widgetShadow)}`; + this._treeContainer.style.boxShadow = 'var(--vscode-shadow-lg)'; this._treeContainer.style.border = `1px solid ${this._themeService.getColorTheme().getColor(widgetBorder)}`; this._domNode.appendChild(this._treeContainer); diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 8eff5df9389..9f63f8390de 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -5,6 +5,52 @@ /* Container */ +/* Editor depth shadows - the ::after pseudo-element draws inset shadows on each edge, + * creating the illusion that sidebar, panel, and auxiliarybar float above it. */ +.monaco-workbench.vs .part.editor { + position: relative; +} + +.monaco-workbench.vs .part.editor::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 10; + box-shadow: + inset var(--vscode-shadow-depth-x), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), + inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); +} + +/* When sidebar is on the right, flip the stronger shadow to the right edge */ +.monaco-workbench.sidebar-right.vs .part.editor::after { + box-shadow: + inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05), + inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); +} + +/* Panel positions: strengthen the shadow on whichever edge faces the panel */ +.monaco-workbench.panel-position-left.vs .part.editor::after { + box-shadow: + inset var(--vscode-shadow-depth-x), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04); +} + +.monaco-workbench.panel-position-right.vs .part.editor::after { + box-shadow: + inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05); +} + +.monaco-workbench.panel-position-top.vs .part.editor::after { + box-shadow: + inset var(--vscode-shadow-depth-x), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), + inset 0 var(--vscode-shadow-depth-y); +} + .monaco-workbench .part.editor > .content .editor-group-container { height: 100%; } diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index 6be74735630..6f5271cc84c 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -20,7 +20,7 @@ background: rgba(0, 0, 0, 0.3); .modal-editor-shadow { - box-shadow: 0 4px 32px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2)); + box-shadow: var(--vscode-shadow-xl); border-radius: 8px; overflow: hidden; } diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 924d9b33607..4f9477d0865 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -144,6 +144,14 @@ box-shadow: none; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + box-shadow: inset var(--vscode-shadow-active-tab); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { + box-shadow: var(--vscode-shadow-sm); +} + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ } diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index 9f11489cc35..ee7e8a6a64a 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css @@ -12,6 +12,7 @@ overflow: hidden; border: 1px solid var(--vscode-editorWidget-border); border-radius: var(--vscode-cornerRadius-small); + box-shadow: var(--vscode-shadow-lg); } .monaco-workbench.nostatusbar > .notifications-center { diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index af90372abdc..3dca2ce638a 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -10,7 +10,7 @@ bottom: 25px; /* 22px status bar height + 3px */ display: none; overflow: hidden; - box-shadow: 0 0 12px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border-radius: var(--vscode-cornerRadius-small); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 76d5680b81f..82f6975b6d8 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -15,7 +15,6 @@ import { INotificationsCenterController, NotificationActionRunner } from './noti import { NotificationsList } from './notificationsList.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { $, Dimension, isAncestorOfActiveElement } from '../../../../base/browser/dom.js'; -import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { localize } from '../../../../nls.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; @@ -390,8 +389,6 @@ export class NotificationsCenter extends Themable implements INotificationsCente override updateStyles(): void { if (this.notificationsCenterContainer && this.notificationsCenterHeader) { - const widgetShadowColor = this.getColor(widgetShadow); - this.notificationsCenterContainer.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : ''; const borderColor = this.getColor(NOTIFICATIONS_CENTER_BORDER); this.notificationsCenterContainer.style.border = borderColor ? `1px solid ${borderColor}` : ''; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 4aa6355a41b..bd77caab75a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -14,7 +14,6 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { NOTIFICATIONS_TOAST_BORDER, NOTIFICATIONS_BACKGROUND } from '../../../common/theme.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; -import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { INotificationsToastController } from './notificationsCommands.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -553,9 +552,6 @@ export class NotificationsToasts extends Themable implements INotificationsToast const backgroundColor = this.getColor(NOTIFICATIONS_BACKGROUND); toast.style.background = backgroundColor ? backgroundColor : ''; - const widgetShadowColor = this.getColor(widgetShadow); - toast.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : ''; - const borderColor = this.getColor(NOTIFICATIONS_TOAST_BORDER); toast.style.border = borderColor ? `1px solid ${borderColor}` : ''; }); diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index cf0977721d5..a2c1090f9d1 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -7,6 +7,7 @@ .monaco-workbench .part.titlebar { display: flex; flex-direction: row; + box-shadow: var(--vscode-shadow-md); } .monaco-workbench.mac .part.titlebar { @@ -176,6 +177,7 @@ height: 22px; width: 38vw; max-width: 600px; + box-shadow: inset var(--vscode-shadow-sm); } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center .action-item.command-center-quick-pick { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css index db45b5b9b48..8ad449ee69d 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css +++ b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css @@ -193,7 +193,7 @@ border-radius: 4px; border: 1px solid var(--vscode-editorWidget-border); background-color: var(--vscode-editor-background); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); max-width: 80%; text-align: center; display: none; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css index a29b8276312..0c26b35ff49 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css @@ -14,19 +14,19 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); overflow: hidden; } @keyframes pulse { 0% { - box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); } 50% { - box-shadow: 0 2px 8px 4px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); } 100% { - box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css index 7bfca698ff8..71dbdf0a6d7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css @@ -11,7 +11,7 @@ background-color: var(--vscode-editorWidget-background); border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); border-radius: 8px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); font-size: 12px; line-height: 1.4; opacity: 0; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css index 402bd4b6b0b..e24c87525bf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css @@ -7,7 +7,7 @@ opacity: 0; transition: opacity 0.2s ease-in-out; display: flex; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); border-radius: 6px; overflow: hidden; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css index cda82e4a998..6767728af16 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css @@ -7,7 +7,7 @@ position: absolute; background-color: var(--vscode-editorWidget-background); color: var(--vscode-editorWidget-foreground); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 2px solid var(--vscode-focusBorder); border-radius: 6px; margin-top: -1px; diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css index 0a8d982123f..b002f8c148d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css @@ -9,7 +9,7 @@ border-radius: 8px; display: flex; align-items: center; - box-shadow: 0 4px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); z-index: 1000; min-height: var(--vscode-editor-dictation-widget-height); line-height: var(--vscode-editor-dictation-widget-height); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index 5f1597b9de2..6fb4864cf87 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -30,7 +30,7 @@ transition: top 200ms linear; background-color: var(--vscode-editorWidget-background) !important; color: var(--vscode-editorWidget-foreground); - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 1px solid var(--vscode-widget-border); border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 2e54d39fb8e..a801205ecc7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -30,7 +30,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; +import { widgetBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { getTitleBarStyle, TitlebarStyle } from '../../../../platform/window/common/window.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; @@ -259,9 +259,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { if (this.$el) { this.$el.style.backgroundColor = this.getColor(debugToolBarBackground) || ''; - const widgetShadowColor = this.getColor(widgetShadow); - this.$el.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : ''; - const contrastBorderColor = this.getColor(widgetBorder); const borderColor = this.getColor(debugToolBarBorder); diff --git a/src/vs/workbench/contrib/debug/browser/media/debugHover.css b/src/vs/workbench/contrib/debug/browser/media/debugHover.css index e7dd01a9cfb..f8e51e2e4de 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugHover.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugHover.css @@ -13,6 +13,7 @@ word-break: break-all; white-space: pre; border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .monaco-editor .debug-hover-widget .complex-value { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index f8a588049f0..ca34e6f77ba 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -13,6 +13,7 @@ left: 0; top: 0; -webkit-app-region: no-drag; + box-shadow: var(--vscode-shadow-lg); } .monaco-workbench .debug-toolbar .monaco-action-bar .action-item { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index fd3aa7ae144..3031aeba46d 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -161,6 +161,11 @@ .extensions-viewlet > .extensions .extension-list-item { position: absolute; + box-shadow: var(--vscode-shadow-sm); +} + +.extensions-viewlet > .extensions .extension-list-item:hover { + box-shadow: var(--vscode-shadow-md); } .extensions-viewlet > .extensions .extension-list-item.loading { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 5c7b116c2bf..6556757c59c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -8,7 +8,7 @@ color: inherit; border-radius: var(--vscode-cornerRadius-large); border: 1px solid var(--vscode-inlineChat-border); - box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); background: var(--vscode-inlineChat-background); padding-top: 3px; position: relative; diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index 37d4268342a..de36a936fd3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -9,7 +9,7 @@ border-radius: 8px; display: flex; align-items: center; - box-shadow: 0 4px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); cursor: pointer; min-width: var(--vscode-inline-chat-affordance-height); min-height: var(--vscode-inline-chat-affordance-height); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index be58df0988b..9b8cada0767 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -9,7 +9,7 @@ background: var(--vscode-panel-background); border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); border-radius: var(--vscode-cornerRadius-large); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); z-index: 100; } @@ -108,7 +108,7 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); overflow: hidden; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css index 65208ad34b0..4bd8e44bf11 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css @@ -16,7 +16,7 @@ visibility: hidden; background-color: var(--vscode-editorWidget-background) !important; color: var(--vscode-editorWidget-foreground); - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 54f1f0dfc78..e0309cff01e 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -299,6 +299,7 @@ display: block; position: absolute; pointer-events: none; + box-shadow: inset var(--vscode-shadow-sm); } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { @@ -383,6 +384,7 @@ .notebookOverlay .monaco-list-row .cell-title-toolbar { border-radius: var(--vscode-cornerRadius-medium); + box-shadow: var(--vscode-shadow-sm); } .notebookOverlay .monaco-list-row .cell-title-toolbar, diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 18206f1ab62..b6a6170b071 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; -import { asCssVariable, editorWidgetBackground, editorWidgetForeground, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; +import { asCssVariable, editorWidgetBackground, editorWidgetForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { ScrollType } from '../../../../editor/common/editorCommon.js'; import { SearchWidget, SearchOptions } from './preferencesWidgets.js'; import { Promises, timeout } from '../../../../base/common/async.js'; @@ -171,7 +171,6 @@ export class DefineKeybindingWidget extends Widget { this._domNode.domNode.style.backgroundColor = asCssVariable(editorWidgetBackground); this._domNode.domNode.style.color = asCssVariable(editorWidgetForeground); - this._domNode.domNode.style.boxShadow = `0 2px 8px ${asCssVariable(widgetShadow)}`; this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, this._domNode.domNode, { ariaLabel: message, history: new Set([]), inputBoxStyles: defaultInputBoxStyles })); this._keybindingInputWidget.startRecordingKeys(); diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css index 3874b5b70f7..75905e2e6d4 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css @@ -7,6 +7,7 @@ padding: 10px; border-radius: var(--vscode-cornerRadius-large); position: absolute; + box-shadow: var(--vscode-shadow-lg); } .defineKeybindingWidget .message { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 280ae562ace..e8ab65c8a5a 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -295,6 +295,7 @@ pointer-events: none; z-index: 10; position: absolute; + box-shadow: var(--vscode-shadow-sm); } .settings-editor > .settings-body .settings-toc-container .monaco-list { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 20c78c396f1..408b771f84c 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -38,6 +38,7 @@ height: 100%; align-items: center; flex-flow: nowrap; + box-shadow: var(--vscode-shadow-sm); } .scm-view.hide-provider-counts .scm-provider > .count, From 7b9ab5ae65c107840b61fb0f2283edbb74c1056f Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 3 Mar 2026 11:50:19 +0100 Subject: [PATCH 031/448] agent sessions approval row --- .../sessions/browser/sessionsViewPane.ts | 1 + .../agentSessionApprovalModel.ts | 124 ++++ .../agentSessions/agentSessionsControl.ts | 14 +- .../agentSessions/agentSessionsViewer.ts | 92 ++- .../media/agentsessionsviewer.css | 48 ++ .../agentSessionApprovalModel.test.ts | 522 +++++++++++++++ .../agentSessionsViewer.fixture.ts | 599 ++++++++++++++++++ 7 files changed, 1396 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index eb76bcf42f2..59ffa2aead3 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -143,6 +143,7 @@ export class AgenticSessionsViewPane extends ViewPane { filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, disableHover: true, + enableApprovalRow: true, getHoverPosition: () => this.getSessionHoverPosition(), trackActiveEditorSession: () => true, collapseOlderSections: () => true, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts new file mode 100644 index 00000000000..5b2aa56855a --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; +import { Disposable, DisposableResourceMap, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, autorunIterableDelta, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js'; +import { IChatModel } from '../../common/model/chatModel.js'; +import { IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; + +export interface IAgentSessionApprovalInfo { + readonly label: string; + readonly languageId: string | undefined; + confirm(): void; +} + +/** + * Tracks approval state for all live chat sessions. For each session, + * exposes an observable that emits {@link IAgentSessionApprovalInfo} + * when a tool invocation is waiting for user confirmation, or `undefined` + * when no approval is needed. + */ +export class AgentSessionApprovalModel extends Disposable { + + private readonly _approvals = new Map>(); + private readonly _modelTrackers = this._register(new DisposableResourceMap()); + + constructor( + @IChatService private readonly _chatService: IChatService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(autorunIterableDelta( + reader => this._chatService.chatModels.read(reader), + ({ addedValues, removedValues }) => { + for (const model of addedValues) { + this._modelTrackers.set(model.sessionResource, this._trackModel(model)); + } + for (const model of removedValues) { + this._modelTrackers.deleteAndDispose(model.sessionResource); + this._approvals.get(model.sessionResource.toString())?.set(undefined, undefined); + } + } + )); + } + + getApproval(sessionResource: URI): IObservable { + return this._getOrCreateApproval(sessionResource.toString()); + } + + private _getOrCreateApproval(key: string): ISettableObservable { + let obs = this._approvals.get(key); + if (!obs) { + obs = observableValue(`sessionApproval.${key}`, undefined); + this._approvals.set(key, obs); + } + return obs; + } + + private _trackModel(model: IChatModel): IDisposable { + const settable = this._getOrCreateApproval(model.sessionResource.toString()); + + const setIfChanged = (value: IAgentSessionApprovalInfo | undefined) => { + const current = settable.get(); + if (current === value) { + return; + } + if (current !== undefined && value !== undefined && current.label === value.label && current.languageId === value.languageId) { + return; + } + settable.set(value, undefined); + }; + + return autorun(reader => { + const needsInput = model.requestNeedsInput.read(reader); + if (!needsInput) { + setIfChanged(undefined); + return; + } + + const lastResponse = model.lastRequest?.response; + if (!lastResponse?.response?.value) { + setIfChanged(undefined); + return; + } + + for (const part of lastResponse.response.value) { + if (part.kind !== 'toolInvocation') { + continue; + } + const state = part.state.read(reader); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + let label: string; + let languageId: string | undefined; + if (part.toolSpecificData?.kind === 'terminal') { + const terminalData = migrateLegacyTerminalToolSpecificData(part.toolSpecificData); + label = terminalData.presentationOverrides?.commandLine ?? terminalData.commandLine.forDisplay ?? terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; + languageId = this._languageService.getLanguageIdByLanguageName(terminalData.presentationOverrides?.language ?? terminalData.language) ?? undefined; + } else if (needsInput.detail) { + label = needsInput.detail; + } else { + const msg = part.invocationMessage; + label = typeof msg === 'string' ? msg : renderAsPlaintext(msg); + } + + const confirmState = state; + setIfChanged({ + label, + languageId, + confirm: () => confirmState.confirm({ type: ToolConfirmKind.UserAction }), + }); + return; + } + } + + setIfChanged(undefined); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 384baff52d4..95d06b9dc2a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -11,6 +11,7 @@ import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../p import { $, append, EventHelper } from '../../../../../base/browser/dom.js'; import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; +import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; @@ -41,6 +42,7 @@ export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOption readonly filter: IAgentSessionsFilter; readonly source: string; readonly disableHover?: boolean; + readonly enableApprovalRow?: boolean; getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; @@ -163,13 +165,15 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo }; const sorter = new AgentSessionsSorter(this.options); + const approvalModel = this.options.enableApprovalRow ? this._register(this.instantiationService.createInstance(AgentSessionApprovalModel)) : undefined; + const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options, approvalModel)); const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'AgentSessionsView', this.sessionsContainer, - new AgentSessionsListDelegate(), + new AgentSessionsListDelegate(approvalModel), new AgentSessionsCompressionDelegate(), [ - this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options)), + sessionRenderer, this.instantiationService.createInstance(AgentSessionSectionRenderer), ], new AgentSessionsDataSource(this.options.filter, sorter), @@ -191,6 +195,12 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo ChatContextKeys.agentSessionsViewerFocused.bindTo(list.contextKeyService); + this._register(sessionRenderer.onDidChangeItemHeight(session => { + if (list.hasNode(session)) { + list.updateElementHeight(session, undefined); + } + })); + const model = this.agentSessionsService.model; this._register(this.options.filter.onDidChange(async () => { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index eb1748803f0..095bd49e0f1 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -37,12 +37,18 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { Event } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js'; import { AgentSessionProviders, getAgentSessionTime } from './agentSessions.js'; import { AgentSessionsGrouping } from './agentSessionsFilter.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; + export type AgentSessionListItem = IAgentSession | IAgentSessionSection; @@ -70,6 +76,11 @@ interface IAgentSessionItemTemplate { readonly separator: HTMLElement; readonly description: HTMLElement; + // Approval row + readonly approvalRow: HTMLElement; + readonly approvalLabel: HTMLElement; + readonly approvalButtonContainer: HTMLElement; + readonly contextKeyService: IContextKeyService; readonly elementDisposable: DisposableStore; readonly disposables: IDisposable; @@ -84,12 +95,18 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre static readonly TEMPLATE_ID = 'agent-session'; + static readonly APPROVAL_ROW_HEIGHT = 40; + readonly templateId = AgentSessionRenderer.TEMPLATE_ID; private readonly sessionHover = this._register(new MutableDisposable()); + private readonly _onDidChangeItemHeight = this._register(new Emitter()); + readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; + constructor( private readonly options: IAgentSessionRendererOptions, + private readonly _approvalModel: AgentSessionApprovalModel | undefined, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @IProductService private readonly productService: IProductService, @IHoverService private readonly hoverService: IHoverService, @@ -129,6 +146,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre h('span.agent-session-status-time@statusTime') ]), ]), + ]), + h('div.agent-session-approval-row@approvalRow', [ + h('span.agent-session-approval-label@approvalLabel'), + h('div.agent-session-approval-button@approvalButtonContainer'), ]) ]) ] @@ -156,6 +177,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre statusContainer: elements.statusContainer, statusProviderIcon: elements.statusProviderIcon, statusTime: elements.statusTime, + approvalRow: elements.approvalRow, + approvalLabel: elements.approvalLabel, + approvalButtonContainer: elements.approvalButtonContainer, contextKeyService, elementDisposable, disposables @@ -229,6 +253,11 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre // Hover this.renderHover(session, template); + + // Approval row + if (this._approvalModel) { + this.renderApprovalRow(session, template); + } } private renderBadge(session: ITreeNode, template: IAgentSessionItemTemplate): boolean { @@ -396,6 +425,55 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre }; } + private renderApprovalRow(session: ITreeNode, template: IAgentSessionItemTemplate): void { + if (this._approvalModel === undefined) { + throw new BugIndicatingError('Approval model is required to render approval row'); + } + + const approvalModel = this._approvalModel; + // Initialize from current model state to avoid unnecessary height changes on first render + const initialInfo = approvalModel.getApproval(session.element.resource).get(); + let wasVisible = !!initialInfo; + template.approvalRow.classList.toggle('visible', wasVisible); + + const buttonStore = template.elementDisposable.add(new DisposableStore()); + + template.elementDisposable.add(autorun(reader => { + buttonStore.clear(); + + const info = approvalModel.getApproval(session.element.resource).read(reader); + const visible = !!info; + + template.approvalRow.classList.toggle('visible', visible); + + if (info) { + // Render as a syntax-highlighted code block + const codeblockContent = new MarkdownString().appendCodeblock(info.languageId ?? 'json', info.label); + this.renderMarkdownOrText(codeblockContent, template.approvalLabel, buttonStore); + + // Hover with full content as a code block + buttonStore.add(this.hoverService.setupDelayedHover(template.approvalLabel, { + content: codeblockContent, + style: HoverStyle.Pointer, + position: { hoverPosition: HoverPosition.BELOW }, + })); + + template.approvalButtonContainer.textContent = ''; + const button = buttonStore.add(new Button(template.approvalButtonContainer, { + title: localize('allowActionOnce', "Allow once"), + ...defaultButtonStyles + })); + button.label = localize('allowAction', "Allow"); + buttonStore.add(button.onDidClick(() => info.confirm())); + } + + if (wasVisible !== visible) { + wasVisible = visible; + this._onDidChangeItemHeight.fire(session.element); + } + })); + } + renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void { throw new Error('Should never happen since session is incompressible'); } @@ -509,12 +587,22 @@ export class AgentSessionsListDelegate implements IListVirtualDelegate .rendered-markdown, + & > .rendered-markdown > .code, + & > .rendered-markdown > .code > span { + display: block; + overflow: hidden; + } + + .monaco-tokenized-source { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; + } + } + + .agent-session-approval-button { + flex-shrink: 0; + + .monaco-button { + padding: 2px 10px; + font-size: 12px; + white-space: nowrap; + } + } + } } .agent-session-section { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts new file mode 100644 index 00000000000..945c6cd2f72 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts @@ -0,0 +1,522 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { AgentSessionApprovalModel, IAgentSessionApprovalInfo } from '../../../browser/agentSessions/agentSessionApprovalModel.js'; +import { MockChatModel } from '../../common/model/mockChatModel.js'; +import { MockChatService } from '../../common/chatService/mockChatService.js'; +import { IChatToolInvocation, IChatTerminalToolInvocationData, ToolConfirmKind, ConfirmedReason } from '../../../common/chatService/chatService.js'; +import { IChatModel, IChatRequestModel, IChatResponseModel, IResponse, IChatProgressResponseContent } from '../../../common/model/chatModel.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; + +function makeToolInvocationPart(options: { + state: IChatToolInvocation.State; + toolSpecificData?: IChatToolInvocation['toolSpecificData']; + invocationMessage?: string | MarkdownString; +}): IChatToolInvocation { + return { + kind: 'toolInvocation', + presentation: undefined!, + originMessage: undefined, + invocationMessage: options.invocationMessage ?? 'Running tool...', + pastTenseMessage: undefined, + source: undefined!, + toolId: 'test-tool', + toolCallId: 'call-1', + state: observableValue('toolState', options.state), + toolSpecificData: options.toolSpecificData, + toJSON: () => undefined!, + }; +} + +function makeTerminalToolData(overrides?: Partial): IChatTerminalToolInvocationData { + return { + kind: 'terminal', + commandLine: { original: 'echo hello' }, + language: 'sh', + ...overrides, + }; +} + +function makeWaitingState(confirm?: (reason: ConfirmedReason) => void): IChatToolInvocation.State { + return { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: {}, + confirm: confirm ?? (() => { }), + } as IChatToolInvocation.State; +} + +function makePostApprovalState(confirm?: (reason: ConfirmedReason) => void): IChatToolInvocation.State { + return { + type: IChatToolInvocation.StateKind.WaitingForPostApproval, + parameters: {}, + confirmed: { type: ToolConfirmKind.UserAction }, + resultDetails: undefined, + confirm: confirm ?? (() => { }), + contentForModel: [], + } as IChatToolInvocation.State; +} + +function makeExecutingState(): IChatToolInvocation.State { + return { + type: IChatToolInvocation.StateKind.Executing, + parameters: {}, + confirmed: { type: ToolConfirmKind.UserAction }, + progress: observableValue('progress', { message: undefined, progress: undefined }), + } as IChatToolInvocation.State; +} + +/** Creates a minimal mock that satisfies the response chain: lastRequest.response.response.value */ +function mockModelWithResponse(model: MockChatModel, parts: IChatProgressResponseContent[]): void { + const response: Partial = { + response: { value: parts, getMarkdown: () => '', toString: () => '' } satisfies IResponse, + }; + const request: Partial = { + response: response as IChatResponseModel, + }; + (model as { lastRequest: IChatRequestModel | undefined }).lastRequest = request as IChatRequestModel; +} + +class MockLanguageService { + getLanguageIdByLanguageName(name: string): string | undefined { + switch (name) { + case 'bash': return 'sh'; + case 'python': return 'python'; + case 'powershell': return 'pwsh'; + default: return name; + } + } +} + +suite('AgentSessionApprovalModel', () => { + + const disposables = new DisposableStore(); + let chatService: MockChatService; + let chatModelsObs: ISettableObservable>; + let langservice: MockLanguageService; + + setup(() => { + chatService = new MockChatService(); + langservice = new MockLanguageService(); + chatModelsObs = chatService.chatModels as ISettableObservable>; + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function createModel(): AgentSessionApprovalModel { + const model = new AgentSessionApprovalModel(chatService, langservice as ILanguageService); + disposables.add(model); + return model; + } + + function addChatModel(uri?: URI): MockChatModel { + const chatModel = disposables.add(new MockChatModel(uri ?? URI.parse(`test://session/${Math.random()}`))); + chatModelsObs.set([...Array.from(chatModelsObs.get()), chatModel], undefined); + return chatModel; + } + + function getApproval(approvalModel: AgentSessionApprovalModel, chatModel: MockChatModel): IAgentSessionApprovalInfo | undefined { + return approvalModel.getApproval(chatModel.sessionResource).get(); + } + + test('returns undefined when no models exist', () => { + const approvalModel = createModel(); + const result = approvalModel.getApproval(URI.parse('test://nonexistent')).get(); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when model has no requestNeedsInput', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns undefined when requestNeedsInput is set but no response exists', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns undefined when response has no tool invocation parts', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + mockModelWithResponse(chatModel, []); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns undefined when tool invocation is in Executing state', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ state: makeExecutingState() }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns approval info for WaitingForConfirmation state with terminal data', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'echo hello', + language: 'sh', + }); + }); + + test('returns approval info for WaitingForPostApproval state', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makePostApprovalState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'npm install' } }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'npm install', + language: 'sh', + }); + }); + + test('prefers presentationOverrides.commandLine and language', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'python -c "print(1)"' }, + language: 'sh', + presentationOverrides: { commandLine: 'print(1)', language: 'python' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'print(1)', + language: 'python', + }); + }); + + test('uses forDisplay from commandLine when available', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'echo raw', forDisplay: 'echo display' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'echo display'); + }); + + test('uses userEdited from commandLine when forDisplay is not set', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'orig', userEdited: 'user-edited' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'user-edited'); + }); + + test('uses toolEdited from commandLine as fallback', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'orig', toolEdited: 'tool-edited' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'tool-edited'); + }); + + test('uses needsInput.detail when tool is not terminal', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ state: makeWaitingState() }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test', detail: 'Custom detail message' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'Custom detail message', + language: undefined, + }); + }); + + test('uses invocationMessage string when no terminal data and no detail', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + invocationMessage: 'Searching files...', + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'Searching files...', + language: undefined, + }); + }); + + test('uses invocationMessage MarkdownString when no terminal data and no detail', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + invocationMessage: new MarkdownString('**Running** tool'), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'Running tool'); + }); + + test('confirm() delegates to tool state confirm with UserAction', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + let confirmedWith: ConfirmedReason | undefined; + const part = makeToolInvocationPart({ + state: makeWaitingState(reason => { confirmedWith = reason; }), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + getApproval(approvalModel, chatModel)?.confirm(); + assert.deepStrictEqual(confirmedWith, { type: ToolConfirmKind.UserAction }); + }); + + test('reacts to requestNeedsInput becoming undefined', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + chatModel.requestNeedsInput.set(undefined, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('reacts to tool state changing from waiting to executing', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const stateObs = observableValue('toolState', makeWaitingState()); + const part: IChatToolInvocation = { + ...makeToolInvocationPart({ state: makeWaitingState(), toolSpecificData: makeTerminalToolData() }), + state: stateObs, + }; + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + stateObs.set(makeExecutingState(), undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('tracks multiple models independently', () => { + const approvalModel = createModel(); + const chatModel1 = addChatModel(URI.parse('test://session/1')); + const chatModel2 = addChatModel(URI.parse('test://session/2')); + + const part1 = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'cmd1' } }), + }); + mockModelWithResponse(chatModel1, [part1]); + chatModel1.requestNeedsInput.set({ title: 'Session 1' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel1)?.label, 'cmd1'); + assert.strictEqual(getApproval(approvalModel, chatModel2), undefined); + }); + + test('clears approval when model is removed', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + // Remove model from chatModels + chatModelsObs.set([], undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('picks the first WaitingForConfirmation part when multiple parts exist', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const executingPart = makeToolInvocationPart({ state: makeExecutingState() }); + const waitingPart = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'second-cmd' } }), + }); + mockModelWithResponse(chatModel, [executingPart, waitingPart]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'second-cmd'); + }); + + test('handles model added after approval model is created', () => { + const approvalModel = createModel(); + + // No models yet + const uri = URI.parse('test://session/late'); + assert.strictEqual(approvalModel.getApproval(uri).get(), undefined); + + // Add model later + const chatModel = addChatModel(uri); + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'late-cmd' } }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'late-cmd'); + }); + + test('handles legacy terminal tool data', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + // Legacy format has `command` instead of `commandLine` + const legacyData = { kind: 'terminal' as const, command: 'legacy-cmd', language: 'bash' }; + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: legacyData, + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'legacy-cmd', + language: 'bash', + }); + }); + + test('observable is reused for the same session resource', () => { + const approvalModel = createModel(); + const uri = URI.parse('test://session/same'); + + const obs1 = approvalModel.getApproval(uri); + const obs2 = approvalModel.getApproval(uri); + assert.strictEqual(obs1, obs2); + }); + + test('skips non-toolInvocation parts', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const markdownPart = { kind: 'markdownContent' as const, content: new MarkdownString('hello') }; + const waitingPart = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'the-cmd' } }), + }); + mockModelWithResponse(chatModel, [markdownPart as unknown as IChatProgressResponseContent, waitingPart]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'the-cmd'); + }); + + test('updating requestNeedsInput triggers re-evaluation', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + // Initially no requestNeedsInput + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + + // Set requestNeedsInput + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + // Clear again + chatModel.requestNeedsInput.set(undefined, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts new file mode 100644 index 00000000000..f88f88719f4 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts @@ -0,0 +1,599 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { FuzzyScore } from '../../../../base/common/filters.js'; +import { ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { IMarkdownRendererService, MarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js'; +import { EditorMarkdownCodeBlockRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/editorMarkdownCodeBlockRenderer.js'; +import { AgentSessionRenderer, AgentSessionSectionRenderer, IAgentSessionRendererOptions } from '../../../contrib/chat/browser/agentSessions/agentSessionsViewer.js'; +import { AgentSessionStatus, IAgentSession, AgentSessionSection, IAgentSessionSection } from '../../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { AgentSessionProviders } from '../../../contrib/chat/browser/agentSessions/agentSessions.js'; +import { AgentSessionApprovalModel, IAgentSessionApprovalInfo } from '../../../contrib/chat/browser/agentSessions/agentSessionApprovalModel.js'; +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; + +import '../../../contrib/chat/browser/agentSessions/media/agentsessionsviewer.css'; + +// ============================================================================ +// Mock helpers +// ============================================================================ + +function createMockSession(overrides: Partial & { label: string; status: AgentSessionStatus; providerType: string }): IAgentSession { + const now = Date.now(); + return new class extends mock() { + override readonly resource = overrides.resource ?? URI.parse(`vscode-chat-session://${overrides.providerType}/session-${Math.random().toString(36).slice(2)}`); + override readonly label = overrides.label; + override readonly status = overrides.status; + override readonly providerType = overrides.providerType; + override readonly providerLabel = overrides.providerLabel ?? overrides.providerType; + override readonly icon = overrides.icon ?? Codicon.vm; + override readonly badge = overrides.badge; + override readonly description = overrides.description; + override readonly tooltip = overrides.tooltip; + override readonly changes = overrides.changes; + override readonly timing = overrides.timing ?? { + created: now - 60 * 60 * 1000, + lastRequestStarted: undefined, + lastRequestEnded: undefined, + }; + override isArchived(): boolean { return overrides.isArchived?.() ?? false; } + override setArchived(): void { } + override isRead(): boolean { return overrides.isRead?.() ?? true; } + override setRead(): void { } + }(); +} + +function wrapAsTreeNode(element: T): ITreeNode { + return { + element, + children: [], + depth: 0, + visibleChildrenCount: 0, + visibleChildIndex: 0, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined, + }; +} + +const rendererOptions: IAgentSessionRendererOptions = { + disableHover: true, + getHoverPosition: () => HoverPosition.BELOW, +}; + +// ============================================================================ +// Render helpers +// ============================================================================ + +function createMockApprovalModel(sessionResource: URI, info: IAgentSessionApprovalInfo): AgentSessionApprovalModel { + const obs = observableValue('mockApproval', info); + return new class extends mock() { + override getApproval(resource: URI) { + if (resource.toString() === sessionResource.toString()) { + return obs; + } + return observableValue('mockApproval.empty', undefined); + } + }(); +} + +function renderSessionItem(ctx: ComponentFixtureContext, session: IAgentSession, approvalModel?: AgentSessionApprovalModel): void { + const { container, disposableStore } = ctx; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + reg.define(IMarkdownRendererService, MarkdownRendererService); + reg.defineInstance(IProductService, new class extends mock() { + override readonly urlProtocol = 'vscode'; + }()); + }, + }); + + const configService = instantiationService.get(IConfigurationService) as TestConfigurationService; + configService.setUserConfiguration('editor', { fontFamily: 'monospace' }); + const markdownRendererService = instantiationService.get(IMarkdownRendererService); + markdownRendererService.setDefaultCodeBlockRenderer(instantiationService.createInstance(EditorMarkdownCodeBlockRenderer)); + + const renderer = disposableStore.add( + instantiationService.createInstance(AgentSessionRenderer, rendererOptions, approvalModel ?? undefined) + ); + + container.style.width = '350px'; + container.style.height = 'auto'; + container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + container.classList.add('agent-sessions-viewer'); + + const listRow = document.createElement('div'); + listRow.classList.add('monaco-list-row'); + listRow.style.position = 'relative'; + container.appendChild(listRow); + + const template = renderer.renderTemplate(listRow); + renderer.renderElement(wrapAsTreeNode(session), 0, template); +} + +function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionSection): void { + const { container, disposableStore } = ctx; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + }, + }); + + const renderer = instantiationService.createInstance(AgentSessionSectionRenderer); + + container.style.width = '350px'; + container.style.height = 'auto'; + container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + container.classList.add('agent-sessions-viewer'); + + const listRow = document.createElement('div'); + listRow.classList.add('monaco-list-row'); + listRow.style.position = 'relative'; + container.appendChild(listRow); + + const template = renderer.renderTemplate(listRow); + renderer.renderElement(wrapAsTreeNode(section), 0, template); +} + +// ============================================================================ +// Fixtures +// ============================================================================ + +const now = Date.now(); + +export default defineThemedFixtureGroup({ + + // --- Status variants --- + + CompletedRead: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Refactor auth middleware', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 2 * 60 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 60 * 1000, + lastRequestEnded: now - 2 * 60 * 60 * 1000 + 45 * 1000, + }, + })), + }), + + CompletedUnread: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Add unit tests for parser', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + isRead: () => false, + timing: { + created: now - 30 * 60 * 1000, + lastRequestStarted: now - 30 * 60 * 1000, + lastRequestEnded: now - 25 * 60 * 1000, + }, + })), + }), + + InProgress: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Implement dark mode toggle', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 5 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + NeedsInput: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Fix CI pipeline configuration', + status: AgentSessionStatus.NeedsInput, + providerType: AgentSessionProviders.Local, + isRead: () => false, + timing: { + created: now - 10 * 60 * 1000, + lastRequestStarted: now - 8 * 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + FailedWithDuration: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Deploy staging environment', + status: AgentSessionStatus.Failed, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 60 * 60 * 1000, + lastRequestStarted: now - 60 * 60 * 1000, + lastRequestEnded: now - 60 * 60 * 1000 + 3 * 60 * 1000, + }, + })), + }), + + FailedWithoutDuration: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Migrate database schema', + status: AgentSessionStatus.Failed, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 3 * 60 * 60 * 1000, + lastRequestStarted: undefined, + lastRequestEnded: undefined, + }, + })), + }), + + // --- Content variants --- + + WithDiffChanges: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Refactor settings page', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + changes: { files: 5, insertions: 142, deletions: 87 }, + timing: { + created: now - 45 * 60 * 1000, + lastRequestStarted: now - 45 * 60 * 1000, + lastRequestEnded: now - 40 * 60 * 1000, + }, + })), + }), + + WithFileChangesList: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Update API endpoints', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Background, + icon: Codicon.worktree, + changes: [ + { modifiedUri: URI.file('/src/api/routes.ts'), insertions: 25, deletions: 10 }, + { modifiedUri: URI.file('/src/api/handlers.ts'), insertions: 50, deletions: 30 }, + { modifiedUri: URI.file('/tests/api.test.ts'), insertions: 40, deletions: 5 }, + ], + timing: { + created: now - 2 * 60 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 60 * 1000, + lastRequestEnded: now - 90 * 60 * 1000, + }, + })), + }), + + WithBadge: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Optimize build pipeline', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + badge: 'PR #1234', + timing: { + created: now - 4 * 60 * 60 * 1000, + lastRequestStarted: now - 4 * 60 * 60 * 1000, + lastRequestEnded: now - 3.5 * 60 * 60 * 1000, + }, + })), + }), + + WithMarkdownBadge: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Review security patches', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + badge: new MarkdownString('$(shield) Secure'), + timing: { + created: now - 6 * 60 * 60 * 1000, + lastRequestStarted: now - 6 * 60 * 60 * 1000, + lastRequestEnded: now - 5.5 * 60 * 60 * 1000, + }, + })), + }), + + WithDescription: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Upgrade dependencies', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + description: 'Updated 12 packages to latest versions', + timing: { + created: now - 24 * 60 * 60 * 1000, + lastRequestStarted: now - 24 * 60 * 60 * 1000, + lastRequestEnded: now - 23.5 * 60 * 60 * 1000, + }, + })), + }), + + WithMarkdownDescription: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Fix accessibility issues', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + description: new MarkdownString('$(check) All WCAG checks passed'), + timing: { + created: now - 48 * 60 * 60 * 1000, + lastRequestStarted: now - 48 * 60 * 60 * 1000, + lastRequestEnded: now - 47 * 60 * 60 * 1000, + }, + })), + }), + + WithBadgeAndDiff: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Implement search feature', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + badge: 'draft', + changes: { files: 8, insertions: 320, deletions: 45 }, + timing: { + created: now - 3 * 60 * 60 * 1000, + lastRequestStarted: now - 3 * 60 * 60 * 1000, + lastRequestEnded: now - 2.5 * 60 * 60 * 1000, + }, + })), + }), + + // --- State variants --- + + Archived: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Old migration script', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + isArchived: () => true, + timing: { + created: now - 7 * 24 * 60 * 60 * 1000, + lastRequestStarted: now - 7 * 24 * 60 * 60 * 1000, + lastRequestEnded: now - 7 * 24 * 60 * 60 * 1000 + 10 * 60 * 1000, + }, + })), + }), + + ArchivedUnread: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Archived unread task', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + isArchived: () => true, + isRead: () => false, + timing: { + created: now - 5 * 24 * 60 * 60 * 1000, + lastRequestStarted: now - 5 * 24 * 60 * 60 * 1000, + lastRequestEnded: now - 5 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000, + }, + })), + }), + + // --- Provider-type variants --- + + CloudProvider: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Generate API documentation', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + timing: { + created: now - 90 * 60 * 1000, + lastRequestStarted: now - 90 * 60 * 1000, + lastRequestEnded: now - 80 * 60 * 1000, + }, + })), + }), + + BackgroundProvider: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Run linter across codebase', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Background, + icon: Codicon.worktree, + timing: { + created: now - 120 * 60 * 1000, + lastRequestStarted: now - 120 * 60 * 1000, + lastRequestEnded: now - 110 * 60 * 1000, + }, + })), + }), + + ClaudeProvider: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Analyze code complexity', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Claude, + icon: Codicon.claude, + timing: { + created: now - 150 * 60 * 1000, + lastRequestStarted: now - 150 * 60 * 1000, + lastRequestEnded: now - 140 * 60 * 1000, + }, + })), + }), + + CloudProviderInProgress: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Build integration tests', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + isRead: () => false, + timing: { + created: now - 10 * 60 * 1000, + lastRequestStarted: now - 3 * 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + // --- In-progress with description override --- + + InProgressWithDescription: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Scaffold new microservice', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Background, + icon: Codicon.worktree, + description: 'Installing dependencies...', + timing: { + created: now - 5 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + // --- Section headers --- + + SectionToday: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Today, + label: 'Today', + sessions: [], + }), + }), + + SectionYesterday: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Yesterday, + label: 'Yesterday', + sessions: [], + }), + }), + + SectionLastWeek: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Week, + label: 'Last 7 days', + sessions: [], + }), + }), + + SectionOlder: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Older, + label: 'Older', + sessions: [], + }), + }), + + SectionArchived: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Archived, + label: 'Archived', + sessions: [], + }), + }), + + SectionMore: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.More, + label: 'More', + sessions: [], + }), + }), + + // --- Approval row variants --- + + ApprovalRowJson: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-json'); + const approvalModel = createMockApprovalModel(resource, { + label: '{ "action": "deleteFile", "path": "/src/old-module.ts" }', + languageId: 'json', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Clean up deprecated modules', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 5 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRowBash: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-bash'); + const approvalModel = createMockApprovalModel(resource, { + label: 'npm install --save express@latest', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Update server dependencies', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 3 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRowPowerShell: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-powershell'); + const approvalModel = createMockApprovalModel(resource, { + label: 'Start-Job -ScriptBlock { Set-Location \'c:\\some\\path\'; npm install } | Out-Null', + languageId: 'pwsh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Clean up old log files', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 4 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRowLongLabel: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-long'); + const approvalModel = createMockApprovalModel(resource, { + label: 'rm -rf node_modules && npm cache clean --force && npm install --legacy-peer-deps --ignore-scripts', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Reset and reinstall all dependencies', + status: AgentSessionStatus.NeedsInput, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + isRead: () => false, + timing: { + created: now - 10 * 60 * 1000, + lastRequestStarted: now - 5 * 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), +}); From 985ce3841e1688958ffafc535f5c5226185ebd5e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 10:59:00 +0000 Subject: [PATCH 032/448] refactor: reorganize shadow variables in theme styles for consistency Co-authored-by: Copilot --- .../lib/stylelint/vscode-known-variables.json | 16 +++++++------- extensions/theme-2026/themes/styles.css | 21 ++----------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 2cdd3b0077f..1ea3723af79 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -934,14 +934,6 @@ "--notebook-editor-font-weight", "--outline-element-color", "--separator-border", - "--vscode-shadow-active-tab", - "--vscode-shadow-depth-x", - "--vscode-shadow-depth-y", - "--vscode-shadow-hover", - "--vscode-shadow-lg", - "--vscode-shadow-md", - "--vscode-shadow-sm", - "--vscode-shadow-xl", "--status-border-top-color", "--tab-border-bottom-color", "--tab-border-top-color", @@ -979,6 +971,14 @@ "--vscode-repl-line-height", "--vscode-sash-hover-size", "--vscode-sash-size", + "--vscode-shadow-active-tab", + "--vscode-shadow-depth-x", + "--vscode-shadow-depth-y", + "--vscode-shadow-hover", + "--vscode-shadow-lg", + "--vscode-shadow-md", + "--vscode-shadow-sm", + "--vscode-shadow-xl", "--vscode-testing-coverage-lineHeight", "--vscode-editorStickyScroll-scrollableWidth", "--vscode-editorStickyScroll-foldingOpacityTransition", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 9e5d90e3a2e..a13d0cdad9b 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -22,7 +22,6 @@ } /* Quick Input (Command Palette) */ - .monaco-workbench.vs-dark .quick-input-widget { border: 1px solid var(--vscode-menu-border) !important; } @@ -103,17 +102,11 @@ } /* Context Menus */ - -.monaco-workbench .context-view .monaco-menu { - border: none; -} - .monaco-workbench .action-widget .action-widget-action-bar { background: transparent; } /* Suggest Widget */ - .monaco-workbench.vs-dark .monaco-editor .suggest-widget { border: 1px solid var(--vscode-editorWidget-border); } @@ -124,7 +117,6 @@ } /* Peek View */ - .monaco-workbench .monaco-editor .peekview-widget .head, .monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; @@ -134,13 +126,13 @@ border: 1px solid var(--vscode-editorWidget-border); } +/* Chat Editor Overlay */ .monaco-workbench.vs-dark .chat-editor-overlay-widget, .monaco-workbench.vs-dark .chat-diff-change-content-widget { border: 1px solid var(--vscode-editorWidget-border); } /* Settings */ - .monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { border-radius: var(--radius-sm); background: transparent !important; @@ -148,21 +140,13 @@ border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } -/* Extensions */ -.monaco-workbench .extensions-list .extension-list-item { - border: none; -} - /* Breadcrumbs */ .monaco-workbench.vs .breadcrumbs-control { border-bottom: 1px solid var(--vscode-editorWidget-border); } -/* Input Boxes */ - - -.monaco-inputbox .monaco-action-bar .action-item .codicon, +/* Input Boxes */\n.monaco-inputbox .monaco-action-bar .action-item .codicon, .monaco-workbench .search-container .input-box, .monaco-custom-toggle { color: var(--vscode-icon-foreground) !important; @@ -273,7 +257,6 @@ } /* Command Center */ - .monaco-workbench .part.titlebar .command-center .agent-status-pill { border-color: var(--vscode-input-border); } From 858363ee8d29528d2d1ab171e5056fb365e85a30 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 11:59:56 +0100 Subject: [PATCH 033/448] sessions - always prefer session label (#298926) * sessions - always prefer session label * Update src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../sessions/browser/sessionsTitleBarWidget.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 5590f70761a..c5d61de4e50 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -233,20 +233,19 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { /** * Get the label of the active chat session. - * Prefers the live model title over the snapshot label from the active session service. - * Falls back to a generic label if no active session is found. */ private _getActiveSessionLabel(): string { const activeSession = this.activeSessionService.getActiveSession(); - if (activeSession?.resource) { - const model = this.chatService.getSession(activeSession.resource); - if (model?.title) { - return model.title; - } + const label = activeSession?.label; + if (label) { + return label; // prefer session label to support renamed sessions } - if (activeSession?.label) { - return activeSession.label; + if (activeSession) { + const activeModel = this.chatService.getSession(activeSession.resource); + if (activeModel?.title) { + return activeModel.title; // fall back to chat model title if available + } } return localize('agentSessions.newSession', "New Session"); From cbfb7586bc56b760388cfebf6c30c79730941910 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:04:06 +0100 Subject: [PATCH 034/448] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/agentSessions/agentSessionsViewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 095bd49e0f1..fb7426b88e8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -602,7 +602,7 @@ export class AgentSessionsListDelegate implements IListVirtualDelegate Date: Tue, 3 Mar 2026 11:17:45 +0000 Subject: [PATCH 035/448] refactor: remove redundant overflow styles and apply box-shadow to notifications list container --- .../parts/notifications/media/notificationsToasts.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index 3dca2ce638a..73764a1003e 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -9,8 +9,6 @@ right: 3px; bottom: 25px; /* 22px status bar height + 3px */ display: none; - overflow: hidden; - box-shadow: var(--vscode-shadow-lg); border-radius: var(--vscode-cornerRadius-small); } @@ -37,10 +35,6 @@ flex-direction: column; } -.monaco-workbench > .notifications-toasts .notification-toast-container { - overflow: hidden; /* this ensures that the notification toast does not shine through */ -} - .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast { margin: 4px; /* enables separation and drop shadows around toasts */ transform: translate3d(0px, 100%, 0px); /* move the notification 50px to the bottom (to prevent bleed through) */ @@ -48,6 +42,10 @@ transition: transform 300ms ease-out, opacity 300ms ease-out; } +.monaco-workbench > .notifications-toasts .notifications-list-container { + box-shadow: var(--vscode-shadow-lg); +} + .monaco-workbench > .notifications-toasts .notifications-list-container, .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast, .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast .monaco-scrollable-element, From 50d56004c5500013ad475d3f8191fd74099c06cc Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 11:20:06 +0000 Subject: [PATCH 036/448] refactor: remove overflow styles from notifications and clean up input box styles --- extensions/theme-2026/themes/styles.css | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index a13d0cdad9b..af8bc218032 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -92,11 +92,6 @@ /* Notifications */ -.monaco-workbench .notifications-toasts, -.monaco-workbench > .notifications-toasts .notification-toast-container { - overflow: visible; -} - .monaco-workbench .notifications-list-container .monaco-list-rows { background: transparent !important; } @@ -146,7 +141,8 @@ border-bottom: 1px solid var(--vscode-editorWidget-border); } -/* Input Boxes */\n.monaco-inputbox .monaco-action-bar .action-item .codicon, +/* Input Boxes */ +.monaco-inputbox .monaco-action-bar .action-item .codicon, .monaco-workbench .search-container .input-box, .monaco-custom-toggle { color: var(--vscode-icon-foreground) !important; From f7d4cc7365a37a0261cc87bf1ab4bb036ee5d531 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 3 Mar 2026 12:29:27 +0100 Subject: [PATCH 037/448] fix sessions title bar (#298932) * fix title alignment * fix sessions title bar --- src/vs/sessions/browser/layoutActions.ts | 6 +- src/vs/sessions/browser/menus.ts | 10 +-- .../browser/parts/media/titlebarpart.css | 76 ++++++++++++++++--- src/vs/sessions/browser/parts/sidebarPart.ts | 2 +- src/vs/sessions/browser/parts/titlebarPart.ts | 15 +++- .../changesView/browser/changesViewActions.ts | 5 +- .../contrib/chat/browser/chat.contribution.ts | 27 +------ .../contrib/chat/browser/runScriptAction.ts | 4 +- .../browser/media/sessionsTitleBarWidget.css | 27 +------ .../browser/sessionsTitleBarWidget.ts | 16 +--- .../browser/sessionsTerminalContribution.ts | 4 +- .../test/browser/layoutActions.test.ts | 2 +- 12 files changed, 104 insertions(+), 90 deletions(-) diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index cd1f5880504..c9bf983b7c9 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -52,7 +52,7 @@ class ToggleSidebarVisibilityAction extends Action2 { }, menu: [ { - id: Menus.TitleBarLeft, + id: Menus.TitleBarLeftLayout, group: 'navigation', order: 0, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) @@ -104,7 +104,7 @@ class ToggleSecondarySidebarVisibilityAction extends Action2 { f1: true, menu: [ { - id: Menus.TitleBarRight, + id: Menus.TitleBarRightLayout, group: 'navigation', order: 10, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) @@ -165,7 +165,7 @@ registerAction2(ToggleSecondarySidebarVisibilityAction); registerAction2(TogglePanelVisibilityAction); // Floating window controls: always-on-top -MenuRegistry.appendMenuItem(Menus.TitleBarRight, { +MenuRegistry.appendMenuItem(Menus.TitleBarRightLayout, { command: { id: 'workbench.action.toggleWindowAlwaysOnTop', title: localize('toggleWindowAlwaysOnTop', "Toggle Always on Top"), diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index 74ebe982e7d..c322fba968b 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -13,11 +13,10 @@ export const Menus = { CommandCenter: new MenuId('SessionsCommandCenter'), CommandCenterCenter: new MenuId('SessionsCommandCenterCenter'), TitleBarContext: new MenuId('SessionsTitleBarContext'), - TitleBarControlMenu: new MenuId('SessionsTitleBarControlMenu'), - TitleBarLeft: new MenuId('SessionsTitleBarLeft'), - TitleBarCenter: new MenuId('SessionsTitleBarCenter'), - TitleBarRight: new MenuId('SessionsTitleBarRight'), - OpenSubMenu: new MenuId('SessionsOpenSubMenu'), + TitleBarLeftLayout: new MenuId('SessionsTitleBarLeftLayout'), + TitleBarSessionTitle: new MenuId('SessionsTitleBarSessionTitle'), + TitleBarSessionMenu: new MenuId('SessionsTitleBarSessionMenu'), + TitleBarRightLayout: new MenuId('SessionsTitleBarRightLayout'), PanelTitle: new MenuId('SessionsPanelTitle'), SidebarTitle: new MenuId('SessionsSidebarTitle'), AuxiliaryBarTitle: new MenuId('SessionsAuxiliaryBarTitle'), @@ -25,5 +24,4 @@ export const Menus = { SidebarFooter: new MenuId('SessionsSidebarFooter'), SidebarCustomizations: new MenuId('SessionsSidebarCustomizations'), AgentFeedbackEditorContent: new MenuId('AgentFeedbackEditorContent'), - SessionTitleActions: new MenuId('SessionTitleActions'), } as const; diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index 6aab26d2631..f2f5d1ff68f 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -8,10 +8,73 @@ height: 100%; align-items: center; order: 0; - flex-grow: 2; + flex-grow: 0; + flex-shrink: 0; + width: auto; justify-content: flex-start; } +.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-center { + order: 1; + width: auto; + flex-grow: 0; + flex-shrink: 1; + min-width: 0px; + margin: 0; + justify-content: flex-start; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left { + width: fit-content; + flex-grow: 0; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center { + flex: 1; + max-width: none; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center .window-title { + margin: unset; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right { + order: 2; + width: fit-content; + flex-grow: 0; + justify-content: flex-end; + margin-right: 10px; +} + +/* Session Title Actions Container (before right toolbar) */ +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container { + display: none; + flex-shrink: 0; + -webkit-app-region: no-drag; + height: 100%; +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container:not(.has-no-actions) { + display: flex; + align-items: center; +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container:not(.has-no-actions):not(:last-child)::after { + content: ''; + width: 1px; + height: 16px; + margin: 0 4px; + background-color: var(--vscode-disabledForeground); +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container .codicon { + color: inherit; +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container .monaco-action-bar .action-item { + display: flex; +} + /* Left Tool Bar Container */ .monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { display: none; @@ -40,11 +103,8 @@ display: flex; } -/* TODO: Hack to avoid flicker when sidebar becomes visible. - * The contribution swaps the menu item synchronously, but the toolbar - * re-render is async, causing a brief flash. Hide the container via - * CSS when sidebar is visible (nosidebar class is removed synchronously). */ -.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { +/* Hide the entire titlebar left when the sidebar is visible */ +.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left { display: none !important; } @@ -52,7 +112,3 @@ .agent-sessions-workbench.mac .part.titlebar .window-controls-container { -webkit-app-region: drag; } - -.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left .window-controls-container { - display: none !important; -} diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index b9132d4d13e..75eb74869dd 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -120,7 +120,7 @@ export class SidebarPart extends AbstractPaneCompositePart { ViewContainerLocation.Sidebar, Extensions.Viewlets, Menus.SidebarTitle, - Menus.TitleBarLeft, + Menus.TitleBarLeftLayout, notificationService, storageService, contextMenuService, diff --git a/src/vs/sessions/browser/parts/titlebarPart.ts b/src/vs/sessions/browser/parts/titlebarPart.ts index fa99c2e21e3..18c2d2867f0 100644 --- a/src/vs/sessions/browser/parts/titlebarPart.ts +++ b/src/vs/sessions/browser/parts/titlebarPart.ts @@ -185,7 +185,7 @@ export class TitlebarPart extends Part implements ITitlebarPart { // Left toolbar (driven by Menus.TitleBarLeft, rendered after window controls via CSS order) const leftToolbarContainer = append(this.leftContent, $('div.left-toolbar-container')); - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, leftToolbarContainer, Menus.TitleBarLeft, { + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, leftToolbarContainer, Menus.TitleBarLeftLayout, { contextMenu: Menus.TitleBarContext, telemetrySource: 'titlePart.left', hiddenItemStrategy: HiddenItemStrategy.NoHide, @@ -204,13 +204,22 @@ export class TitlebarPart extends Part implements ITitlebarPart { })); // Right toolbar (driven by Menus.TitleBarRight - includes account submenu) - const rightToolbarContainer = prepend(this.rightContent, $('div.action-toolbar-container')); - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRight, { + const rightToolbarContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-layout-actions-container')); + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRightLayout, { contextMenu: Menus.TitleBarContext, telemetrySource: 'titlePart.right', toolbarOptions: { primaryGroup: () => true }, })); + // Session title actions toolbar (before right toolbar) + const sessionActionsContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-session-actions-container')); + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, sessionActionsContainer, Menus.TitleBarSessionMenu, { + contextMenu: Menus.TitleBarContext, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + telemetrySource: 'titlePart.sessionActions', + toolbarOptions: { primaryGroup: () => true }, + })); + // Context menu on the titlebar this._register(addDisposableListener(this.rootContainer, EventType.CONTEXT_MENU, e => { EventHelper.stop(e); diff --git a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts b/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts index 1a06ef09fb5..4b348230ee0 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts @@ -35,7 +35,8 @@ const openChangesViewActionOptions: IAction2Options = { icon: Codicon.diffMultiple, f1: false, menu: { - id: Menus.SessionTitleActions, + id: Menus.TitleBarSessionMenu, + group: 'navigation', order: 1, when: ContextKeyExpr.equals(activeSessionHasChangesContextKey.key, true), }, @@ -158,7 +159,7 @@ class ChangesViewActionsContribution extends Disposable implements IWorkbenchCon ) { super(); - this._register(actionViewItemService.register(Menus.SessionTitleActions, OpenChangesViewAction.ID, (action, options) => { + this._register(actionViewItemService.register(Menus.TitleBarSessionMenu, OpenChangesViewAction.ID, (action, options) => { return instantiationService.createInstance(ChangesActionViewItem, action, options); })); diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 74b536c6947..c10d4b4cdc0 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -47,11 +47,11 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { id: OpenSessionWorktreeInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in VS Code'), icon: Codicon.vscodeInsiders, + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext), menu: [{ - id: Menus.TitleBarRight, + id: Menus.TitleBarSessionMenu, group: 'navigation', order: 10, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext) }] }); } @@ -92,29 +92,6 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { } registerAction2(OpenSessionWorktreeInVSCodeAction); -// Disabled placeholder shown in the titlebar when the active session does not support opening in VS Code -class OpenSessionWorktreeInVSCodeNotAvailableAction extends Action2 { - constructor() { - super({ - id: 'chat.openSessionWorktreeInVSCode.notAvailable', - title: localize2('openInVSCode', 'Open in VS Code'), - tooltip: localize('openInVSCodeNotAvailableTooltip', "Open in VS Code is not available for this session type"), - icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.false(), - menu: [{ - id: Menus.TitleBarRight, - group: 'navigation', - order: 10, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated()) - }] - }); - } - - override run(): void { } -} - -registerAction2(OpenSessionWorktreeInVSCodeNotAvailableAction); - class NewChatInSessionsWindowAction extends Action2 { constructor() { diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index e5c024cb2ba..fcb730a7b56 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -285,7 +285,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } // Register the Run split button submenu on the workbench title bar (background sessions only) -MenuRegistry.appendMenuItem(Menus.TitleBarRight, { +MenuRegistry.appendMenuItem(Menus.TitleBarSessionMenu, { submenu: RunScriptDropdownMenuId, isSplitButton: true, title: localize2('run', "Run"), @@ -305,7 +305,7 @@ class RunScriptNotAvailableAction extends Action2 { icon: Codicon.play, precondition: ContextKeyExpr.false(), menu: [{ - id: Menus.TitleBarRight, + id: Menus.TitleBarSessionMenu, group: 'navigation', order: 8, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated()) diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index 577c5a482f6..26b50d594d9 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -6,13 +6,11 @@ /* Container - button style hover */ .command-center .agent-sessions-titlebar-container { display: flex; - width: 38vw; - max-width: 600px; - display: flex; + width: 100%; flex-direction: row; flex-wrap: nowrap; align-items: center; - justify-content: center; + justify-content: flex-start; padding: 0 10px; height: 22px; border-radius: 4px; @@ -30,28 +28,13 @@ padding: 0 4px; border-radius: 4px; min-width: 0; + max-width: 600px; } .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-pill:hover { background-color: var(--vscode-toolbar-hoverBackground); } -/* Session title actions toolbar */ -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-actions { - display: flex; - align-items: center; - flex-shrink: 0; -} - -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-actions .actions-container { - height: auto; -} - -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-actions .action-item { - display: flex; - align-items: center; -} - .command-center .agent-sessions-titlebar-container:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; @@ -63,11 +46,9 @@ align-items: center; gap: 6px; min-width: 0; - justify-content: center; + justify-content: flex-start; cursor: pointer; } - -/* Kind icon */ .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-icon { display: flex; align-items: center; diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index c5d61de4e50..6ff448786dd 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -14,7 +14,7 @@ import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; + import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; @@ -179,14 +179,6 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { this._container.appendChild(sessionPill); - // Session title actions toolbar (rendered next to the session title) - const actionsContainer = $('span.agent-sessions-titlebar-actions'); - this._dynamicDisposables.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, Menus.SessionTitleActions, { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { primaryGroup: () => true }, - })); - this._container.appendChild(actionsContainer); - // Hover this._dynamicDisposables.add(this.hoverService.setupManagedHover( getDefaultHoverDelegate('mouse'), @@ -317,14 +309,14 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben // Register the submenu item in the Agent Sessions command center this._register(MenuRegistry.appendMenuItem(Menus.CommandCenter, { - submenu: Menus.TitleBarControlMenu, + submenu: Menus.TitleBarSessionTitle, title: localize('agentSessionsControl', "Agent Sessions"), order: 101, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.negate(), SessionsWelcomeVisibleContext.negate()) })); // Register a placeholder action so the submenu appears - this._register(MenuRegistry.appendMenuItem(Menus.TitleBarControlMenu, { + this._register(MenuRegistry.appendMenuItem(Menus.TitleBarSessionTitle, { command: { id: FocusAgentSessionsAction.id, title: localize('showSessions', "Show Sessions"), @@ -334,7 +326,7 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben when: IsAuxiliaryWindowContext.negate() })); - this._register(actionViewItemService.register(Menus.CommandCenter, Menus.TitleBarControlMenu, (action, options) => { + this._register(actionViewItemService.register(Menus.CommandCenter, Menus.TitleBarSessionTitle, (action, options) => { if (!(action instanceof SubmenuItemAction)) { return undefined; } diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 269d3a63a23..4696c11f97d 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -149,9 +149,9 @@ class OpenSessionInTerminalAction extends Action2 { title: localize2('openInTerminal', "Open Terminal"), icon: Codicon.terminal, menu: [{ - id: Menus.TitleBarRight, + id: Menus.TitleBarSessionMenu, group: 'navigation', - order: 11, + order: 9, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }] }); diff --git a/src/vs/sessions/test/browser/layoutActions.test.ts b/src/vs/sessions/test/browser/layoutActions.test.ts index 786236ec970..f8362f7d654 100644 --- a/src/vs/sessions/test/browser/layoutActions.test.ts +++ b/src/vs/sessions/test/browser/layoutActions.test.ts @@ -16,7 +16,7 @@ suite('Sessions - Layout Actions', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('always-on-top toggle action is contributed to TitleBarRight', () => { - const items = MenuRegistry.getMenuItems(Menus.TitleBarRight); + const items = MenuRegistry.getMenuItems(Menus.TitleBarRightLayout); const menuItems = items.filter(isIMenuItem); const toggleAlwaysOnTop = menuItems.find(item => item.command.id === 'workbench.action.toggleWindowAlwaysOnTop'); From 51f5cafd6f41af7652eece368ee64dd68bb49310 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 3 Mar 2026 12:40:44 +0100 Subject: [PATCH 038/448] Revert "Port github extension to use esbuild" (#298920) --- extensions/esbuild-extension-common.mts | 4 +- extensions/github/.vscodeignore | 2 +- ...sbuild.mts => extension.webpack.config.js} | 30 +++--- extensions/github/package.json | 3 +- extensions/github/src/branchProtection.ts | 2 +- extensions/github/src/canonicalUriProvider.ts | 2 +- extensions/github/src/commands.ts | 3 +- extensions/github/src/credentialProvider.ts | 2 +- extensions/github/src/extension.ts | 2 +- .../github/src/historyItemDetailsProvider.ts | 2 +- extensions/github/src/links.ts | 3 +- extensions/github/src/publish.ts | 2 +- extensions/github/src/pushErrorHandler.ts | 3 +- .../github/src/remoteSourcePublisher.ts | 2 +- extensions/github/src/shareProviders.ts | 2 +- .../github/src/typings/git.constants.ts | 98 ------------------- extensions/github/src/util.ts | 2 +- 17 files changed, 34 insertions(+), 130 deletions(-) rename extensions/github/{esbuild.mts => extension.webpack.config.js} (50%) delete mode 100644 extensions/github/src/typings/git.constants.ts diff --git a/extensions/esbuild-extension-common.mts b/extensions/esbuild-extension-common.mts index cc716f2ca6a..1c458e4bfe1 100644 --- a/extensions/esbuild-extension-common.mts +++ b/extensions/esbuild-extension-common.mts @@ -33,7 +33,6 @@ async function tryBuild(options: BuildOptions, didBuild?: (outDir: string) => un interface RunConfig { readonly platform: 'node' | 'browser'; - readonly format?: 'cjs' | 'esm'; readonly srcDir: string; readonly outdir: string; readonly entryPoints: string[] | Record | { in: string; out: string }[]; @@ -49,7 +48,6 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { sourcemap: true, target: ['es2024'], external: ['vscode'], - format: config.format ?? 'cjs', entryPoints: config.entryPoints, outdir, logOverride: { @@ -59,8 +57,10 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { }; if (config.platform === 'node') { + options.format = 'cjs'; options.mainFields = ['module', 'main']; } else if (config.platform === 'browser') { + options.format = 'cjs'; options.mainFields = ['browser', 'module', 'main']; options.alias = { 'path': 'path-browserify', diff --git a/extensions/github/.vscodeignore b/extensions/github/.vscodeignore index a6590bd3934..77ec048a6da 100644 --- a/extensions/github/.vscodeignore +++ b/extensions/github/.vscodeignore @@ -2,7 +2,7 @@ src/** !src/common/config.json out/** build/** -esbuild*.mts +extension.webpack.config.js tsconfig*.json package-lock.json testWorkspace/** diff --git a/extensions/github/esbuild.mts b/extensions/github/extension.webpack.config.js similarity index 50% rename from extensions/github/esbuild.mts rename to extensions/github/extension.webpack.config.js index f91916e622d..9e2b191a389 100644 --- a/extensions/github/esbuild.mts +++ b/extensions/github/extension.webpack.config.js @@ -2,18 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.mts'; +// @ts-check +import withDefaults from '../shared.webpack.config.mjs'; -const srcDir = path.join(import.meta.dirname, 'src'); -const outDir = path.join(import.meta.dirname, 'dist'); - -run({ - platform: 'node', - format: 'esm', - entryPoints: { - 'extension': path.join(srcDir, 'extension.ts'), +export default withDefaults({ + context: import.meta.dirname, + entry: { + extension: './src/extension.ts' }, - srcDir, - outdir: outDir, -}, process.argv); + output: { + libraryTarget: 'module', + chunkFormat: 'module', + }, + externals: { + 'vscode': 'module vscode', + }, + experiments: { + outputModule: true + } +}); diff --git a/extensions/github/package.json b/extensions/github/package.json index 42f408ac96b..bce90fe1812 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -19,7 +19,8 @@ "extensionDependencies": [ "vscode.git-base" ], - "main": "./dist/extension.js", + "main": "./out/extension.js", + "type": "module", "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { diff --git a/extensions/github/src/branchProtection.ts b/extensions/github/src/branchProtection.ts index 0c616d33905..040df24942a 100644 --- a/extensions/github/src/branchProtection.ts +++ b/extensions/github/src/branchProtection.ts @@ -6,7 +6,7 @@ import { EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode'; import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema'; import { AuthenticationError, OctokitService } from './auth.js'; -import type { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.d.ts'; +import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.js'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { TelemetryReporter } from '@vscode/extension-telemetry'; diff --git a/extensions/github/src/canonicalUriProvider.ts b/extensions/github/src/canonicalUriProvider.ts index 9218707ed26..0838c7377dd 100644 --- a/extensions/github/src/canonicalUriProvider.ts +++ b/extensions/github/src/canonicalUriProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode'; -import type { API } from './typings/git.d.ts'; +import { API } from './typings/git.js'; const SUPPORTED_SCHEMES = ['ssh', 'https', 'file']; diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 4a1d1c10ce8..33acf5a406b 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { RefType } from './typings/git.constants.js'; -import type { API as GitAPI, Repository } from './typings/git.d.ts'; +import { API as GitAPI, RefType, Repository } from './typings/git.js'; import { publishRepository } from './publish.js'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js'; diff --git a/extensions/github/src/credentialProvider.ts b/extensions/github/src/credentialProvider.ts index 4964724eed6..d184960c23b 100644 --- a/extensions/github/src/credentialProvider.ts +++ b/extensions/github/src/credentialProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.d.ts'; +import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.js'; import { workspace, Uri, Disposable } from 'vscode'; import { getSession } from './auth.js'; diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index e6a44f516ac..17906c57d44 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -6,7 +6,7 @@ import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode'; import { TelemetryReporter } from '@vscode/extension-telemetry'; import { GithubRemoteSourceProvider } from './remoteSourceProvider.js'; -import type { API, GitExtension } from './typings/git.d.ts'; +import { API, GitExtension } from './typings/git.js'; import { registerCommands } from './commands.js'; import { GithubCredentialProviderManager } from './credentialProvider.js'; import { DisposableStore, repositoryHasGitHubRemote } from './util.js'; diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index d0a145ec9f2..9a267b9e844 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -5,7 +5,7 @@ import { Command, l10n, LogOutputChannel, workspace } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; -import type { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.d.ts'; +import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.js'; import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util.js'; import { AuthenticationError, OctokitService } from './auth.js'; import { getAvatarLink } from './links.js'; diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index fbdde106149..b4f8379e5f7 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { RefType } from './typings/git.constants.js'; -import type { API as GitAPI, Repository } from './typings/git.d.ts'; +import { API as GitAPI, RefType, Repository } from './typings/git.js'; import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean { diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index dab81037d59..618f7527450 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { API as GitAPI, Repository } from './typings/git.d.ts'; +import { API as GitAPI, Repository } from './typings/git.js'; import { getOctokit } from './auth.js'; import { TextEncoder } from 'util'; import { basename } from 'path'; diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index 751654515f9..f7b0b9ef869 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -6,8 +6,7 @@ import { TextDecoder } from 'util'; import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode'; import { getOctokit } from './auth.js'; -import { GitErrorCodes } from './typings/git.constants.js'; -import type { PushErrorHandler, Remote, Repository } from './typings/git.d.ts'; +import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git.js'; import * as path from 'path'; import { TelemetryReporter } from '@vscode/extension-telemetry'; diff --git a/extensions/github/src/remoteSourcePublisher.ts b/extensions/github/src/remoteSourcePublisher.ts index 67c1e567e36..97ce05a835c 100644 --- a/extensions/github/src/remoteSourcePublisher.ts +++ b/extensions/github/src/remoteSourcePublisher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { publishRepository } from './publish.js'; -import type { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.d.ts'; +import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.js'; export class GithubRemoteSourcePublisher implements RemoteSourcePublisher { readonly name = 'GitHub'; diff --git a/extensions/github/src/shareProviders.ts b/extensions/github/src/shareProviders.ts index a52cf84d704..d2e94a47147 100644 --- a/extensions/github/src/shareProviders.ts +++ b/extensions/github/src/shareProviders.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { API } from './typings/git.d.ts'; +import { API } from './typings/git.js'; import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links.js'; diff --git a/extensions/github/src/typings/git.constants.ts b/extensions/github/src/typings/git.constants.ts deleted file mode 100644 index 5847e21d5d0..00000000000 --- a/extensions/github/src/typings/git.constants.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type * as git from './git'; - -export type ForcePushMode = git.ForcePushMode; -export type RefType = git.RefType; -export type Status = git.Status; -export type GitErrorCodes = git.GitErrorCodes; - -export const ForcePushMode = Object.freeze({ - Force: 0, - ForceWithLease: 1, - ForceWithLeaseIfIncludes: 2, -}) satisfies typeof git.ForcePushMode; - -export const RefType = Object.freeze({ - Head: 0, - RemoteHead: 1, - Tag: 2, -}) satisfies typeof git.RefType; - -export const Status = Object.freeze({ - INDEX_MODIFIED: 0, - INDEX_ADDED: 1, - INDEX_DELETED: 2, - INDEX_RENAMED: 3, - INDEX_COPIED: 4, - - MODIFIED: 5, - DELETED: 6, - UNTRACKED: 7, - IGNORED: 8, - INTENT_TO_ADD: 9, - INTENT_TO_RENAME: 10, - TYPE_CHANGED: 11, - - ADDED_BY_US: 12, - ADDED_BY_THEM: 13, - DELETED_BY_US: 14, - DELETED_BY_THEM: 15, - BOTH_ADDED: 16, - BOTH_DELETED: 17, - BOTH_MODIFIED: 18, -}) satisfies typeof git.Status; - -export const GitErrorCodes = Object.freeze({ - BadConfigFile: 'BadConfigFile', - BadRevision: 'BadRevision', - AuthenticationFailed: 'AuthenticationFailed', - NoUserNameConfigured: 'NoUserNameConfigured', - NoUserEmailConfigured: 'NoUserEmailConfigured', - NoRemoteRepositorySpecified: 'NoRemoteRepositorySpecified', - NotAGitRepository: 'NotAGitRepository', - NotASafeGitRepository: 'NotASafeGitRepository', - NotAtRepositoryRoot: 'NotAtRepositoryRoot', - Conflict: 'Conflict', - StashConflict: 'StashConflict', - UnmergedChanges: 'UnmergedChanges', - PushRejected: 'PushRejected', - ForcePushWithLeaseRejected: 'ForcePushWithLeaseRejected', - ForcePushWithLeaseIfIncludesRejected: 'ForcePushWithLeaseIfIncludesRejected', - RemoteConnectionError: 'RemoteConnectionError', - DirtyWorkTree: 'DirtyWorkTree', - CantOpenResource: 'CantOpenResource', - GitNotFound: 'GitNotFound', - CantCreatePipe: 'CantCreatePipe', - PermissionDenied: 'PermissionDenied', - CantAccessRemote: 'CantAccessRemote', - RepositoryNotFound: 'RepositoryNotFound', - RepositoryIsLocked: 'RepositoryIsLocked', - BranchNotFullyMerged: 'BranchNotFullyMerged', - NoRemoteReference: 'NoRemoteReference', - InvalidBranchName: 'InvalidBranchName', - BranchAlreadyExists: 'BranchAlreadyExists', - NoLocalChanges: 'NoLocalChanges', - NoStashFound: 'NoStashFound', - LocalChangesOverwritten: 'LocalChangesOverwritten', - NoUpstreamBranch: 'NoUpstreamBranch', - IsInSubmodule: 'IsInSubmodule', - WrongCase: 'WrongCase', - CantLockRef: 'CantLockRef', - CantRebaseMultipleBranches: 'CantRebaseMultipleBranches', - PatchDoesNotApply: 'PatchDoesNotApply', - NoPathFound: 'NoPathFound', - UnknownPath: 'UnknownPath', - EmptyCommitMessage: 'EmptyCommitMessage', - BranchFastForwardRejected: 'BranchFastForwardRejected', - BranchNotYetBorn: 'BranchNotYetBorn', - TagConflict: 'TagConflict', - CherryPickEmpty: 'CherryPickEmpty', - CherryPickConflict: 'CherryPickConflict', - WorktreeContainsChanges: 'WorktreeContainsChanges', - WorktreeAlreadyExists: 'WorktreeAlreadyExists', - WorktreeBranchAlreadyUsed: 'WorktreeBranchAlreadyUsed', -}) satisfies Record; diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index bcdddaed6e5..2247292dd93 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { Repository } from './typings/git.d.ts'; +import { Repository } from './typings/git.js'; export class DisposableStore { From f8fab139b23839f1b6dbfdbaf859d53a8e2e2277 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 3 Mar 2026 12:44:37 +0100 Subject: [PATCH 039/448] Hiding and showing of terminals --- .../browser/sessionsTerminalContribution.ts | 106 +++++++++++---- .../sessionsTerminalContribution.test.ts | 121 ++++++++++++++++++ .../contrib/terminal/browser/terminal.ts | 6 + .../terminal/browser/terminalService.ts | 39 ++++++ 4 files changed, 250 insertions(+), 22 deletions(-) diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 269d3a63a23..903961e8943 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -44,9 +44,10 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben static readonly ID = 'workbench.contrib.sessionsTerminal'; - /** Maps worktree/repository fsPath (lower-cased) to the terminal instance id. */ - private readonly _pathToInstanceId = new Map(); - private _lastTargetFsPath: string | undefined; + /** Maps worktree/repository fsPath (lower-cased) to terminal instance ids. */ + private readonly _pathToInstanceIds = new Map>(); + private _activeKey: string | undefined; + private _isCreatingTerminal = false; constructor( @ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService, @@ -75,13 +76,24 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben // Clean up mapping when terminals are disposed this._register(this._terminalService.onDidDisposeInstance(instance => { - for (const [path, id] of this._pathToInstanceId) { - if (id === instance.instanceId) { - this._pathToInstanceId.delete(path); - break; + for (const [path, ids] of this._pathToInstanceIds) { + if (ids.delete(instance.instanceId) && ids.size === 0) { + this._pathToInstanceIds.delete(path); } } })); + + // When terminals are restored on startup, ensure visibility matches active session + this._register(this._terminalService.onDidCreateInstance(instance => { + if (this._isCreatingTerminal || this._activeKey === undefined) { + return; + } + // If this instance is not tracked by us, hide it + const activeIds = this._pathToInstanceIds.get(this._activeKey); + if (!activeIds?.has(instance.instanceId)) { + this._terminalService.moveToBackground(instance); + } + })); } /** @@ -91,16 +103,23 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben */ async ensureTerminal(cwd: URI, focus: boolean): Promise { const key = cwd.fsPath.toLowerCase(); - const existingId = this._pathToInstanceId.get(key); + const ids = this._pathToInstanceIds.get(key); + const existingId = ids ? ids.values().next().value : undefined; const existing = existingId !== undefined ? this._terminalService.getInstanceFromId(existingId) : undefined; if (existing) { + await this._terminalService.showBackgroundTerminal(existing); this._terminalService.setActiveInstance(existing); } else { - const instance = await this._terminalService.createTerminal({ config: { cwd } }); - this._pathToInstanceId.set(key, instance.instanceId); - this._terminalService.setActiveInstance(instance); - this._logService.trace(`[SessionsTerminal] Created terminal ${instance.instanceId} for ${cwd.fsPath}`); + this._isCreatingTerminal = true; + try { + const instance = await this._terminalService.createTerminal({ config: { cwd } }); + this._addInstanceToPath(key, instance.instanceId); + this._terminalService.setActiveInstance(instance); + this._logService.trace(`[SessionsTerminal] Created terminal ${instance.instanceId} for ${cwd.fsPath}`); + } finally { + this._isCreatingTerminal = false; + } } if (focus) { @@ -116,25 +135,68 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben const sessionCwd = getSessionCwd(session); const targetPath = sessionCwd ?? await this._pathService.userHome(); - const targetFsPath = targetPath.fsPath; - if (this._lastTargetFsPath?.toLowerCase() === targetFsPath.toLowerCase()) { + const targetKey = targetPath.fsPath.toLowerCase(); + if (this._activeKey === targetKey) { return; } - this._lastTargetFsPath = targetFsPath; + this._activeKey = targetKey; await this.ensureTerminal(targetPath, false); + + // If the active key changed while we were awaiting, a newer call has + // taken over — skip the visibility update to avoid flicker. + if (this._activeKey !== targetKey) { + return; + } + this._updateTerminalVisibility(targetKey); + } + + private _addInstanceToPath(key: string, instanceId: number): void { + let ids = this._pathToInstanceIds.get(key); + if (!ids) { + ids = new Set(); + this._pathToInstanceIds.set(key, ids); + } + ids.add(instanceId); + } + + /** + * Hides all foreground terminals that do not belong to the given active key + * and shows all background terminals that do belong to it. + */ + private _updateTerminalVisibility(activeKey: string): void { + const activeIds = this._pathToInstanceIds.get(activeKey); + + // Hide foreground terminals not belonging to the active session + for (const instance of [...this._terminalService.foregroundInstances]) { + if (!activeIds?.has(instance.instanceId)) { + this._terminalService.moveToBackground(instance); + } + } + + // Show background terminals belonging to the active session + if (activeIds) { + for (const id of activeIds) { + const instance = this._terminalService.getInstanceFromId(id); + if (instance && !this._terminalService.foregroundInstances.includes(instance)) { + this._terminalService.showBackgroundTerminal(instance, true); + } + } + } } private _closeTerminalsForPath(fsPath: string): void { const key = fsPath.toLowerCase(); - const instanceId = this._pathToInstanceId.get(key); - if (instanceId !== undefined) { - const instance = this._terminalService.getInstanceFromId(instanceId); - if (instance) { - this._terminalService.safeDisposeTerminal(instance); - this._logService.trace(`[SessionsTerminal] Closed archived terminal ${instanceId}`); + const ids = this._pathToInstanceIds.get(key); + if (ids) { + for (const instanceId of ids) { + const instance = this._terminalService.getInstanceFromId(instanceId); + if (instance) { + this._terminalService.safeDisposeTerminal(instance); + this._logService.trace(`[SessionsTerminal] Closed archived terminal ${instanceId}`); + } } - this._pathToInstanceId.delete(key); + this._pathToInstanceIds.delete(key); } } } diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index 5cb061bb85e..c2108711f80 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -59,12 +59,16 @@ suite('SessionsTerminalContribution', () => { let onDidChangeSessionArchivedState: Emitter; let onDidDisposeInstance: Emitter; + let onDidCreateInstance: Emitter; let createdTerminals: { cwd: URI }[]; let activeInstanceSet: number[]; let focusCalls: number; let disposedInstances: ITerminalInstance[]; let nextInstanceId: number; let terminalInstances: Map; + let backgroundedInstances: Set; + let moveToBackgroundCalls: number[]; + let showBackgroundCalls: number[]; setup(() => { createdTerminals = []; @@ -73,12 +77,16 @@ suite('SessionsTerminalContribution', () => { disposedInstances = []; nextInstanceId = 1; terminalInstances = new Map(); + backgroundedInstances = new Set(); + moveToBackgroundCalls = []; + showBackgroundCalls = []; const instantiationService = store.add(new TestInstantiationService()); activeSessionObs = observableValue('activeSession', undefined); onDidChangeSessionArchivedState = store.add(new Emitter()); onDidDisposeInstance = store.add(new Emitter()); + onDidCreateInstance = store.add(new Emitter()); instantiationService.stub(ILogService, new NullLogService()); @@ -88,11 +96,16 @@ suite('SessionsTerminalContribution', () => { instantiationService.stub(ITerminalService, new class extends mock() { override onDidDisposeInstance = onDidDisposeInstance.event; + override onDidCreateInstance = onDidCreateInstance.event; + override get foregroundInstances(): readonly ITerminalInstance[] { + return [...terminalInstances.values()].filter(i => !backgroundedInstances.has(i.instanceId)); + } override async createTerminal(opts?: any): Promise { const id = nextInstanceId++; const instance = { instanceId: id } as ITerminalInstance; createdTerminals.push({ cwd: opts?.config?.cwd }); terminalInstances.set(id, instance); + onDidCreateInstance.fire(instance); return instance; } override getInstanceFromId(id: number): ITerminalInstance | undefined { @@ -107,6 +120,15 @@ suite('SessionsTerminalContribution', () => { override async safeDisposeTerminal(instance: ITerminalInstance): Promise { disposedInstances.push(instance); terminalInstances.delete(instance.instanceId); + backgroundedInstances.delete(instance.instanceId); + } + override moveToBackground(instance: ITerminalInstance): void { + backgroundedInstances.add(instance.instanceId); + moveToBackgroundCalls.push(instance.instanceId); + } + override async showBackgroundTerminal(instance: ITerminalInstance): Promise { + backgroundedInstances.delete(instance.instanceId); + showBackgroundCalls.push(instance.instanceId); } }); @@ -346,6 +368,105 @@ suite('SessionsTerminalContribution', () => { await tick(); assert.strictEqual(createdTerminals.length, 2, 'should reuse the terminal for cwd1'); }); + + // --- Terminal visibility management --- + + test('hides terminals from previous session when switching to a new session', async () => { + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + const firstTerminalId = createdTerminals.length; + assert.strictEqual(firstTerminalId, 1); + + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // The first terminal (id=1) should have been moved to background + assert.ok(moveToBackgroundCalls.includes(1), 'terminal for cwd1 should be backgrounded'); + assert.ok(backgroundedInstances.has(1), 'terminal for cwd1 should remain backgrounded'); + }); + + test('shows previously hidden terminals when switching back to their session', async () => { + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Switch back to cwd1 + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Terminal for cwd1 (id=1) should be shown again + assert.ok(showBackgroundCalls.includes(1), 'terminal for cwd1 should be shown'); + assert.ok(!backgroundedInstances.has(1), 'terminal for cwd1 should be foreground'); + // Terminal for cwd2 (id=2) should now be backgrounded + assert.ok(backgroundedInstances.has(2), 'terminal for cwd2 should be backgrounded'); + }); + + test('only terminals of the active session are visible after multiple switches', async () => { + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + const cwd3 = URI.file('/cwd3'); + + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + activeSessionObs.set(makeAgentSession({ worktree: cwd3, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Only terminal for cwd3 (id=3) should be foreground + assert.ok(backgroundedInstances.has(1), 'terminal for cwd1 should be backgrounded'); + assert.ok(backgroundedInstances.has(2), 'terminal for cwd2 should be backgrounded'); + assert.ok(!backgroundedInstances.has(3), 'terminal for cwd3 should be foreground'); + }); + + test('hides restored terminals that do not belong to the active session', async () => { + // Set an active session first + const cwd1 = URI.file('/cwd1'); + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Simulate a terminal being restored (e.g. on startup) that is not tracked + const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + terminalInstances.set(restoredInstance.instanceId, restoredInstance); + onDidCreateInstance.fire(restoredInstance); + + // The restored terminal should be moved to background + assert.ok(moveToBackgroundCalls.includes(restoredInstance.instanceId), 'restored terminal should be backgrounded'); + }); + + test('does not hide restored terminals before any session is active', async () => { + // Simulate a terminal being restored before any session is active + const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + terminalInstances.set(restoredInstance.instanceId, restoredInstance); + onDidCreateInstance.fire(restoredInstance); + + assert.strictEqual(moveToBackgroundCalls.length, 0, 'should not background before any session is active'); + }); + + test('ensureTerminal shows a backgrounded terminal instead of creating a new one', async () => { + const cwd = URI.file('/test-cwd'); + await contribution.ensureTerminal(cwd, false); + const instanceId = activeInstanceSet[0]; + + // Manually background it + backgroundedInstances.add(instanceId); + + // ensureTerminal should show it, not create a new one + await contribution.ensureTerminal(cwd, false); + + assert.strictEqual(createdTerminals.length, 1, 'should not create a new terminal'); + assert.ok(showBackgroundCalls.includes(instanceId), 'should show the backgrounded terminal'); + }); }); function tick(): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index a730051c4e7..b54f86b3ad3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -520,6 +520,12 @@ export interface ITerminalService extends ITerminalInstanceHost { * @param forceSaveState Used when the window is shutting down and we need to reveal and save hideFromUser terminals */ showBackgroundTerminal(instance: ITerminalInstance, suppressSetActive?: boolean): Promise; + /** + * Moves a visible terminal instance to the background. The terminal process + * remains alive but the instance is removed from its group/editor and tracked + * internally so it can later be shown again via {@link showBackgroundTerminal}. + */ + moveToBackground(instance: ITerminalInstance): void; revealActiveTerminal(preserveFocus?: boolean): Promise; moveToEditor(source: ITerminalInstance, group?: GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): void; moveIntoNewEditor(source: ITerminalInstance): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 6107058afad..1524e37ad3e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1233,6 +1233,45 @@ export class TerminalService extends Disposable implements ITerminalService { } } + moveToBackground(instance: ITerminalInstance): void { + // Already backgrounded + if (this._backgroundedTerminalInstances.some(bg => bg.instance === instance)) { + return; + } + + // Remove from its current location (panel group or editor) + if (instance.target === TerminalLocation.Editor) { + this._terminalEditorService.detachInstance(instance); + } else { + const group = this._terminalGroupService.getGroupForInstance(instance); + if (!group) { + return; + } + group.removeInstance(instance); + } + + instance.detachFromElement(); + + // Track in background + this._backgroundedTerminalInstances.push({ instance, terminalLocationOptions: instance.target === TerminalLocation.Editor ? { viewColumn: ACTIVE_GROUP } : undefined }); + this._backgroundedTerminalDisposables.set(instance.instanceId, [ + instance.onDisposed(instance => { + const idx = this._backgroundedTerminalInstances.findIndex(bg => bg.instance === instance); + if (idx !== -1) { + this._backgroundedTerminalInstances.splice(idx, 1); + } + const disposables = this._backgroundedTerminalDisposables.get(instance.instanceId); + if (disposables) { + dispose(disposables); + } + this._backgroundedTerminalDisposables.delete(instance.instanceId); + this._onDidDisposeInstance.fire(instance); + }) + ]); + + this._onDidChangeInstances.fire(); + } + public async showBackgroundTerminal(instance: ITerminalInstance, suppressSetActive?: boolean): Promise { const index = this._backgroundedTerminalInstances.findIndex(bg => bg.instance === instance); if (index === -1) { From 51223b6fb519e6522ecf2dd5936998a06e5e0ed6 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:44:47 +0100 Subject: [PATCH 040/448] Enhance task entry interface and add tests for command arguments (#298935) * Enhance task entry interface to support command arguments for different platforms * Add tests for runTask to validate command and argument handling --- .../browser/sessionsConfigurationService.ts | 48 ++++++++++++++----- .../sessionsConfigurationService.test.ts | 48 +++++++++++++++++++ 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts index f1a859d4aad..a36bed73da5 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts @@ -30,10 +30,11 @@ export interface ITaskEntry { readonly script?: string; readonly type?: string; readonly command?: string; + readonly args?: CommandString[]; readonly inSessions?: boolean; - readonly windows?: { command?: string }; - readonly osx?: { command?: string }; - readonly linux?: { command?: string }; + readonly windows?: { command?: string; args?: CommandString[] }; + readonly osx?: { command?: string; args?: CommandString[] }; + readonly linux?: { command?: string; args?: CommandString[] }; readonly [key: string]: unknown; } @@ -293,21 +294,42 @@ export class SessionsConfigurationService extends Disposable implements ISession if (!task.script) { return undefined; } - if (task.path) { - return `npm --prefix ${task.path} run ${task.script}`; - } - return `npm run ${task.script}`; + const base = task.path + ? `npm --prefix ${task.path} run ${task.script}` + : `npm run ${task.script}`; + return this._appendArgs(base, task.args); } + + let command: string | undefined; + let platformArgs: CommandString[] | undefined; + if (isWindows && task.windows?.command) { - return task.windows.command; + command = task.windows.command; + platformArgs = task.windows.args; + } else if (isMacintosh && task.osx?.command) { + command = task.osx.command; + platformArgs = task.osx.args; + } else if (!isWindows && !isMacintosh && task.linux?.command) { + command = task.linux.command; + platformArgs = task.linux.args; + } else { + command = task.command; } - if (isMacintosh && task.osx?.command) { - return task.osx.command; + + // Platform-specific args override task-level args + const args = platformArgs ?? task.args; + return this._appendArgs(command, args); + } + + private _appendArgs(command: string | undefined, args: CommandString[] | undefined): string | undefined { + if (!command) { + return undefined; } - if (!isWindows && !isMacintosh && task.linux?.command) { - return task.linux.command; + if (!args || args.length === 0) { + return command; } - return task.command; + const resolvedArgs = args.map(a => CommandString.value(a)).join(' '); + return `${command} ${resolvedArgs}`; } private _ensureFileWatch(folder: URI): void { diff --git a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts index f1f2706807a..d127cff33e9 100644 --- a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts @@ -396,6 +396,54 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(createdTerminals[1].name, 'test'); }); + test('runTask appends args to shell command', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { label: 'build', type: 'shell', command: 'dotnet', args: ['build', '--configuration', 'Release'], inSessions: true }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'dotnet build --configuration Release'); + }); + + test('runTask appends args to npm task', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { label: 'test', type: 'npm', script: 'test', args: ['--', '--coverage'], inSessions: true }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'npm run test -- --coverage'); + }); + + test('runTask resolves CommandString objects in args', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { + label: 'build', type: 'shell', command: 'gcc', + args: [ + { value: '-o', quoting: 'escape' as const }, + 'output.exe', + 'main.c', + ], + inSessions: true + }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'gcc -o output.exe main.c'); + }); + + test('runTask sends only command when args is empty', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { label: 'build', type: 'shell', command: 'make', args: [], inSessions: true }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'make'); + }); + test('runTask creates different terminals for same command in different worktrees', async () => { const wt1 = URI.parse('file:///worktree1'); const wt2 = URI.parse('file:///worktree2'); From 8e35f3edc6b71ffeb9b22ef2136af797a1a49731 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:45:42 +0100 Subject: [PATCH 041/448] Agent sessions approval row (#298933) * Initial plan * fix: update test expectation for legacy terminal tool data to expect 'sh' language id Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --- .../browser/agentSessions/agentSessionApprovalModel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts index 945c6cd2f72..321bac437d2 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts @@ -471,7 +471,7 @@ suite('AgentSessionApprovalModel', () => { language: result?.languageId, }, { label: 'legacy-cmd', - language: 'bash', + language: 'sh', }); }); From b02a0fd311db5a94d294c5abc88e77144b2bbb55 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 11:48:31 +0000 Subject: [PATCH 042/448] refactor: enhance box-shadow styling for menu container based on shadow color Co-authored-by: Copilot --- src/vs/base/browser/ui/menu/menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index ec712c7cf87..0b6afe0e2eb 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -1239,7 +1239,7 @@ ${formatRule(Codicon.menuSubmenu)} border: none; animation: fadeIn 0.083s linear; -webkit-app-region: no-drag; - box-shadow: var(--vscode-shadow-lg); + box-shadow: var(--vscode-shadow-lg${style.shadowColor ? `, 0 0 12px ${style.shadowColor}` : ''}); } .context-view.monaco-menu-container :focus, From 3205ec39c097c6f537bcd62ed6902777d842fa6f Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 11:50:47 +0000 Subject: [PATCH 043/448] refactor: add box-shadow styling to rename widget for improved visibility --- src/vs/editor/contrib/rename/browser/renameWidget.css | 2 +- src/vs/editor/contrib/rename/browser/renameWidget.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.css b/src/vs/editor/contrib/rename/browser/renameWidget.css index b68e37efee9..730bf8895b8 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.css +++ b/src/vs/editor/contrib/rename/browser/renameWidget.css @@ -7,11 +7,11 @@ z-index: 100; color: inherit; border-radius: 4px; + box-shadow: var(--vscode-shadow-hover); } .monaco-editor .rename-box.preview { padding: 4px 4px 0 4px; - box-shadow: var(--vscode-shadow-hover); } .monaco-editor .rename-box .rename-input-with-button { diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index efd2fc16232..b7e32811605 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -41,7 +41,8 @@ import { inputForeground, quickInputListFocusBackground, quickInputListFocusForeground, - widgetBorder + widgetBorder, + widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { HoverStyle } from '../../../../base/browser/ui/hover/hover.js'; @@ -242,8 +243,10 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable return; } + const widgetShadowColor = theme.getColor(widgetShadow); const widgetBorderColor = theme.getColor(widgetBorder); this._domNode.style.backgroundColor = String(theme.getColor(editorWidgetBackground) ?? ''); + this._domNode.style.boxShadow = widgetShadowColor ? ` 0 0 8px 2px ${widgetShadowColor}` : ''; this._domNode.style.border = widgetBorderColor ? `1px solid ${widgetBorderColor}` : ''; this._domNode.style.color = String(theme.getColor(inputForeground) ?? ''); From cd41bf1fe189aab9cd880fafc722e591ea54dd12 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 3 Mar 2026 12:30:21 +0100 Subject: [PATCH 044/448] Updates component explorer --- .github/skills/component-fixtures/SKILL.md | 2 +- build/vite/package-lock.json | 16 +++---- build/vite/package.json | 4 +- package-lock.json | 16 +++---- package.json | 4 +- .../test/browser/updateWidget.fixture.ts | 13 +++++- .../componentFixtures/aiStats.fixture.ts | 4 +- .../componentFixtures/baseUI.fixture.ts | 8 ++++ .../chatProgressContentPart.fixture.ts | 7 +++- .../chatQuestionCarousel.fixture.ts | 9 +++- .../codeActionList.fixture.ts | 4 +- .../componentFixtures/codeEditor.fixture.ts | 3 +- .../componentFixtures/findWidget.fixture.ts | 4 +- .../browser/componentFixtures/fixtureUtils.ts | 42 +++++++++++++++++-- .../inlineCompletions.fixture.ts | 5 ++- .../inlineCompletionsExtras.fixture.ts | 6 ++- .../promptFilePickers.fixture.ts | 4 +- .../componentFixtures/renameWidget.fixture.ts | 4 +- .../suggestWidget.fixture.ts | 4 +- 19 files changed, 122 insertions(+), 37 deletions(-) diff --git a/.github/skills/component-fixtures/SKILL.md b/.github/skills/component-fixtures/SKILL.md index ec2df9d4e92..6c7eb5a6059 100644 --- a/.github/skills/component-fixtures/SKILL.md +++ b/.github/skills/component-fixtures/SKILL.md @@ -30,7 +30,7 @@ src/vs/workbench/test/browser/componentFixtures/ ```typescript import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'myFeature/' }, { Default: defineComponentFixture({ render: renderMyComponent }), AnotherVariant: defineComponentFixture({ render: renderMyComponent }), }); diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index de462673a18..b7e27044aef 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,8 +8,8 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-vite-plugin": "^0.1.1-16", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-vite-plugin": "^0.1.1-19", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" @@ -683,9 +683,9 @@ "license": "MIT" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-16", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-16.tgz", - "integrity": "sha512-is1RxdlNO5K1RSqWd5z8BN6gPrqEBZfjgUi3ZJbQj8Z4VqmqoJsNLIzBXOIlQJX+5mWgeNdOq3vxe0u15ZkAlA==", + "version": "0.1.1-19", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-19.tgz", + "integrity": "sha512-wvcjw1A8wSH/oR5q+lZrBSyOQZfvXtLPYkQJBj11FBKu35iHko0FTIPMG25Ee+TpT2/BWLd29dWwiJODDQbC8w==", "dev": true, "license": "MIT", "dependencies": { @@ -694,9 +694,9 @@ } }, "node_modules/@vscode/component-explorer-vite-plugin": { - "version": "0.1.1-16", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-16.tgz", - "integrity": "sha512-z2EqusWl49dUF3vNDgmJJJQXkv4ejeBH9AdFZUWOiGaMvjjFX6UV7oQ733b+vo5YFE8my9WaK7D691i2wZ47Fg==", + "version": "0.1.1-19", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-19.tgz", + "integrity": "sha512-V0wMhLvHMbeUHOzwGrBPMwwvcbGhXXaQTCGc9hNfF4fjUutOtQFu5o+9XKDG1hIcKgk5qyvcRoXjVazBcg19lA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/build/vite/package.json b/build/vite/package.json index 5e5d59d1a16..14f6ad51c57 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-vite-plugin": "^0.1.1-16", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-vite-plugin": "^0.1.1-19", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" diff --git a/package-lock.json b/package-lock.json index 00fc750db9c..f6af524389b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,8 +84,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260130", - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-cli": "^0.1.1-12", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-cli": "^0.1.1-15", "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", @@ -3051,9 +3051,9 @@ "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-16", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-16.tgz", - "integrity": "sha512-is1RxdlNO5K1RSqWd5z8BN6gPrqEBZfjgUi3ZJbQj8Z4VqmqoJsNLIzBXOIlQJX+5mWgeNdOq3vxe0u15ZkAlA==", + "version": "0.1.1-19", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-19.tgz", + "integrity": "sha512-wvcjw1A8wSH/oR5q+lZrBSyOQZfvXtLPYkQJBj11FBKu35iHko0FTIPMG25Ee+TpT2/BWLd29dWwiJODDQbC8w==", "dev": true, "license": "MIT", "dependencies": { @@ -3062,9 +3062,9 @@ } }, "node_modules/@vscode/component-explorer-cli": { - "version": "0.1.1-12", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-12.tgz", - "integrity": "sha512-SaChUP94wkf1RaaJ/MnpQsxsr7pUpqQJq5Z9QLbrZuUqRil2TZEHwYLSqpQPqLgybNxZtrlMDivTjcCWXFTttg==", + "version": "0.1.1-15", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-15.tgz", + "integrity": "sha512-5unK3ehSezNAGJqN4Nn1CjIjavLY9Rc17buUOC/4SfqyXSFStWN/0JG7S/ESgwqW1I2WruadZis0X0sS33dlFQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8e5dc8cdf8f..30e324b07da 100644 --- a/package.json +++ b/package.json @@ -153,8 +153,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260130", - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-cli": "^0.1.1-12", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-cli": "^0.1.1-15", "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts index a67edb78b11..225223a3dda 100644 --- a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts +++ b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts @@ -56,48 +56,59 @@ function renderUpdateWidget(ctx: ComponentFixtureContext, state: State): void { widget.render(ctx.container); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'sessions/' }, { Ready: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (ctx) => renderUpdateWidget(ctx, State.Ready(mockUpdate, true, false)), }), CheckingForUpdates: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.CheckingForUpdates(true)), }), AvailableForDownload: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.AvailableForDownload(mockUpdate)), }), Downloading0Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 0, 100_000_000)), }), Downloading30Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 30_000_000, 100_000_000)), }), Downloading65Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 65_000_000, 100_000_000)), }), Downloading100Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 100_000_000, 100_000_000)), }), DownloadingIndeterminate: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false)), }), Downloaded: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloaded(mockUpdate, true, false)), }), Updating: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Updating(mockUpdate)), }), Overwriting: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Overwriting(mockUpdate, true)), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts index 001d6010501..0ab26885d3e 100644 --- a/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts @@ -9,12 +9,14 @@ import { ISessionData } from '../../../contrib/editTelemetry/browser/editStats/a import { Random } from '../../../../editor/test/common/core/random.js'; import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { AiStatsHover: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderAiStatsHover({ ...context, data: createSampleDataWithSessions() }), }), AiStatsHoverNoData: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderAiStatsHover({ ...context, data: createEmptyData() }), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts index 0c8ab56d71f..2bc08dd7587 100644 --- a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts @@ -22,34 +22,42 @@ import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGro export default defineThemedFixtureGroup({ Buttons: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderButtons, }), ButtonBar: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderButtonBar, }), Toggles: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderToggles, }), InputBoxes: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderInputBoxes, }), CountBadges: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderCountBadges, }), ActionBar: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderActionBar, }), ProgressBars: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderProgressBars, }), HighlightedLabels: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderHighlightedLabels, }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts index d22153e5766..cdb3ec7edb9 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts @@ -101,8 +101,9 @@ function renderProgressPart( container.appendChild(itemContainer); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { WithSpinner: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Searching workspace for relevant files...'), @@ -112,6 +113,7 @@ export default defineThemedFixtureGroup({ }), Completed: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Found 12 relevant files'), @@ -121,6 +123,7 @@ export default defineThemedFixtureGroup({ }), WithCustomIcon: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Running tests...'), @@ -130,6 +133,7 @@ export default defineThemedFixtureGroup({ }), WithInlineCode: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Reading `src/vs/workbench/contrib/chat/browser/chatWidget.ts`'), @@ -139,6 +143,7 @@ export default defineThemedFixtureGroup({ }), LongMessage: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Searching across multiple workspace folders for TypeScript files matching the pattern you described, including test files and configuration'), diff --git a/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts index 383f9dea3ab..db26a9199e6 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts @@ -124,20 +124,24 @@ const multiSelectQuestion: IChatQuestion = { // Fixtures // ============================================================================ -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { SingleTextQuestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([textQuestion])), }), SingleSelectQuestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([singleSelectQuestion])), }), MultiSelectQuestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([multiSelectQuestion])), }), MultipleQuestions: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([ textQuestion, singleSelectQuestion, @@ -146,10 +150,12 @@ export default defineThemedFixtureGroup({ }), NoSkip: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([singleSelectQuestion], false)), }), SubmittedSummary: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => { const carousel = createCarousel([textQuestion, singleSelectQuestion, multiSelectQuestion]); carousel.isUsed = true; @@ -163,6 +169,7 @@ export default defineThemedFixtureGroup({ }), SkippedSummary: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => { const carousel = createCarousel([textQuestion, singleSelectQuestion]); carousel.isUsed = true; diff --git a/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts index 76be7d9d682..9e54ad98196 100644 --- a/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts @@ -92,11 +92,13 @@ const simpleFixes: IActionListItem[] = [ { kind: ActionListItemKind.Action, item: 'fix-3', label: 'Add \'await\' to async call', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, ]; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { GroupedCodeActions: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderCodeActionList({ ...context, items: quickFixItems }), }), SimpleQuickFixes: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCodeActionList({ ...context, items: simpleFixes }), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts index c752df7da14..03c64b694aa 100644 --- a/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts @@ -68,8 +68,9 @@ function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtur editor.setModel(model); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { CodeEditor: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCodeEditor(context), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts index 4ed036840b6..c187a3e530f 100644 --- a/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts @@ -119,11 +119,13 @@ async function renderFindWidget(options: FindFixtureOptions): Promise { await new Promise(resolve => setTimeout(resolve, 300)); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { Find: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderFindWidget({ ...context, searchString: 'count' }), }), FindAndReplace: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderFindWidget({ ...context, searchString: 'count', replaceString: 'value', showReplace: true }), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index d482e55e701..e5318aaeecf 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -474,6 +474,24 @@ export function createTextModel( // Fixture Adapters // ============================================================================ +export interface ThemedFixtureGroupLabels { + readonly kind?: 'screenshot' | 'animated'; + readonly blocksCi?: true; +} + +function resolveLabels(labels: ThemedFixtureGroupLabels | undefined): string[] { + const result: string[] = []; + if (labels?.kind === 'screenshot') { + result.push('.screenshot'); + } else if (labels?.kind === 'animated') { + result.push('animated'); + } + if (labels?.blocksCi) { + result.push('blocks-ci'); + } + return result; +} + export interface ComponentFixtureContext { container: HTMLElement; disposableStore: DisposableStore; @@ -482,6 +500,7 @@ export interface ComponentFixtureContext { export interface ComponentFixtureOptions { render: (context: ComponentFixtureContext) => void | Promise; + labels?: ThemedFixtureGroupLabels; } type ThemedFixtures = ReturnType; @@ -509,18 +528,33 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed }, }); - return defineFixtureVariants({ + const labels = resolveLabels(options.labels); + return defineFixtureVariants(labels.length > 0 ? { labels } : {}, { Dark: createFixture(darkTheme), Light: createFixture(lightTheme), }); } -type ThemedFixtureGroupInput = Record; +interface ThemedFixtureGroupOptions { + readonly path?: string; + readonly labels?: ThemedFixtureGroupLabels; +} + +type ThemedFixtureGroupFixtures = Record; /** * Creates a nested fixture group from themed fixtures. * E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light */ -export function defineThemedFixtureGroup(group: ThemedFixtureGroupInput): ReturnType { - return defineFixtureGroup(group); +export function defineThemedFixtureGroup(options: ThemedFixtureGroupOptions, fixtures: ThemedFixtureGroupFixtures): ReturnType; +export function defineThemedFixtureGroup(fixtures: ThemedFixtureGroupFixtures): ReturnType; +export function defineThemedFixtureGroup(optionsOrFixtures: ThemedFixtureGroupOptions | ThemedFixtureGroupFixtures, fixtures?: ThemedFixtureGroupFixtures): ReturnType { + if (fixtures) { + const options = optionsOrFixtures as ThemedFixtureGroupOptions; + return defineFixtureGroup({ + labels: resolveLabels(options.labels), + path: options.path, + }, fixtures as ThemedFixtureGroupFixtures); + } + return defineFixtureGroup(optionsOrFixtures as ThemedFixtureGroupFixtures); } diff --git a/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts index 8d4fe0ae306..8a598804fea 100644 --- a/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts @@ -107,9 +107,10 @@ function renderInlineEdit(options: InlineEditOptions): void { // Fixtures // ============================================================================ -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { // Side-by-side view: Multi-line replacement SideBySideView: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderInlineEdit({ ...context, code: `function greet(name) { @@ -123,6 +124,7 @@ export default defineThemedFixtureGroup({ // Word replacement view: Single word change WordReplacementView: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderInlineEdit({ ...context, code: `class BufferData { @@ -139,6 +141,7 @@ export default defineThemedFixtureGroup({ // Insertion view: Insert new content InsertionView: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderInlineEdit({ ...context, code: `class BufferData { diff --git a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts index bbd8420665c..e9298fcf4d1 100644 --- a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts @@ -276,17 +276,21 @@ function createLongDistanceEditor(options: { controller?.model?.get(); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { HintsToolbar: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderHintsToolbar(context), }), HintsToolbarHovered: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderHintsToolbar({ ...context, simulateHover: true }), }), JumpToHint: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderJumpToHint, }), LongDistanceHint: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => createLongDistanceEditor({ ...context, code: LONG_DISTANCE_CODE, diff --git a/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts index 444777d13cd..6e87b84b4d0 100644 --- a/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts @@ -50,8 +50,9 @@ class FixtureQuickInputService extends QuickInputService { } } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { PromptFiles: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: context => renderPromptFilePickerFixture({ ...context, type: PromptsType.prompt, @@ -69,6 +70,7 @@ export default defineThemedFixtureGroup({ }), InstructionFilesWithAgentInstructions: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: context => renderPromptFilePickerFixture({ ...context, type: PromptsType.instructions, diff --git a/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts index 82416f4872c..0d85c6e0da4 100644 --- a/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts @@ -93,8 +93,9 @@ function renderRenameWidget(options: RenameFixtureOptions): void { ); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { RenameVariable: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderRenameWidget({ ...context, cursorLine: 4, @@ -105,6 +106,7 @@ export default defineThemedFixtureGroup({ }), }), RenameClass: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderRenameWidget({ ...context, cursorLine: 1, diff --git a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts index 623650a7a8d..a5aada2ea26 100644 --- a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts @@ -144,8 +144,9 @@ const mixedKindCompletions: CompletionList = { ], }; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { MethodCompletions: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderSuggestWidget({ ...context, code: `const element = document.getElementById('app'); @@ -159,6 +160,7 @@ if (element) { }), MixedKinds: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderSuggestWidget({ ...context, code: '', From 7794c410c65cbb3118b72b209bba9a6206eaeb6e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 13:01:41 +0100 Subject: [PATCH 045/448] update distro (#298931) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 30e324b07da..b6bbd9aee07 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.111.0", - "distro": "447d8d5a69fba52fcf6ea15dd29e92dd5dbbd2cd", + "distro": "e802965a9da346fb619bb708f64e54e927167133", "author": { "name": "Microsoft Corporation" }, @@ -250,4 +250,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file From fff1ab05afda2e631364cceee92027a62196840d Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 12:08:55 +0000 Subject: [PATCH 046/448] Update editor widget background colors in 2026 Light theme --- extensions/theme-2026/themes/2026-light.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 9a3a44fb872..082b53b39bd 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -134,26 +134,26 @@ "editorCodeLens.foreground": "#606060", "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F0F1F2FF", - "editorWidget.background": "#F0F0F3", + "editorWidget.background": "#FAFAFD", "editorWidget.border": "#EEEEF1", "editorWidget.foreground": "#202020", - "editorSuggestWidget.background": "#F0F0F3", + "editorSuggestWidget.background": "#FAFAFD", "editorSuggestWidget.border": "#EEEEF1", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", - "editorHoverWidget.background": "#F0F0F3", + "editorHoverWidget.background": "#FAFAFD", "editorHoverWidget.border": "#EEEEF1", "peekView.border": "#0069CC", - "peekViewEditor.background": "#F0F0F3", + "peekViewEditor.background": "#FAFAFD", "peekViewEditor.matchHighlightBackground": "#0069CC33", - "peekViewResult.background": "#F0F0F3", + "peekViewResult.background": "#FAFAFD", "peekViewResult.fileForeground": "#202020", "peekViewResult.lineForeground": "#606060", "peekViewResult.matchHighlightBackground": "#0069CC33", "peekViewResult.selectionBackground": "#0069CC26", "peekViewResult.selectionForeground": "#202020", - "peekViewTitle.background": "#F0F0F3", + "peekViewTitle.background": "#FAFAFD", "peekViewTitleDescription.foreground": "#606060", "peekViewTitleLabel.foreground": "#202020", "editorGutter.addedBackground": "#587c0c", From 93b18fe9826afee8034d73e911020d75fae288ba Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 12:14:03 +0000 Subject: [PATCH 047/448] Add padding to statusbar left and right items for improved layout --- .../workbench/browser/parts/statusbar/media/statusbarpart.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 7faaf9e7f4b..3f354e7e1e3 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -49,10 +49,12 @@ .monaco-workbench .part.statusbar > .right-items { flex-wrap: wrap; /* overflow elements by wrapping */ flex-direction: row-reverse; /* let the elements to the left wrap first */ + padding-right: 2px; } .monaco-workbench .part.statusbar > .left-items { flex-grow: 1; /* left items push right items to the far right end */ + padding-left: 2px; } .monaco-workbench .part.statusbar > .items-container > .statusbar-item { From 23cfa3e320098bc5e5a6341024a9af9e83b869b7 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 12:28:18 +0000 Subject: [PATCH 048/448] Refactor statusbar padding for improved layout and item visibility Co-authored-by: Copilot --- .../parts/statusbar/media/statusbarpart.css | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 3f354e7e1e3..2cc604448bd 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -49,13 +49,10 @@ .monaco-workbench .part.statusbar > .right-items { flex-wrap: wrap; /* overflow elements by wrapping */ flex-direction: row-reverse; /* let the elements to the left wrap first */ - padding-right: 2px; } .monaco-workbench .part.statusbar > .left-items { - flex-grow: 1; /* left items push right items to the far right end */ - padding-left: 2px; -} + flex-grow: 1; /* left items push right items to the far right end */} .monaco-workbench .part.statusbar > .items-container > .statusbar-item { display: inline-block; @@ -93,6 +90,16 @@ padding-left: 0; } +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { + padding-right: 0; + padding-left: 2px; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.right.last-visible-item { + padding-right: 2px; + padding-left: 0; +} + .monaco-workbench .part.statusbar > .items-container > .statusbar-item > .statusbar-item-label { cursor: pointer; display: flex; From 6e242477bc76566ecfe0073a0fbe3c7ce0cd99b8 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 3 Mar 2026 12:28:44 +0000 Subject: [PATCH 049/448] Remove padding adjustments for first and last visible statusbar items --- .../browser/parts/statusbar/media/statusbarpart.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 2cc604448bd..1f3b102ebb1 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -84,12 +84,6 @@ border-right: 5px solid transparent; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item, -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.right.last-visible-item { - padding-right: 0; - padding-left: 0; -} - .monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { padding-right: 0; padding-left: 2px; From 8c6874835d5eb0412ed2d40b541c57ce25cb5407 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 13:39:31 +0100 Subject: [PATCH 050/448] sessions - updates to selfhost setup (#298943) * sessions - updates to selfhost setup * document more hooks --- .github/hooks/hooks.json | 16 ++++++++++++++-- .vscode/tasks.json | 13 +++++++++++++ package.json | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json index 59c170e420e..3e0f178b023 100644 --- a/.github/hooks/hooks.json +++ b/.github/hooks/hooks.json @@ -4,7 +4,19 @@ "sessionStart": [ { "type": "command", - "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup npm ci > /tmp/npm-ci-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi" + "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup bash -c 'npm ci && npm run compile' > /tmp/worktree-setup-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi" + } + ], + "sessionEnd": [ + { + "type": "command", + "bash": "" + } + ], + "agentStop": [ + { + "type": "command", + "bash": "" } ], "userPromptSubmitted": [ @@ -26,4 +38,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e950c75d912..9e9cc12ca99 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -241,6 +241,19 @@ "inSessions": true, "problemMatcher": [] }, + { + "label": "Run and Compile Dev Sessions", + "type": "shell", + "command": "npm run transpile-client && ./scripts/code.sh", + "windows": { + "command": "npm run transpile-client && .\\scripts\\code.bat" + }, + "args": [ + "--sessions" + ], + "inSessions": true, + "problemMatcher": [] + }, { "type": "npm", "script": "electron", diff --git a/package.json b/package.json index b6bbd9aee07..7bc8f3f17d2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "watch-client": "npm run gulp watch-client", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", + "transpile-client": "node build/next/index.ts transpile", "watch-client-transpile": "node build/next/index.ts transpile --watch", "watch-client-transpiled": "deemon npm run watch-client-transpile", "kill-watch-client-transpiled": "deemon --kill npm run watch-client-transpile", From fb87d94563382dd5edaaed4f24b976318fd1ee2c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:45:46 +0100 Subject: [PATCH 051/448] Git - expose random name generation for branches (#298938) --- extensions/git/src/api/api1.ts | 4 +++ extensions/git/src/api/git.d.ts | 2 ++ extensions/git/src/commands.ts | 46 +--------------------------- extensions/git/src/repository.ts | 51 ++++++++++++++++++++++++++++++++ extensions/git/src/util.ts | 6 ++-- 5 files changed, 62 insertions(+), 47 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index abe5c331074..e5820c0ded7 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -347,6 +347,10 @@ export class ApiRepository implements Repository { migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise { return this.#repository.migrateChanges(sourceRepositoryPath, options); } + + generateRandomBranchName(): Promise { + return this.#repository.generateRandomBranchName(); + } } export class ApiGit implements Git { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 287dd4399bf..122134c2c8b 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -325,6 +325,8 @@ export interface Repository { deleteWorktree(path: string, options?: { force?: boolean }): Promise; migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; + + generateRandomBranchName(): Promise; } export interface RemoteSource { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1fc850565de..15f962b4307 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -7,7 +7,6 @@ import * as os from 'os'; import * as path from 'path'; import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages, SourceControlArtifact, ProgressLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; -import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import type { CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; import { ForcePushMode, GitErrorCodes, RefType, Status } from './api/git.constants'; import { Git, GitError, Repository as GitRepository, Stash, Worktree } from './git'; @@ -2943,48 +2942,6 @@ export class CommandCenter { await this._branch(repository, undefined, true); } - private async generateRandomBranchName(repository: Repository, separator: string): Promise { - const config = workspace.getConfiguration('git'); - const branchRandomNameDictionary = config.get('branchRandomName.dictionary')!; - - const dictionaries: string[][] = []; - for (const dictionary of branchRandomNameDictionary) { - if (dictionary.toLowerCase() === 'adjectives') { - dictionaries.push(adjectives); - } - if (dictionary.toLowerCase() === 'animals') { - dictionaries.push(animals); - } - if (dictionary.toLowerCase() === 'colors') { - dictionaries.push(colors); - } - if (dictionary.toLowerCase() === 'numbers') { - dictionaries.push(NumberDictionary.generate({ length: 3 })); - } - } - - if (dictionaries.length === 0) { - return ''; - } - - // 5 attempts to generate a random branch name - for (let index = 0; index < 5; index++) { - const randomName = uniqueNamesGenerator({ - dictionaries, - length: dictionaries.length, - separator - }); - - // Check for local ref conflict - const refs = await repository.getRefs({ pattern: `refs/heads/${randomName}` }); - if (refs.length === 0) { - return randomName; - } - } - - return ''; - } - private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise { const config = workspace.getConfiguration('git'); const branchPrefix = config.get('branchPrefix')!; @@ -2998,8 +2955,7 @@ export class CommandCenter { } const getBranchName = async (): Promise => { - const branchName = branchRandomNameEnabled ? await this.generateRandomBranchName(repository, branchWhitespaceChar) : ''; - return `${branchPrefix}${branchName}`; + return await repository.generateRandomBranchName() ?? branchPrefix; }; const getValueSelection = (value: string): [number, number] | undefined => { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 6810f3cca42..b79bb3bc4aa 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import TelemetryReporter from '@vscode/extension-telemetry'; +import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; @@ -3294,6 +3295,56 @@ export class Repository implements Disposable { return this.unpublishedCommits; } + async generateRandomBranchName(): Promise { + const config = workspace.getConfiguration('git', Uri.file(this.root)); + const branchRandomNameEnabled = config.get('branchRandomName.enable', false); + + if (!branchRandomNameEnabled) { + return undefined; + } + + const branchPrefix = config.get('branchPrefix', ''); + const branchWhitespaceChar = config.get('branchWhitespaceChar', '-'); + const branchRandomNameDictionary = config.get('branchRandomName.dictionary', ['adjectives', 'animals']); + + const dictionaries: string[][] = []; + for (const dictionary of branchRandomNameDictionary) { + if (dictionary.toLowerCase() === 'adjectives') { + dictionaries.push(adjectives); + } + if (dictionary.toLowerCase() === 'animals') { + dictionaries.push(animals); + } + if (dictionary.toLowerCase() === 'colors') { + dictionaries.push(colors); + } + if (dictionary.toLowerCase() === 'numbers') { + dictionaries.push(NumberDictionary.generate({ length: 3 })); + } + } + + if (dictionaries.length === 0) { + return undefined; + } + + // 5 attempts to generate a random branch name + for (let index = 0; index < 5; index++) { + const randomName = uniqueNamesGenerator({ + dictionaries, + length: dictionaries.length, + separator: branchWhitespaceChar + }); + + // Check for local ref conflict + const refs = await this.getRefs({ pattern: `refs/heads/${branchPrefix}${randomName}` }); + if (refs.length === 0) { + return `${branchPrefix}${randomName}`; + } + } + + return undefined; + } + dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index c6ec6ece45c..cbf1b56e34e 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -867,10 +867,12 @@ export function getStashDescription(stash: Stash): string | undefined { return descriptionSegments.join(' \u2022 '); } +export const CopilotWorktreeBranchPrefix = 'copilot-worktree-'; + export function isCopilotWorktree(path: string): boolean { const lastSepIndex = path.lastIndexOf(sep); return lastSepIndex !== -1 - ? path.substring(lastSepIndex + 1).startsWith('copilot-worktree-') - : path.startsWith('copilot-worktree-'); + ? path.substring(lastSepIndex + 1).startsWith(CopilotWorktreeBranchPrefix) + : path.startsWith(CopilotWorktreeBranchPrefix); } From 42cdbe270e1acce24258b2536f1735a1e3d68f99 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 3 Mar 2026 12:51:06 +0100 Subject: [PATCH 052/448] configures dependabot to automatically update component explorer --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f7e3481c75b..fc5cda5555b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,17 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-name: "@vscode/component-explorer" + - dependency-name: "@vscode/component-explorer-cli" + - package-ecosystem: "npm" + directory: "/build/vite" + schedule: + interval: "daily" + allow: + - dependency-name: "@vscode/component-explorer" + - dependency-name: "@vscode/component-explorer-vite-plugin" From e83344611ce4178236c6e38e4c77f4fb2614b052 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 14:34:46 +0100 Subject: [PATCH 053/448] sessions - indicate isolation level in viewer (#298955) * feat - add isolation icon support in agent sessions * refactor - remove isolation icon from agent sessions * refactor - remove isolation icon styles from CSS --- .../contrib/sessions/browser/sessionsViewPane.ts | 1 + .../browser/agentSessions/agentSessionsControl.ts | 1 + .../browser/agentSessions/agentSessionsViewer.ts | 12 +++++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 16e331f441c..953bd0b26f9 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -144,6 +144,7 @@ export class AgenticSessionsViewPane extends ViewPane { filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, disableHover: true, + showIsolationIcon: true, enableApprovalRow: true, getHoverPosition: () => this.getSessionHoverPosition(), trackActiveEditorSession: () => true, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 95d06b9dc2a..66977278aeb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -42,6 +42,7 @@ export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOption readonly filter: IAgentSessionsFilter; readonly source: string; readonly disableHover?: boolean; + readonly showIsolationIcon?: boolean; readonly enableApprovalRow?: boolean; getHoverPosition(): HoverPosition; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index fb7426b88e8..ca655535956 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -88,6 +88,7 @@ interface IAgentSessionItemTemplate { export interface IAgentSessionRendererOptions { readonly disableHover?: boolean; + readonly showIsolationIcon?: boolean; getHoverPosition(): HoverPosition; } @@ -382,8 +383,17 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre }; // Provider icon (only shown for non-local sessions) + // When showIsolationIcon is enabled for background sessions, show worktree/folder icon instead const isLocal = session.element.providerType === AgentSessionProviders.Local; - template.statusProviderIcon.className = isLocal ? '' : `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`; + if (isLocal) { + template.statusProviderIcon.className = ''; + } else if (this.options.showIsolationIcon && session.element.providerType === AgentSessionProviders.Background) { + const hasWorktree = typeof session.element.metadata?.worktreePath === 'string'; + const isolationIcon = hasWorktree ? Codicon.worktree : Codicon.folder; + template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(isolationIcon)}`; + } else { + template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`; + } // Time label template.statusTime.textContent = getTimeLabel(session.element); From 46b1b6e72e7eb33ff7ed44bf9c9681944448fb89 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 3 Mar 2026 14:50:06 +0100 Subject: [PATCH 054/448] sessions: fix separator in windows (#298961) --- src/vs/sessions/browser/parts/media/titlebarpart.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index f2f5d1ff68f..146ed67fa61 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -59,7 +59,8 @@ align-items: center; } -.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container:not(.has-no-actions):not(:last-child)::after { +/* Separator between session actions and layout actions toolbar */ +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-session-actions-container:not(.has-no-actions) + .titlebar-layout-actions-container:not(.has-no-actions)::before { content: ''; width: 1px; height: 16px; From 091ef378baaa141c8bc4bbe9775d4cb3bd655a80 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:56:43 +0100 Subject: [PATCH 055/448] Update grammars (#298962) --- extensions/css/cgmanifest.json | 2 +- extensions/css/syntaxes/css.tmLanguage.json | 4 +- extensions/dart/cgmanifest.json | 2 +- extensions/dart/syntaxes/dart.tmLanguage.json | 65 +++++- extensions/go/cgmanifest.json | 4 +- extensions/go/syntaxes/go.tmLanguage.json | 6 +- extensions/php/cgmanifest.json | 2 +- extensions/php/syntaxes/php.tmLanguage.json | 27 ++- extensions/ruby/cgmanifest.json | 2 +- extensions/ruby/syntaxes/ruby.tmLanguage.json | 212 +++++++++--------- extensions/swift/cgmanifest.json | 2 +- .../swift/syntaxes/swift.tmLanguage.json | 8 +- 12 files changed, 203 insertions(+), 133 deletions(-) diff --git a/extensions/css/cgmanifest.json b/extensions/css/cgmanifest.json index 7b85089b6b9..93bd8ba0f31 100644 --- a/extensions/css/cgmanifest.json +++ b/extensions/css/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "microsoft/vscode-css", "repositoryUrl": "https://github.com/microsoft/vscode-css", - "commitHash": "a927fe2f73927bf5c25d0b0c4dd0e63d69fd8887" + "commitHash": "9a07d76cb0e7a56f9bfc76328a57227751e4adb4" } }, "licenseDetail": [ diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index 5ba8bc90b73..484af027c19 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-css/commit/a927fe2f73927bf5c25d0b0c4dd0e63d69fd8887", + "version": "https://github.com/microsoft/vscode-css/commit/9a07d76cb0e7a56f9bfc76328a57227751e4adb4", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -1401,7 +1401,7 @@ "property-keywords": { "patterns": [ { - "match": "(?xi) (?)", "endCaptures": { "0": { "name": "punctuation.definition.arguments.end.bracket.round.php" @@ -2536,16 +2536,33 @@ ] }, "invoke-call": { - "captures": { + "begin": "(?i)((\\$+)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\\s*(\\()", + "beginCaptures": { "1": { "name": "variable.other.php" }, "2": { "name": "punctuation.definition.variable.php" + }, + "3": { + "name": "punctuation.definition.arguments.begin.bracket.round.php" } }, - "match": "(?i)((\\$+)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)(?=\\s*\\()", - "name": "meta.function-call.invoke.php" + "end": "\\)|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.bracket.round.php" + } + }, + "name": "meta.function-call.invoke.php", + "patterns": [ + { + "include": "#named-arguments" + }, + { + "include": "$self" + } + ] }, "namespace": { "begin": "(?i)(?:(namespace)|[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)?(\\\\)", diff --git a/extensions/ruby/cgmanifest.json b/extensions/ruby/cgmanifest.json index 5d7a9662061..0fb779250dd 100644 --- a/extensions/ruby/cgmanifest.json +++ b/extensions/ruby/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "Shopify/ruby-lsp", "repositoryUrl": "https://github.com/Shopify/ruby-lsp", - "commitHash": "59da6a0ae3409437474b85d0daa5535f1878699d" + "commitHash": "ba41f8b4f9677fb14c1ecbe15d73ebe12a0d3859" } }, "licenseDetail": [ diff --git a/extensions/ruby/syntaxes/ruby.tmLanguage.json b/extensions/ruby/syntaxes/ruby.tmLanguage.json index f5e3f2b0c0d..8cda18871b5 100644 --- a/extensions/ruby/syntaxes/ruby.tmLanguage.json +++ b/extensions/ruby/syntaxes/ruby.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Shopify/ruby-lsp/commit/59da6a0ae3409437474b85d0daa5535f1878699d", + "version": "https://github.com/Shopify/ruby-lsp/commit/ba41f8b4f9677fb14c1ecbe15d73ebe12a0d3859", "name": "Ruby", "scopeName": "source.ruby", "patterns": [ @@ -1583,7 +1583,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HTML)\\b\\1))", "comment": "Heredoc with embedded HTML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)HTML)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.html", "patterns": [ { @@ -1594,12 +1599,7 @@ } }, "contentName": "text.html", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)HTML)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1620,7 +1620,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HAML)\\b\\1))", "comment": "Heredoc with embedded HAML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)HAML)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.haml", "patterns": [ { @@ -1631,12 +1636,7 @@ } }, "contentName": "text.haml", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)HAML)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1657,7 +1657,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)XML)\\b\\1))", "comment": "Heredoc with embedded XML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)XML)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.xml", "patterns": [ { @@ -1668,12 +1673,7 @@ } }, "contentName": "text.xml", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)XML)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1694,7 +1694,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SQL)\\b\\1))", "comment": "Heredoc with embedded SQL", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)SQL)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.sql", "patterns": [ { @@ -1705,12 +1710,7 @@ } }, "contentName": "source.sql", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)SQL)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1731,7 +1731,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\b\\1))", "comment": "Heredoc with embedded GraphQL", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:GRAPHQL|GQL))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.graphql", "patterns": [ { @@ -1742,12 +1747,7 @@ } }, "contentName": "source.graphql", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1768,7 +1768,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CSS)\\b\\1))", "comment": "Heredoc with embedded CSS", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)CSS)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.css", "patterns": [ { @@ -1779,12 +1784,7 @@ } }, "contentName": "source.css", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)CSS)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1805,7 +1805,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CPP)\\b\\1))", "comment": "Heredoc with embedded C++", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)CPP)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.cpp", "patterns": [ { @@ -1816,12 +1821,7 @@ } }, "contentName": "source.cpp", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)CPP)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1842,7 +1842,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)C)\\b\\1))", "comment": "Heredoc with embedded C", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)C)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.c", "patterns": [ { @@ -1853,12 +1858,7 @@ } }, "contentName": "source.c", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)C)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1879,7 +1879,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\b\\1))", "comment": "Heredoc with embedded Javascript", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:JS|JAVASCRIPT))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.js", "patterns": [ { @@ -1890,12 +1895,7 @@ } }, "contentName": "source.js", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1916,7 +1916,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)JQUERY)\\b\\1))", "comment": "Heredoc with embedded jQuery Javascript", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)JQUERY)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.js.jquery", "patterns": [ { @@ -1927,12 +1932,7 @@ } }, "contentName": "source.js.jquery", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)JQUERY)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1953,7 +1953,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:SH|SHELL))\\b\\1))", "comment": "Heredoc with embedded Shell", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:SH|SHELL))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.shell", "patterns": [ { @@ -1964,12 +1969,7 @@ } }, "contentName": "source.shell", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:SH|SHELL))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1990,7 +1990,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)LUA)\\b\\1))", "comment": "Heredoc with embedded Lua", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)LUA)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.lua", "patterns": [ { @@ -2001,12 +2006,7 @@ } }, "contentName": "source.lua", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)LUA)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -2027,7 +2027,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)RUBY)\\b\\1))", "comment": "Heredoc with embedded Ruby", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)RUBY)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.ruby", "patterns": [ { @@ -2038,12 +2043,7 @@ } }, "contentName": "source.ruby", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)RUBY)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -2064,7 +2064,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:YAML|YML))\\b\\1))", "comment": "Heredoc with embedded YAML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:YAML|YML))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.yaml", "patterns": [ { @@ -2075,12 +2080,7 @@ } }, "contentName": "source.yaml", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:YAML|YML))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -2101,7 +2101,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SLIM)\\b\\1))", "comment": "Heredoc with embedded Slim", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)SLIM)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.slim", "patterns": [ { @@ -2112,12 +2117,7 @@ } }, "contentName": "text.slim", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)SLIM)\\s*$)", "patterns": [ { "include": "#heredoc" diff --git a/extensions/swift/cgmanifest.json b/extensions/swift/cgmanifest.json index ecd2705da2a..02ea0744ecd 100644 --- a/extensions/swift/cgmanifest.json +++ b/extensions/swift/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jtbandes/swift-tmlanguage", "repositoryUrl": "https://github.com/jtbandes/swift-tmlanguage", - "commitHash": "45ac01d47c6d63402570c2c36bcfbadbd1c7bca6" + "commitHash": "3fca2fa10f7dc962d19ee617b17844d6eecfa2cb" } }, "license": "MIT" diff --git a/extensions/swift/syntaxes/swift.tmLanguage.json b/extensions/swift/syntaxes/swift.tmLanguage.json index a8bbe5d00b4..d52cabb836b 100644 --- a/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/extensions/swift/syntaxes/swift.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jtbandes/swift-tmlanguage/commit/45ac01d47c6d63402570c2c36bcfbadbd1c7bca6", + "version": "https://github.com/jtbandes/swift-tmlanguage/commit/3fca2fa10f7dc962d19ee617b17844d6eecfa2cb", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -3848,7 +3848,7 @@ }, { "name": "string.quoted.double.block.raw.swift", - "begin": "#\"\"\"", + "begin": "#\"\"\"(?!#)(?=(?:[^\"]|\"(?!#))*$)", "end": "\"\"\"#(#*)", "beginCaptures": { "0": { @@ -3884,7 +3884,7 @@ }, { "name": "string.quoted.double.block.raw.swift", - "begin": "(##+)\"\"\"", + "begin": "(? Date: Tue, 3 Mar 2026 15:13:11 +0100 Subject: [PATCH 056/448] Enhance source map handling in build tasks for CI environments --- build/gulpfile.vscode.ts | 5 +++-- build/gulpfile.vscode.web.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 686028110b5..b38f9a2f08b 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -237,8 +237,9 @@ function runTsGoTypeCheck(): Promise { } const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; -const useCdnSourceMapsForPackagingTasks = !!process.env['CI']; -const stripSourceMapsInPackagingTasks = !!process.env['CI']; +const isCI = !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!process.env['GITHUB_WORKSPACE']; +const useCdnSourceMapsForPackagingTasks = isCI; +const stripSourceMapsInPackagingTasks = isCI; const minifyVSCodeTask = task.define('minify-vscode', task.series( bundleVSCodeTask, util.rimraf('out-vscode-min'), diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index e9cc3720fcf..3e6b29adfe9 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -33,7 +33,7 @@ const quality = (product as { quality?: string }).quality; const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; // esbuild-based bundle for standalone web -function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promise { +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, sourceMapBaseUrl?: string): Promise { return new Promise((resolve, reject) => { const scriptPath = path.join(REPO_ROOT, 'build/next/index.ts'); const args = [scriptPath, 'bundle', '--out', outDir, '--target', 'web']; @@ -44,6 +44,9 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promis if (nls) { args.push('--nls'); } + if (sourceMapBaseUrl) { + args.push('--source-map-base-url', sourceMapBaseUrl); + } const proc = cp.spawn(process.execPath, args, { cwd: REPO_ROOT, @@ -164,8 +167,9 @@ const minifyVSCodeWebTask = task.define('minify-vscode-web-OLD', task.series( gulp.task(minifyVSCodeWebTask); // esbuild-based tasks (new) +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const esbuildBundleVSCodeWebTask = task.define('esbuild-vscode-web', () => runEsbuildBundle('out-vscode-web', false, true)); -const esbuildBundleVSCodeWebMinTask = task.define('esbuild-vscode-web-min', () => runEsbuildBundle('out-vscode-web-min', true, true)); +const esbuildBundleVSCodeWebMinTask = task.define('esbuild-vscode-web-min', () => runEsbuildBundle('out-vscode-web-min', true, true, `${sourceMappingURLBase}/core`)); function packageTask(sourceFolderName: string, destinationFolderName: string) { const destination = path.join(BUILD_ROOT, destinationFolderName); From 06b198fdd95fce8c76e0cfa7797fc301ff08e6c2 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 3 Mar 2026 12:52:33 +0100 Subject: [PATCH 057/448] improves npm cache logic --- build/npm/installStateHash.ts | 37 ++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/build/npm/installStateHash.ts b/build/npm/installStateHash.ts index 5674a1eaee3..1ee80522d6c 100644 --- a/build/npm/installStateHash.ts +++ b/build/npm/installStateHash.ts @@ -36,15 +36,46 @@ export interface PostinstallState { readonly fileHashes: Record; } -const packageJsonIgnoredKeys = new Set(['distro']); +const packageJsonRelevantKeys = new Set([ + 'name', + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + 'peerDependenciesMeta', + 'overrides', + 'engines', + 'workspaces', + 'bundledDependencies', + 'bundleDependencies', +]); + +const packageLockJsonIgnoredKeys = new Set(['version']); function normalizeFileContent(filePath: string): string { const raw = fs.readFileSync(filePath, 'utf8'); - if (path.basename(filePath) === 'package.json') { + const basename = path.basename(filePath); + if (basename === 'package.json') { const json = JSON.parse(raw); - for (const key of packageJsonIgnoredKeys) { + const filtered: Record = {}; + for (const key of packageJsonRelevantKeys) { + // eslint-disable-next-line local/code-no-in-operator + if (key in json) { + filtered[key] = json[key]; + } + } + return JSON.stringify(filtered, null, '\t') + '\n'; + } + if (basename === 'package-lock.json') { + const json = JSON.parse(raw); + for (const key of packageLockJsonIgnoredKeys) { delete json[key]; } + if (json.packages?.['']) { + for (const key of packageLockJsonIgnoredKeys) { + delete json.packages[''][key]; + } + } return JSON.stringify(json, null, '\t') + '\n'; } return raw; From bf882b69a0ee51398d79d652c3205fbc8adafaab Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 3 Mar 2026 23:45:16 +0900 Subject: [PATCH 058/448] fix: explicitly set volume size for dmg (#298918) --- build/darwin/dmg-settings.py.template | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/darwin/dmg-settings.py.template b/build/darwin/dmg-settings.py.template index 4a54a69ab02..f471029f32a 100644 --- a/build/darwin/dmg-settings.py.template +++ b/build/darwin/dmg-settings.py.template @@ -6,8 +6,9 @@ format = 'ULMO' badge_icon = {{BADGE_ICON}} background = {{BACKGROUND}} -# Volume size (None = auto-calculate) -size = None +# Volume size +size = '1g' +shrink = False # Files and symlinks files = [{{APP_PATH}}] From 03645977690d332a55983812603c1d761a1a45c0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 15:55:08 +0100 Subject: [PATCH 059/448] sessions - context menu on title for session actions (#298968) * sessions - context menu on title for session actions * Update src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ccr * ccr --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/sessionsTitleBarWidget.ts | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 6ff448786dd..f2a3198b33b 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -4,16 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import './media/sessionsTitleBarWidget.css'; -import { $, addDisposableListener, EventType, reset } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, EventType, getActiveWindow, reset } from '../../../../base/browser/dom.js'; +import { Separator } from '../../../../base/common/actions.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { MarshalledId } from '../../../../base/common/marshallingIds.js'; +import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { localize } from '../../../../nls.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IMenuService, MenuId, MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { IMarshalledAgentSessionContext } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; @@ -65,6 +72,10 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IChatService private readonly chatService: IChatService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, ) { super(undefined, action, options); @@ -176,6 +187,11 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { e.stopPropagation(); this._showSessionsPicker(); })); + this._dynamicDisposables.add(addDisposableListener(sessionPill, EventType.CONTEXT_MENU, (e) => { + e.preventDefault(); + e.stopPropagation(); + this._showContextMenu(e); + })); this._container.appendChild(sessionPill); @@ -284,6 +300,42 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { return basename(uri); } + private _showContextMenu(e: MouseEvent): void { + const activeSession = this.activeSessionService.getActiveSession(); + if (!activeSession) { + return; + } + + const agentSession = this.agentSessionsService.getSession(activeSession.resource); + if (!agentSession) { + return; + } + + this.chatSessionsService.activateChatSessionItemProvider(agentSession.providerType); + + const contextOverlay: Array<[string, boolean | string]> = [ + [ChatContextKeys.isArchivedAgentSession.key, agentSession.isArchived()], + [ChatContextKeys.isReadAgentSession.key, agentSession.isRead()], + [ChatContextKeys.agentSessionType.key, agentSession.providerType], + ]; + + const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay)); + + const marshalledContext: IMarshalledAgentSessionContext = { + session: agentSession, + sessions: [agentSession], + $mid: MarshalledId.AgentSessionContext, + }; + + this.contextMenuService.showContextMenu({ + getActions: () => Separator.join(...menu.getActions({ arg: marshalledContext, shouldForwardArgs: true }).map(([, actions]) => actions)), + getAnchor: () => new StandardMouseEvent(getActiveWindow(), e), + getActionsContext: () => marshalledContext + }); + + menu.dispose(); + } + private _showSessionsPicker(): void { const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined, { overrideSessionOpen: (session, openOptions) => this.activeSessionService.openSession(session.resource, openOptions) From 2be5f84ffebfe57b64280b675832c57a37962aaa Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 3 Mar 2026 16:03:31 +0100 Subject: [PATCH 060/448] f5 run action keybinding and command handler --- .../contrib/chat/browser/runScriptAction.ts | 79 +++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index fcb730a7b56..a1cd803058a 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -5,11 +5,14 @@ import { equals } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, derivedOpts, IObservable } from '../../../../base/common/observable.js'; import { localize, localize2 } from '../../../../nls.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { SessionsCategories } from '../../../common/categories.js'; @@ -26,6 +29,7 @@ export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdow // Action IDs const RUN_SCRIPT_ACTION_ID = 'workbench.action.agentSessions.runScript'; +const RUN_SCRIPT_ACTION_PRIMARY_ID = 'workbench.action.agentSessions.runScriptPrimary'; const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction'; function getTaskDisplayLabel(task: ITaskEntry): string { @@ -62,6 +66,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr constructor( @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, ) { @@ -94,6 +99,40 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr private _registerActions(): void { const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: RUN_SCRIPT_ACTION_PRIMARY_ID, + title: { value: localize('runPrimaryScript', 'Run Primary Script'), original: 'Run Primary Script' }, + icon: Codicon.play, + category: SessionsCategories.Sessions, + f1: true, + }); + } + + async run(): Promise { + const activeState = that._activeRunState.get(); + if (!activeState) { + return; + } + + const { tasks, session, lastRunTaskLabel } = activeState; + if (tasks.length === 0) { + const task = await that._showConfigureQuickPick(session); + if (task) { + await that._sessionsConfigService.runTask(task, session); + } + return; + } + + const mruIndex = lastRunTaskLabel !== undefined + ? tasks.findIndex(t => t.label === lastRunTaskLabel) + : -1; + const primaryTask = tasks[mruIndex >= 0 ? mruIndex : 0]; + await that._sessionsConfigService.runTask(primaryTask, session); + } + })); + this._register(autorun(reader => { const activeState = this._activeRunState.read(reader); if (!activeState) { @@ -112,13 +151,15 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; const actionId = `${RUN_SCRIPT_ACTION_ID}.${i}`; + const isPrimary = i === (mruIndex >= 0 ? mruIndex : 0); reader.store.add(registerAction2(class extends Action2 { constructor() { super({ id: actionId, title: getTaskDisplayLabel(task), - tooltip: localize('runActionTooltip', "Run '{0}' in terminal", getTaskDisplayLabel(task)), + tooltip: !isPrimary ? localize('runActionTooltip', "Run '{0}' in terminal", getTaskDisplayLabel(task)) + : localize('runActionTooltipKeybinding', "Run '{0}' in terminal ({1})", getTaskDisplayLabel(task), that._keybindingService.lookupKeybinding(RUN_SCRIPT_ACTION_PRIMARY_ID)?.getLabel() ?? ''), icon: Codicon.play, category: SessionsCategories.Sessions, menu: [{ @@ -154,18 +195,20 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } async run(): Promise { - await that._showConfigureQuickPick(session); + const task = await that._showConfigureQuickPick(session); + if (task) { + await that._sessionsConfigService.runTask(task, session); + } } })); })); } - private async _showConfigureQuickPick(session: IActiveSessionItem): Promise { + private async _showConfigureQuickPick(session: IActiveSessionItem): Promise { const nonSessionTasks = await this._sessionsConfigService.getNonSessionTasks(session); if (nonSessionTasks.length === 0) { // No existing tasks, go straight to custom command input - await this._showCustomCommandInput(session); - return; + return this._showCustomCommandInput(session); } interface ITaskPickItem extends IQuickPickItem { @@ -198,38 +241,36 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr }); if (!picked) { - return; + return undefined; } const pickedItem = picked as ITaskPickItem; if (pickedItem.task) { // Existing task — set inSessions: true await this._sessionsConfigService.addTaskToSessions(pickedItem.task, session, pickedItem.source ?? 'workspace'); + return pickedItem.task; } else { // Custom command path - await this._showCustomCommandInput(session); + return this._showCustomCommandInput(session); } } - private async _showCustomCommandInput(session: IActiveSessionItem): Promise { + private async _showCustomCommandInput(session: IActiveSessionItem): 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 as a task in the integrated terminal") }); if (!command) { - return; + return undefined; } const target = await this._pickStorageTarget(session); if (!target) { - return; + return undefined; } - const newTask = await this._sessionsConfigService.createAndAddTask(command, session, target); - if (newTask) { - await this._sessionsConfigService.runTask(newTask, session); - } + return this._sessionsConfigService.createAndAddTask(command, session, target); } private async _pickStorageTarget(session: IActiveSessionItem): Promise { @@ -317,3 +358,13 @@ class RunScriptNotAvailableAction extends Action2 { } registerAction2(RunScriptNotAvailableAction); + +// Register F5 keybinding at module level to ensure it's in the registry +// before the keybinding resolver is cached. The command handler is +// registered later by RunScriptContribution. +KeybindingsRegistry.registerKeybindingRule({ + id: RUN_SCRIPT_ACTION_PRIMARY_ID, + primary: KeyCode.F5, + weight: KeybindingWeight.WorkbenchContrib + 100, + when: IsAuxiliaryWindowContext.toNegated() +}); From 5bed6754c7393af5a8d2444f11709d2a315e5b04 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 3 Mar 2026 16:32:04 +0100 Subject: [PATCH 061/448] fix ctrl+w keybinding --- .../sessions/browser/sessionsViewPane.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 953bd0b26f9..4ed04cbc113 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -325,10 +325,27 @@ KeybindingsRegistry.registerKeybindingRule({ primary: KeyMod.CtrlCmd | KeyCode.KeyN, }); +const CLOSE_SESSION_COMMAND_ID = 'agentSession.close'; +registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { + constructor() { + super({ + id: CLOSE_SESSION_COMMAND_ID, + title: localize2('closeSession', "Close Session"), + f1: true, + precondition: ContextKeyExpr.and(IsNewChatSessionContext.negate(), EditorsVisibleContext.negate()), + category: SessionsCategories.Sessions, + }); + } + override async run(accessor: ServicesAccessor) { + const sessionsService = accessor.get(ISessionsManagementService); + await sessionsService.openNewSessionView(); + } +}); + // Register Cmd+W / Ctrl+W to open new session when the current session is non-empty, // mirroring how Cmd+W closes the active editor in the normal workbench. KeybindingsRegistry.registerKeybindingRule({ - id: ACTION_ID_NEW_CHAT, + id: CLOSE_SESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib + 1, when: ContextKeyExpr.and(IsNewChatSessionContext.negate(), EditorsVisibleContext.negate()), primary: KeyMod.CtrlCmd | KeyCode.KeyW, From 1b92648d631237407db4a36f1bd9ce243befaf85 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 3 Mar 2026 16:58:46 +0100 Subject: [PATCH 062/448] - remember draft state in new chat (#298983) - move changes action to title --- .../changesView/browser/changesViewActions.ts | 121 +-------------- .../contrib/chat/browser/branchPicker.ts | 15 +- .../contrib/chat/browser/newChatViewPane.ts | 139 +++++++++++++----- .../chat/browser/sessionTargetPicker.ts | 19 ++- .../browser/media/sessionsTitleBarWidget.css | 16 ++ .../browser/sessionsTitleBarWidget.ts | 43 +++++- 6 files changed, 193 insertions(+), 160 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts b/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts index 4b348230ee0..9ca4eeb503d 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts @@ -3,28 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/changesViewActions.css'; -import { $, reset } from '../../../../base/browser/dom.js'; -import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { observableFromEvent } from '../../../../base/common/observable.js'; +import { localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { getAgentChangesSummary, hasValidDiff } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { hasValidDiff } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; -import { Menus } from '../../../browser/menus.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { CHANGES_VIEW_ID } from './changesView.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { activeSessionHasChangesContextKey } from '../common/changes.js'; @@ -34,12 +25,6 @@ const openChangesViewActionOptions: IAction2Options = { title: localize2('openChangesView', "Changes"), icon: Codicon.diffMultiple, f1: false, - menu: { - id: Menus.TitleBarSessionMenu, - group: 'navigation', - order: 1, - when: ContextKeyExpr.equals(activeSessionHasChangesContextKey.key, true), - }, }; class OpenChangesViewAction extends Action2 { @@ -58,111 +43,17 @@ class OpenChangesViewAction extends Action2 { registerAction2(OpenChangesViewAction); -/** - * Custom action view item that renders the changes summary as: - * [diff-icon] +insertions -deletions - */ -class ChangesActionViewItem extends BaseActionViewItem { - - private _container: HTMLElement | undefined; - private readonly _renderDisposables = this._register(new DisposableStore()); - - constructor( - action: IAction, - options: IBaseActionViewItemOptions | undefined, - @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, - @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, - @IHoverService private readonly hoverService: IHoverService, - ) { - super(undefined, action, options); - - this._register(autorun(reader => { - this.sessionManagementService.activeSession.read(reader); - this._updateLabel(); - })); - - this._register(this.agentSessionsService.model.onDidChangeSessions(() => { - this._updateLabel(); - })); - } - - override render(container: HTMLElement): void { - super.render(container); - this._container = container; - container.classList.add('changes-action-view-item'); - this._updateLabel(); - } - - private _updateLabel(): void { - if (!this._container) { - return; - } - - this._renderDisposables.clear(); - reset(this._container); - - const activeSession = this.sessionManagementService.getActiveSession(); - if (!activeSession) { - this._container.style.display = 'none'; - return; - } - - const agentSession = this.agentSessionsService.getSession(activeSession.resource); - const changes = agentSession?.changes; - - if (!changes || !hasValidDiff(changes)) { - this._container.style.display = 'none'; - return; - } - - const summary = getAgentChangesSummary(changes); - if (!summary) { - this._container.style.display = 'none'; - return; - } - - this._container.style.display = ''; - - // Diff icon - const iconEl = $('span.changes-action-icon' + ThemeIcon.asCSSSelector(Codicon.diffMultiple)); - this._container.appendChild(iconEl); - - // Insertions - const addedEl = $('span.changes-action-added'); - addedEl.textContent = `+${summary.insertions}`; - this._container.appendChild(addedEl); - - // Deletions - const removedEl = $('span.changes-action-removed'); - removedEl.textContent = `-${summary.deletions}`; - this._container.appendChild(removedEl); - - // Hover - this._renderDisposables.add(this.hoverService.setupManagedHover( - getDefaultHoverDelegate('mouse'), - this._container, - localize('agentSessions.viewChanges', "View All Changes") - )); - } -} - class ChangesViewActionsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.changesViewActions'; constructor( - @IActionViewItemService actionViewItemService: IActionViewItemService, - @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @ISessionsManagementService sessionManagementService: ISessionsManagementService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, ) { super(); - this._register(actionViewItemService.register(Menus.TitleBarSessionMenu, OpenChangesViewAction.ID, (action, options) => { - return instantiationService.createInstance(ChangesActionViewItem, action, options); - })); - // Bind context key: true when the active session has changes const sessionsChanged = observableFromEvent(this, agentSessionsService.model.onDidChangeSessions, () => { }); this._register(bindContextKey(activeSessionHasChangesContextKey, contextKeyService, reader => { diff --git a/src/vs/sessions/contrib/chat/browser/branchPicker.ts b/src/vs/sessions/contrib/chat/browser/branchPicker.ts index e12427b28ca..b5ada806bc2 100644 --- a/src/vs/sessions/contrib/chat/browser/branchPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/branchPicker.ts @@ -31,6 +31,7 @@ interface IBranchItem { export class BranchPicker extends Disposable { private _selectedBranch: string | undefined; + private _preferredBranch: string | undefined; private _newSession: INewSession | undefined; private _branches: string[] = []; @@ -48,6 +49,13 @@ export class BranchPicker extends Disposable { return this._selectedBranch; } + /** + * Sets a preferred branch to select when branches are loaded. + */ + setPreferredBranch(branch: string | undefined): void { + this._preferredBranch = branch; + } + constructor( @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, ) { @@ -85,8 +93,11 @@ export class BranchPicker extends Disposable { .filter((name): name is string => !!name) .filter(name => !name.includes(COPILOT_WORKTREE_PATTERN)); - // Select active branch, main, master, or the first branch by default - const defaultBranch = this._branches.find(b => b === repository.state.get().HEAD?.name) + // Select preferred branch (from draft), active branch, main, master, or the first branch + const preferred = this._preferredBranch; + this._preferredBranch = undefined; + const defaultBranch = (preferred ? this._branches.find(b => b === preferred) : undefined) + ?? this._branches.find(b => b === repository.state.get().HEAD?.name) ?? this._branches.find(b => b === 'main') ?? this._branches.find(b => b === 'master') ?? this._branches[0]; diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 247501dfc64..9f285ef4c48 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -11,7 +11,7 @@ import { toAction } from '../../../../base/common/actions.js'; import { Emitter } from '../../../../base/common/event.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { observableValue } from '../../../../base/common/observable.js'; +import { autorun, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; @@ -56,7 +56,7 @@ import { NewChatContextAttachments } from './newChatContextAttachments.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { FolderPicker } from './folderPicker.js'; import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; -import { IsolationModePicker, SessionTargetPicker } from './sessionTargetPicker.js'; +import { IsolationMode, IsolationModePicker, SessionTargetPicker } from './sessionTargetPicker.js'; import { BranchPicker } from './branchPicker.js'; import { SyncIndicator } from './syncIndicator.js'; import { INewSession, ISessionOptionGroup, RemoteNewSession } from './newSession.js'; @@ -75,6 +75,14 @@ const STORAGE_KEY_DRAFT_STATE = 'sessions.draftState'; const MIN_EDITOR_HEIGHT = 50; const MAX_EDITOR_HEIGHT = 200; +interface IDraftState extends IChatModelInputState { + target?: AgentSessionProviders; + isolationMode?: IsolationMode; + branch?: string; + folderUri?: string; + repo?: string; +} + // #region --- Chat Welcome Widget --- /** @@ -152,6 +160,16 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Slash commands private _slashCommandHandler: SlashCommandHandler | undefined; + // Input state + private _draftState: IDraftState | undefined = { + inputText: '', + attachments: [], + mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent }, + selectedModel: undefined, + selections: [], + contrib: {} + }; + // Input history private readonly _history: ChatHistoryNavigator; private _historyNavigationBackwardsEnablement!: IHistoryNavigationContext['historyNavigationBackwardsEnablement']; @@ -190,6 +208,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._isolationModePicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal); this._syncIndicator.setVisible(isLocal); + this._updateDraftState(); this._focusEditor(); })); @@ -200,21 +219,37 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._register(this._branchPicker.onDidChange((branch) => { this._syncIndicator.setBranch(branch); + this._updateDraftState(); this._focusEditor(); })); this._register(this._folderPicker.onDidSelectFolder(() => { + this._updateDraftState(); this._focusEditor(); })); - this._register(this._isolationModePicker.onDidChange(() => { + this._register(this._isolationModePicker.onDidChange((mode) => { + this._branchPicker.setVisible(mode === 'worktree'); + this._syncIndicator.setVisible(mode === 'worktree'); + this._updateDraftState(); this._focusEditor(); })); + this._register(this._repoPicker.onDidSelectRepo(() => { + this._updateDraftState(); + })); + // When language models change (e.g., extension activates), reinitialize if no model selected this._register(this.languageModelsService.onDidChangeLanguageModels(() => { this._initDefaultModel(); })); + + // Update input state when attachments or model change + this._register(this._contextAttachments.onDidChangeContext(() => this._updateDraftState())); + this._register(autorun(reader => { + this._currentLanguageModel.read(reader); + this._updateDraftState(); + })); } // --- Rendering --- @@ -261,11 +296,12 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._branchPicker.render(branchContainer); this._syncIndicator.render(branchContainer); - // Set initial visibility based on default target + // Set initial visibility based on default target and isolation mode const isLocal = this._targetPicker.selectedTarget === AgentSessionProviders.Background; + const isWorktree = this._isolationModePicker.isolationMode === 'worktree'; this._isolationModePicker.setVisible(isLocal); - this._branchPicker.setVisible(isLocal); - this._syncIndicator.setVisible(isLocal); + this._branchPicker.setVisible(isLocal && isWorktree); + this._syncIndicator.setVisible(isLocal && isWorktree); // Render target buttons & extension pickers this._renderOptionGroupPickers(); @@ -517,6 +553,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._slashCommandHandler = this._register(this.instantiationService.createInstance(SlashCommandHandler, this._editor)); this._register(this._editor.onDidChangeModelContent(() => { + this._updateDraftState(); this._updateSendButtonState(); })); } @@ -848,9 +885,8 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { if (this._history.isAtStart()) { return; } - const state = this._getInputState(); - if (state.inputText || state.attachments.length) { - this._history.overlay(state); + if (this._draftState?.inputText || this._draftState?.attachments.length) { + this._history.overlay(this._draftState); } this._navigateHistory(true); } @@ -859,21 +895,26 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { if (this._history.isAtEnd()) { return; } - const state = this._getInputState(); - if (state.inputText || state.attachments.length) { - this._history.overlay(state); + if (this._draftState?.inputText || this._draftState?.attachments.length) { + this._history.overlay(this._draftState); } this._navigateHistory(false); } - private _getInputState(): IChatModelInputState { - return { + private _updateDraftState(): void { + const attachments = [...this._contextAttachments.attachments]; + this._draftState = { inputText: this._editor?.getModel()?.getValue() ?? '', - attachments: [...this._contextAttachments.attachments], + attachments, mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent }, selectedModel: this._currentLanguageModel.get(), selections: this._editor?.getSelections() ?? [], contrib: {}, + target: this._targetPicker.selectedTarget, + isolationMode: this._isolationModePicker.isolationMode, + branch: this._branchPicker.selectedBranch, + folderUri: this._folderPicker.selectedFolderUri?.toString(), + repo: this._repoPicker.selectedRepo, }; } @@ -932,7 +973,9 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._contextAttachments.attachments.length > 0 ? [...this._contextAttachments.attachments] : undefined ); - this._history.append(this._getInputState()); + if (this._draftState) { + this._history.append(this._draftState); + } this._clearDraftState(); this._sending = true; @@ -940,22 +983,23 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._updateSendButtonState(); this._updateInputLoadingState(); - this.sessionsManagementService.sendRequestForNewSession( - session.resource, - options?.openNewAfterSend ? { openNewSessionView: true } : undefined - ).then(() => { - // Release ref without disposing - the service owns disposal - this._newSession.clearAndLeak(); + + try { + await this.sessionsManagementService.sendRequestForNewSession( + session.resource, + options?.openNewAfterSend ? { openNewSessionView: true } : undefined + ); this._newSessionListener.clear(); this._contextAttachments.clear(); - }, e => { + } catch (e) { this.logService.error('Failed to send request:', e); - }).finally(() => { - this._sending = false; - this._editor.updateOptions({ readOnly: false }); - this._updateSendButtonState(); - this._updateInputLoadingState(); - }); + } + + + this._sending = false; + this._editor.updateOptions({ readOnly: false }); + this._updateSendButtonState(); + this._updateInputLoadingState(); } /** @@ -1001,10 +1045,23 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._currentLanguageModel.set(model, undefined); } } + if (draft.isolationMode) { + this._isolationModePicker.setPreferredIsolationMode(draft.isolationMode); + this._isolationModePicker.setIsolationMode(draft.isolationMode); + } + if (draft.branch) { + this._branchPicker.setPreferredBranch(draft.branch); + } + if (draft.folderUri) { + try { this._folderPicker.setSelectedFolder(URI.parse(draft.folderUri)); } catch { /* ignore */ } + } + if (draft.repo) { + this._repoPicker.setSelectedRepo(draft.repo); + } } } - private _getDraftState(): (IChatModelInputState & { target?: AgentSessionProviders }) | undefined { + private _getDraftState(): IDraftState | undefined { const raw = this.storageService.get(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); if (!raw) { return undefined; @@ -1017,21 +1074,20 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } private _clearDraftState(): void { + this._draftState = undefined; this.storageService.remove(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); } saveState(): void { - const inputState = this._getInputState(); - const state = { - ...inputState, - attachments: inputState.attachments.map(IChatRequestVariableEntry.toExport), - target: this._targetPicker.selectedTarget, - }; - this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(state), StorageScope.WORKSPACE, StorageTarget.MACHINE); + if (this._draftState) { + const state = { + ...this._draftState, + attachments: this._draftState.attachments.map(IChatRequestVariableEntry.toExport), + }; + this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(state), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } } - // --- Layout --- - layout(_height: number, _width: number): void { this._editor?.layout(); } @@ -1118,6 +1174,11 @@ export class NewChatViewPane extends ViewPane { override saveState(): void { this._widget?.saveState(); } + + override dispose(): void { + this._widget?.saveState(); + super.dispose(); + } } // #endregion diff --git a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts index 9b3de3cff05..88aaa11a7a0 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts @@ -137,6 +137,7 @@ export type IsolationMode = 'worktree' | 'workspace'; export class IsolationModePicker extends Disposable { private _isolationMode: IsolationMode = 'worktree'; + private _preferredIsolationMode: IsolationMode | undefined; private _newSession: INewSession | undefined; private _repository: IGitRepository | undefined; @@ -171,7 +172,9 @@ export class IsolationModePicker extends Disposable { setRepository(repository: IGitRepository | undefined): void { this._repository = repository; if (repository) { - this._setMode('worktree'); + const preferred = this._preferredIsolationMode; + this._preferredIsolationMode = undefined; + this._setMode(preferred ?? 'worktree'); } else if (this._isolationMode === 'worktree') { this._setMode('workspace'); } @@ -207,6 +210,20 @@ export class IsolationModePicker extends Disposable { })); } + /** + * Sets a preferred isolation mode to apply when a repository is set. + */ + setPreferredIsolationMode(mode: IsolationMode): void { + this._preferredIsolationMode = mode; + } + + /** + * Programmatically set the isolation mode. + */ + setIsolationMode(mode: IsolationMode): void { + this._setMode(mode); + } + /** * Shows or hides the picker. */ diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index 26b50d594d9..b9f14273091 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -76,3 +76,19 @@ opacity: 0.5; flex-shrink: 0; } + +/* Changes summary */ +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-changes { + display: flex; + align-items: center; + flex-shrink: 0; + gap: 3px; +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-changes-added { + color: var(--vscode-gitDecoration-addedResourceForeground); +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-changes-removed { + color: var(--vscode-gitDecoration-deletedResourceForeground); +} diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index f2a3198b33b..f15bed0dc94 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -19,9 +19,8 @@ import { IMenuService, MenuId, MenuRegistry, SubmenuItemAction } from '../../../ import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { IMarshalledAgentSessionContext } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IMarshalledAgentSessionContext, getAgentChangesSummary, hasValidDiff } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; - import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; @@ -128,9 +127,10 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { const label = this._getActiveSessionLabel(); const icon = this._getActiveSessionIcon(); const repoLabel = this._getRepositoryLabel(); + const changesSummary = this._getChangesSummary(); // Build a render-state key from all displayed data - const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}`; + const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}|${changesSummary?.insertions ?? ''}|${changesSummary?.deletions ?? ''}`; // Skip re-render if state hasn't changed if (this._lastRenderState === renderState) { @@ -175,6 +175,25 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { centerGroup.appendChild(repoEl); } + // Changes summary shown next to the repo + if (changesSummary) { + const separator2 = $('span.agent-sessions-titlebar-separator'); + separator2.textContent = '\u00B7'; + centerGroup.appendChild(separator2); + + const changesEl = $('span.agent-sessions-titlebar-changes'); + + const addedEl = $('span.agent-sessions-titlebar-changes-added'); + addedEl.textContent = `+${changesSummary.insertions}`; + changesEl.appendChild(addedEl); + + const removedEl = $('span.agent-sessions-titlebar-changes-removed'); + removedEl.textContent = `-${changesSummary.deletions}`; + changesEl.appendChild(removedEl); + + centerGroup.appendChild(changesEl); + } + sessionPill.appendChild(centerGroup); // Click handler on pill - show sessions picker @@ -336,6 +355,24 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { menu.dispose(); } + /** + * Get the changes summary for the active session. + */ + private _getChangesSummary(): { insertions: number; deletions: number } | undefined { + const activeSession = this.activeSessionService.getActiveSession(); + if (!activeSession) { + return undefined; + } + + const agentSession = this.agentSessionsService.getSession(activeSession.resource); + const changes = agentSession?.changes; + if (!changes || !hasValidDiff(changes)) { + return undefined; + } + + return getAgentChangesSummary(changes); + } + private _showSessionsPicker(): void { const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined, { overrideSessionOpen: (session, openOptions) => this.activeSessionService.openSession(session.resource, openOptions) From ff7ffa542f145bbd0bad82c38ed1ba1c54f72ec3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 17:04:29 +0100 Subject: [PATCH 063/448] sessions - improve session hover title rendering and persistence (#298950) * fix - add title rendering for session hover * fix - update hover title rendering logic * fix - update hover persistence for session rendering * fix - update hover setup for agent sessions * fix - update hover handling in `agentSessions` --- .../sessions/contrib/sessions/browser/sessionsViewPane.ts | 2 +- .../chat/browser/agentSessions/agentSessionsControl.ts | 2 +- .../chat/browser/agentSessions/agentSessionsViewer.ts | 6 ++++-- .../componentFixtures/agentSessionsViewer.fixture.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 953bd0b26f9..c158a65cb6a 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -143,7 +143,7 @@ export class AgenticSessionsViewPane extends ViewPane { source: 'agentSessionsViewPane', filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, - disableHover: true, + useSimpleHover: true, showIsolationIcon: true, enableApprovalRow: true, getHoverPosition: () => this.getSessionHoverPosition(), diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 66977278aeb..36d5ff8263c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -41,7 +41,7 @@ export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOption readonly overrideStyles: IStyleOverride; readonly filter: IAgentSessionsFilter; readonly source: string; - readonly disableHover?: boolean; + readonly useSimpleHover?: boolean; readonly showIsolationIcon?: boolean; readonly enableApprovalRow?: boolean; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index ca655535956..c49765298f0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -87,7 +87,7 @@ interface IAgentSessionItemTemplate { } export interface IAgentSessionRendererOptions { - readonly disableHover?: boolean; + readonly useSimpleHover?: boolean; readonly showIsolationIcon?: boolean; getHoverPosition(): HoverPosition; } @@ -402,7 +402,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private renderHover(session: ITreeNode, template: IAgentSessionItemTemplate): void { - if (this.options.disableHover) { + if (this.options.useSimpleHover) { + const title = renderAsPlaintext(new MarkdownString(session.element.label)); + template.elementDisposable.add(this.hoverService.setupDelayedHover(template.element, { content: title, position: { hoverPosition: this.options.getHoverPosition() } }, { groupId: 'agent.sessions' })); return; } diff --git a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts index f88f88719f4..90ff4a6f730 100644 --- a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts @@ -68,7 +68,7 @@ function wrapAsTreeNode(element: T): ITreeNode { } const rendererOptions: IAgentSessionRendererOptions = { - disableHover: true, + useSimpleHover: true, getHoverPosition: () => HoverPosition.BELOW, }; From dd8539f530fac62eda0167c1462cce51a5491ee3 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 3 Mar 2026 08:28:10 -0800 Subject: [PATCH 064/448] Session window: apply patch to local --- extensions/git/src/commands.ts | 14 +++ .../browser/applyToParentRepo.contribution.ts | 89 ++++++++++++------- .../contrib/chat/browser/chatRepoInfo.ts | 2 +- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1fc850565de..010d34e4b01 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1061,6 +1061,20 @@ export class CommandCenter { await repo.pull(); } + @command('_git.applyPatch') + async applyPatch(repositoryPath: string, patchContent: string): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + const patchPath = path.join(os.tmpdir(), `vscode-patch-${Date.now()}.patch`); + const { promises: fsp } = await import('fs'); + try { + await fsp.writeFile(patchPath, patchContent, 'utf8'); + await repo.apply(patchPath, { threeWay: true }); + } finally { + await fsp.unlink(patchPath).catch(() => { }); + } + } + @command('git.init') async init(skipFolderPrompt = false): Promise { let repositoryPath: string | undefined = undefined; diff --git a/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts b/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts index 3b5ce530a7b..47a5b66183a 100644 --- a/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts +++ b/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts @@ -10,6 +10,7 @@ import { autorun } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -18,12 +19,13 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; +import { generateUnifiedDiff } from '../../../../workbench/contrib/chat/browser/chatRepoInfo.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; -import { isEqualOrParent, joinPath, relativePath } from '../../../../base/common/resources.js'; +import { isEqualOrParent, relativePath } from '../../../../base/common/resources.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { URI } from '../../../../base/common/uri.js'; @@ -89,6 +91,7 @@ class ApplyToParentRepoAction extends Action2 { const logService = accessor.get(ILogService); const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); + const commandService = accessor.get(ICommandService); const activeSession = sessionManagementService.getActiveSession(); if (!activeSession?.worktree || !activeSession?.repository) { @@ -104,9 +107,9 @@ class ApplyToParentRepoAction extends Action2 { return; } - let copiedCount = 0; - let deletedCount = 0; - let errorCount = 0; + // Generate a combined unified diff patch from all changes + const patchParts: string[] = []; + let fileCount = 0; for (const change of changes) { try { @@ -117,34 +120,54 @@ class ApplyToParentRepoAction extends Action2 { ? change.modifiedUri === undefined : false; + const originalUri = change.originalUri; + let relPath: string | undefined; + if (isDeletion) { - const originalUri = change.originalUri; if (originalUri && isEqualOrParent(toFileUri(originalUri), worktreeRoot)) { - const relPath = relativePath(worktreeRoot, toFileUri(originalUri)); - if (relPath) { - const targetUri = joinPath(repoRoot, relPath); - if (await fileService.exists(targetUri)) { - await fileService.del(targetUri); - deletedCount++; - } - } + relPath = relativePath(worktreeRoot, toFileUri(originalUri)); } } else { if (isEqualOrParent(toFileUri(modifiedUri), worktreeRoot)) { - const relPath = relativePath(worktreeRoot, toFileUri(modifiedUri)); - if (relPath) { - const targetUri = joinPath(repoRoot, relPath); - await fileService.copy(modifiedUri, targetUri, true); - copiedCount++; - } + relPath = relativePath(worktreeRoot, toFileUri(modifiedUri)); } } + + if (!relPath) { + continue; + } + + const changeType: 'added' | 'modified' | 'deleted' = isDeletion + ? 'deleted' + : originalUri ? 'modified' : 'added'; + + const diff = await generateUnifiedDiff( + fileService, + relPath, + originalUri, + modifiedUri, + changeType + ); + + if (diff) { + patchParts.push(diff); + fileCount++; + } } catch (err) { - logService.error('[ApplyToParentRepo] Failed to apply change', err); - errorCount++; + logService.error('[ApplyToParentRepo] Failed to generate diff for change', err); } } + if (patchParts.length === 0) { + notificationService.notify({ + severity: Severity.Info, + message: localize('applyToParentRepoNoDiffs', "No applicable changes to apply to parent repo."), + }); + return; + } + + const combinedPatch = patchParts.join('\n') + '\n'; + const openFolderAction = toAction({ id: 'applyToParentRepo.openFolder', label: localize('openInVSCode', "Open in VS Code"), @@ -168,21 +191,21 @@ class ApplyToParentRepoAction extends Action2 { } }); - const totalApplied = copiedCount + deletedCount; - if (errorCount > 0) { - notificationService.notify({ - severity: Severity.Warning, - message: totalApplied === 1 - ? localize('applyToParentRepoPartial1', "Applied 1 file to parent repo with {0} error(s).", errorCount) - : localize('applyToParentRepoPartialN', "Applied {0} files to parent repo with {1} error(s).", totalApplied, errorCount), - actions: { primary: [openFolderAction] } - }); - } else if (totalApplied > 0) { + try { + await commandService.executeCommand('_git.applyPatch', repoRoot.fsPath, combinedPatch); + notificationService.notify({ severity: Severity.Info, - message: totalApplied === 1 + message: fileCount === 1 ? localize('applyToParentRepoSuccess1', "Applied 1 file to parent repo.") - : localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", totalApplied), + : localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", fileCount), + actions: { primary: [openFolderAction] } + }); + } catch (err) { + logService.error('[ApplyToParentRepo] git apply failed', err); + notificationService.notify({ + severity: Severity.Warning, + message: localize('applyToParentRepoConflict', "Failed to apply patch to parent repo. The parent repo may have diverged — resolve conflicts manually."), actions: { primary: [openFolderAction] } }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts index 494c639a67a..b027ff798b9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts +++ b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts @@ -109,7 +109,7 @@ function determineChangeType(resource: ISCMResource, groupId: string): 'added' | * files is the presence/absence of a trailing newline (content otherwise identical), * no diff will be generated because VS Code's diff algorithm treats the lines as equal. */ -async function generateUnifiedDiff( +export async function generateUnifiedDiff( fileService: IFileService, relPath: string, originalUri: URI | undefined, From 55250cd46a4f514110ecf21051b175c0f5d06b0f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 17:35:28 +0100 Subject: [PATCH 065/448] sessions - disable background throttling (#298985) * sessions - disable background throttling * ccr * . --- src/vs/platform/windows/electron-main/windowImpl.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 7455376e3f4..07551319546 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { FileAccess, Schemas } from '../../../base/common/network.js'; import { getMarks, mark } from '../../../base/common/performance.js'; -import { isTahoeOrNewer, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { isTahoeOrNewer, isLinux, isMacintosh, isWindows, INodeProcess } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { release } from 'os'; @@ -702,11 +702,16 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.windowState = state; this.logService.trace('window#ctor: using window state', state); - const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, undefined, { + const webPreferences: electron.WebPreferences = { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js').fsPath, additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], - v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', - }); + v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none' + }; + if ((process as INodeProcess).isEmbeddedApp) { + webPreferences.backgroundThrottling = false; // disable for sub-app + } + + const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, undefined, webPreferences); // Create the browser window mark('code/willCreateCodeBrowserWindow'); From 278032fc752ce77b6cf3a47bd79ce49159d50445 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 4 Mar 2026 01:32:36 +0900 Subject: [PATCH 066/448] feat: support heap profile and snapshot capture for tsserver --- .../typescript-language-features/package.json | 52 +++++++++++++++++++ .../package.nls.json | 6 +++ .../src/configuration/configuration.ts | 48 +++++++++++++++++ .../src/tsServer/serverProcess.electron.ts | 18 +++++++ 4 files changed, 124 insertions(+) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index ed85772ade5..412986886aa 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -2556,6 +2556,16 @@ "TypeScript" ] }, + "js/ts.tsserver.diagnosticDir": { + "type": "string", + "markdownDescription": "%configuration.tsserver.diagnosticDir%", + "scope": "window", + "keywords": [ + "TypeScript", + "diagnostic", + "memory" + ] + }, "typescript.tsserver.maxTsServerMemory": { "type": "number", "default": 3072, @@ -2563,6 +2573,48 @@ "markdownDeprecationMessage": "%configuration.tsserver.maxTsServerMemory.unifiedDeprecationMessage%", "scope": "window" }, + "js/ts.tsserver.heapSnapshot": { + "type": "number", + "default": 0, + "minimum": 0, + "markdownDescription": "%configuration.tsserver.heapSnapshot%", + "scope": "window", + "keywords": [ + "TypeScript", + "memory", + "diagnostics" + ] + }, + "js/ts.tsserver.heapProfile": { + "type": "object", + "default": { + "enabled": false + }, + "markdownDescription": "%configuration.tsserver.heapProfile%", + "scope": "window", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.heapProfile.enabled%" + }, + "dir": { + "type": "string", + "description": "%configuration.tsserver.heapProfile.dir%" + }, + "interval": { + "type": "number", + "minimum": 1, + "description": "%configuration.tsserver.heapProfile.interval%" + } + }, + "keywords": [ + "TypeScript", + "memory", + "heap", + "profile" + ] + }, "js/ts.tsserver.watchOptions": { "description": "%configuration.tsserver.watchOptions%", "scope": "window", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 8c28dd87ccd..40c4081de54 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -126,6 +126,12 @@ "configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#js/ts.tsserver.node.path#` to run TS Server with a custom Node installation.", "configuration.tsserver.maxTsServerMemory.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.tsserver.maxMemory#` instead.", "configuration.tsserver.maxMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#js/ts.tsserver.node.path#` to run TS Server with a custom Node installation.", + "configuration.tsserver.diagnosticDir": "Directory where TypeScript server writes Node diagnostic output by passing `--diagnostic-dir`.", + "configuration.tsserver.heapSnapshot": "Controls how many near-heap-limit snapshots TypeScript server writes by passing `--heapsnapshot-near-heap-limit`. Set to `0` to disable.", + "configuration.tsserver.heapProfile": "Configures heap profiling for TypeScript server.", + "configuration.tsserver.heapProfile.enabled": "Enable heap profiling for TypeScript server by passing `--heap-prof`.", + "configuration.tsserver.heapProfile.dir": "Directory where TypeScript server writes heap profiles by passing `--heap-prof-dir`.", + "configuration.tsserver.heapProfile.interval": "Sampling interval in bytes for TypeScript server heap profiling by passing `--heap-prof-interval`.", "configuration.tsserver.experimental.enableProjectDiagnostics": "Enables project wide error reporting.", "configuration.tsserver.experimental.enableProjectDiagnostics.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.tsserver.experimental.enableProjectDiagnostics#` instead.", "typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Defaults to use VS Code's locale.", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index a557f08c024..ae43fda659e 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -110,6 +110,12 @@ export class ImplicitProjectConfiguration { } } +export interface TsServerHeapProfileConfiguration { + readonly enabled: boolean; + readonly dir: string | undefined; + readonly interval: number | undefined; +} + export interface TypeScriptServiceConfiguration { readonly locale: string | null; readonly globalTsdk: string | null; @@ -126,6 +132,9 @@ export interface TypeScriptServiceConfiguration { readonly enableDiagnosticsTelemetry: boolean; readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; + readonly diagnosticDir: string | undefined; + readonly heapSnapshot: number; + readonly heapProfile: TsServerHeapProfileConfiguration; readonly enablePromptUseWorkspaceTsdk: boolean; readonly useVsCodeWatcher: boolean; readonly watchOptions: Proto.WatchOptions | undefined; @@ -168,6 +177,9 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu enableDiagnosticsTelemetry: this.readEnableDiagnosticsTelemetry(), enableProjectDiagnostics: this.readEnableProjectDiagnostics(), maxTsServerMemory: this.readMaxTsServerMemory(), + diagnosticDir: this.readDiagnosticDir(), + heapSnapshot: this.readHeapSnapshot(), + heapProfile: this.readHeapProfileConfiguration(), enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(), useVsCodeWatcher: this.readUseVsCodeWatcher(configuration), watchOptions: this.readWatchOptions(), @@ -288,6 +300,42 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return Math.max(memoryInMB, minimumMaxMemory); } + protected readDiagnosticDir(): string | undefined { + const diagnosticDir = readUnifiedConfig('tsserver.diagnosticDir', undefined, { fallbackSection: 'typescript' }); + return typeof diagnosticDir === 'string' && diagnosticDir.length > 0 ? diagnosticDir : undefined; + } + + protected readHeapSnapshot(): number { + const defaultNearHeapLimitSnapshotCount = 0; + const nearHeapLimitSnapshotCount = readUnifiedConfig('tsserver.heapSnapshot', defaultNearHeapLimitSnapshotCount, { fallbackSection: 'typescript' }); + if (!Number.isSafeInteger(nearHeapLimitSnapshotCount)) { + return defaultNearHeapLimitSnapshotCount; + } + return Math.max(nearHeapLimitSnapshotCount, 0); + } + + private readHeapProfileConfiguration(): TsServerHeapProfileConfiguration { + const defaultHeapProfileConfiguration: TsServerHeapProfileConfiguration = { + enabled: false, + dir: undefined, + interval: undefined, + }; + + const rawConfig = readUnifiedConfig<{ enabled?: unknown; dir?: unknown; interval?: unknown }>('tsserver.heapProfile', defaultHeapProfileConfiguration, { fallbackSection: 'typescript' }); + + const enabled = typeof rawConfig.enabled === 'boolean' ? rawConfig.enabled : false; + const dir = typeof rawConfig.dir === 'string' && rawConfig.dir.length > 0 ? rawConfig.dir : undefined; + const interval = typeof rawConfig.interval === 'number' && Number.isSafeInteger(rawConfig.interval) && rawConfig.interval > 0 + ? rawConfig.interval + : undefined; + + return { + enabled, + dir, + interval, + }; + } + protected readEnablePromptUseWorkspaceTsdk(): boolean { return readUnifiedConfig('tsdk.promptToUseWorkspaceVersion', false, { fallbackSection: 'typescript', fallbackSubSectionNameOverride: 'enablePromptUseWorkspaceTsdk' }); } diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index 7dbde90f792..992cae925df 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -162,6 +162,24 @@ function getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptService args.push(`--max-old-space-size=${configuration.maxTsServerMemory}`); } + if (configuration.diagnosticDir) { + args.push(`--diagnostic-dir=${configuration.diagnosticDir}`); + } + + if (configuration.heapSnapshot > 0) { + args.push(`--heapsnapshot-near-heap-limit=${configuration.heapSnapshot}`); + } + + if (configuration.heapProfile.enabled) { + args.push('--heap-prof'); + if (configuration.heapProfile.dir) { + args.push(`--heap-prof-dir=${configuration.heapProfile.dir}`); + } + if (configuration.heapProfile.interval) { + args.push(`--heap-prof-interval=${configuration.heapProfile.interval}`); + } + } + return args; } From 08447b63aae07c490e92657dff4ec2fdfa3a1d9c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:50:57 +0100 Subject: [PATCH 067/448] =?UTF-8?q?refactor:=20remove=20unused=20agent=20f?= =?UTF-8?q?eedback=20line=20decoration=20contribution=20a=E2=80=A6=20(#298?= =?UTF-8?q?996)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: remove unused agent feedback line decoration contribution and associated styles --- .../browser/agentFeedback.contribution.ts | 1 - ...agentFeedbackLineDecorationContribution.ts | 169 ------------------ .../media/agentFeedbackLineDecoration.css | 29 --- 3 files changed, 199 deletions(-) delete mode 100644 src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts delete mode 100644 src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts index 7f5708a85d8..7ff02612cc1 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedback.contribution.ts @@ -5,7 +5,6 @@ import './agentFeedbackEditorInputContribution.js'; import './agentFeedbackEditorWidgetContribution.js'; -import './agentFeedbackLineDecorationContribution.js'; import './agentFeedbackOverviewRulerContribution.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts deleted file mode 100644 index 119bbad2fc0..00000000000 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackLineDecorationContribution.ts +++ /dev/null @@ -1,169 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/agentFeedbackLineDecoration.css'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { 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 addFeedbackHintDecoration = ModelDecorationOptions.register({ - description: 'agent-feedback-add-hint', - linesDecorationsClassName: `${ThemeIcon.asClassName(Codicon.add)} agent-feedback-add-hint`, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, -}); - -export class AgentFeedbackLineDecorationContribution extends Disposable implements IEditorContribution { - - static readonly ID = 'agentFeedback.lineDecorationContribution'; - - private _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._updateFeedbackLines())); - this._store.add(this._editor.onDidChangeModel(() => this._onModelChanged())); - this._store.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onMouseMove(e))); - this._store.add(this._editor.onMouseLeave(() => this._updateHintDecoration(-1))); - this._store.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onMouseDown(e))); - - this._resolveSession(); - this._updateFeedbackLines(); - } - - private _onModelChanged(): void { - this._updateHintDecoration(-1); - this._resolveSession(); - this._updateFeedbackLines(); - } - - private _resolveSession(): void { - const model = this._editor.getModel(); - if (!model) { - this._sessionResource = undefined; - return; - } - this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); - } - - private _updateFeedbackLines(): void { - if (!this._sessionResource) { - this._feedbackLines.clear(); - return; - } - - const feedbackItems = this._agentFeedbackService.getFeedback(this._sessionResource); - const lines = new Set(); - - for (const item of feedbackItems) { - const model = this._editor.getModel(); - if (!model || item.resourceUri.toString() !== model.uri.toString()) { - continue; - } - - lines.add(item.range.startLineNumber); - } - - this._feedbackLines = lines; - } - - private _onMouseMove(e: IEditorMouseEvent): void { - if (!this._sessionResource) { - this._updateHintDecoration(-1); - return; - } - - const isLineDecoration = e.target.type === MouseTargetType.GUTTER_LINE_DECORATIONS && !e.target.detail.isAfterLines; - const isContentArea = e.target.type === MouseTargetType.CONTENT_TEXT || e.target.type === MouseTargetType.CONTENT_EMPTY; - if (e.target.position - && (isLineDecoration || isContentArea) - && !this._feedbackLines.has(e.target.position.lineNumber) - ) { - this._updateHintDecoration(e.target.position.lineNumber); - } else { - this._updateHintDecoration(-1); - } - } - - private _updateHintDecoration(line: number): void { - if (line === this._hintLine) { - return; - } - - this._hintLine = line; - this._editor.changeDecorations(accessor => { - if (this._hintDecorationId) { - accessor.removeDecoration(this._hintDecorationId); - this._hintDecorationId = null; - } - if (line !== -1) { - this._hintDecorationId = accessor.addDecoration( - new Range(line, 1, line, 1), - addFeedbackHintDecoration, - ); - } - }); - } - - private _onMouseDown(e: IEditorMouseEvent): void { - if (!e.target.position - || e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS - || e.target.detail.isAfterLines - || !this._sessionResource - ) { - return; - } - - const lineNumber = e.target.position.lineNumber; - - // Lines with existing feedback - do nothing - if (this._feedbackLines.has(lineNumber)) { - return; - } - - // Select the line content and focus the editor - const model = this._editor.getModel(); - if (!model) { - return; - } - - const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); - const endColumn = model.getLineLastNonWhitespaceColumn(lineNumber); - if (startColumn === 0 || endColumn === 0) { - // Empty line - select the whole line range - this._editor.setSelection(new Selection(lineNumber, model.getLineMaxColumn(lineNumber), lineNumber, 1)); - } else { - this._editor.setSelection(new Selection(lineNumber, endColumn, lineNumber, startColumn)); - } - this._editor.focus(); - } - - override dispose(): void { - this._updateHintDecoration(-1); - super.dispose(); - } -} - -registerEditorContribution(AgentFeedbackLineDecorationContribution.ID, AgentFeedbackLineDecorationContribution, EditorContributionInstantiation.Eventually); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css deleted file mode 100644 index 6f503b0143f..00000000000 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackLineDecoration.css +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-editor .agent-feedback-line-decoration, -.monaco-editor .agent-feedback-add-hint { - border-radius: 3px; - display: flex !important; - align-items: center; - justify-content: center; - background-color: var(--vscode-editorHoverWidget-background); - cursor: pointer; - border: 1px solid var(--vscode-editorHoverWidget-border); - box-sizing: border-box; -} - -.monaco-editor .agent-feedback-line-decoration:hover, -.monaco-editor .agent-feedback-add-hint:hover { - background-color: var(--vscode-editorHoverWidget-border); -} - -.monaco-editor .agent-feedback-add-hint { - opacity: 0.7; -} - -.monaco-editor .agent-feedback-add-hint:hover { - opacity: 1; -} From 92de3b63d53e7574c9a7f7a0a85635b8bbdf6e34 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 4 Mar 2026 01:59:57 +0900 Subject: [PATCH 068/448] chore: apply feedback --- extensions/typescript-language-features/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 412986886aa..fb58ac53c25 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -30,7 +30,9 @@ "typescript.npm", "js/ts.tsserver.npm.path", "typescript.tsserver.nodePath", - "js/ts.tsserver.node.path" + "js/ts.tsserver.node.path", + "js/ts.tsserver.diagnosticDir", + "js/ts.tsserver.heapProfile" ] } }, @@ -2559,7 +2561,7 @@ "js/ts.tsserver.diagnosticDir": { "type": "string", "markdownDescription": "%configuration.tsserver.diagnosticDir%", - "scope": "window", + "scope": "machine", "keywords": [ "TypeScript", "diagnostic", @@ -2591,7 +2593,7 @@ "enabled": false }, "markdownDescription": "%configuration.tsserver.heapProfile%", - "scope": "window", + "scope": "machine", "properties": { "enabled": { "type": "boolean", From fda9390558e9facc07f8cdbd4fd706ad96449f5e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 18:27:01 +0100 Subject: [PATCH 069/448] Indicate when all sessions are filtered hidden (fix #296581) (#298979) * Indicate when all sessions are filtered hidden (fix #296581) * . * Update src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agentSessions/agentSessionsControl.ts | 60 +++++++++++++++++-- .../agentSessions/agentSessionsFilter.ts | 6 +- .../agentSessions/agentSessionsViewer.ts | 20 ++++++- .../media/agentsessionsviewer.css | 23 +++++++ .../viewPane/media/chatViewPane.css | 8 +++ .../agentSessionsDataSource.test.ts | 34 ++++++----- 6 files changed, 127 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 36d5ff8263c..727d81d2f94 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -8,7 +8,10 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; -import { $, append, EventHelper } from '../../../../../base/browser/dom.js'; +import { $, append, EventHelper, addDisposableListener, EventType, hide, setVisibility } from '../../../../../base/browser/dom.js'; +import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../../base/common/keyCodes.js'; +import { localize } from '../../../../../nls.js'; import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; @@ -71,6 +74,8 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo private sessionsContainer: HTMLElement | undefined; get element(): HTMLElement | undefined { return this.sessionsContainer; } + private emptyFilterMessage: HTMLElement | undefined; + private sessionsList: WorkbenchCompressibleAsyncDataTree | undefined; private sessionsListFindIsOpen = false; @@ -106,7 +111,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.focusedAgentSessionTypeContextKey = ChatContextKeys.agentSessionType.bindTo(this.contextKeyService); this.hasMultipleAgentSessionsSelectedContextKey = ChatContextKeys.hasMultipleAgentSessionsSelected.bindTo(this.contextKeyService); - this.createList(this.container); + this.create(this.container); this.registerListeners(); } @@ -140,9 +145,35 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } } - private createList(container: HTMLElement): void { + private create(container: HTMLElement): void { this.sessionsContainer = append(container, $('.agent-sessions-viewer')); + this.createEmptyFilterMessage(this.sessionsContainer); + this.createList(this.sessionsContainer); + } + + private createEmptyFilterMessage(container: HTMLElement): void { + this.emptyFilterMessage = append(container, $('.agent-sessions-empty-filter-message')); + hide(this.emptyFilterMessage); + + const span = append(this.emptyFilterMessage, $('span')); + span.textContent = localize('agentSessions.noFilterResults', "No matching sessions."); + + const link = append(this.emptyFilterMessage, $('span.reset-filter-link')); + link.textContent = localize('agentSessions.clearFilters', "Clear Filter"); + link.tabIndex = 0; + link.setAttribute('role', 'button'); + this._register(addDisposableListener(link, EventType.CLICK, () => this.options.filter.reset())); + this._register(addDisposableListener(link, EventType.KEY_DOWN, (e) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + EventHelper.stop(e, true); + this.options.filter.reset(); + } + })); + } + + private createList(container: HTMLElement): void { const collapseByDefault = (element: unknown) => { if (isAgentSessionSection(element)) { if (element.section === AgentSessionSection.More && !this.options.filter.getExcludes().read) { @@ -168,16 +199,17 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo const sorter = new AgentSessionsSorter(this.options); const approvalModel = this.options.enableApprovalRow ? this._register(this.instantiationService.createInstance(AgentSessionApprovalModel)) : undefined; const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options, approvalModel)); + const sessionFilter = this._register(new AgentSessionsDataSource(this.options.filter, sorter)); const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'AgentSessionsView', - this.sessionsContainer, + container, new AgentSessionsListDelegate(approvalModel), new AgentSessionsCompressionDelegate(), [ sessionRenderer, this.instantiationService.createInstance(AgentSessionSectionRenderer), ], - new AgentSessionsDataSource(this.options.filter, sorter), + sessionFilter, { accessibilityProvider: new AgentSessionsAccessibilityProvider(), dnd: this.instantiationService.createInstance(AgentSessionsDragAndDrop), @@ -202,6 +234,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } })); + this._register(sessionFilter.onDidGetChildren(count => { + this.updateEmptyFilterMessage(count); + })); + const model = this.agentSessionsService.model; this._register(this.options.filter.onDidChange(async () => { @@ -251,6 +287,20 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo })); } + private updateEmptyFilterMessage(visibleChildren: number): void { + if (!this.emptyFilterMessage || !this.sessionsList) { + return; + } + + const model = this.agentSessionsService.model; + const hasSessionsInModel = model.sessions.length > 0; + const hasVisibleChildren = visibleChildren > 0; + const isFilterActive = !this.options.filter.isDefault(); + + const showMessage = hasSessionsInModel && !hasVisibleChildren && isFilterActive; + setVisibility(showMessage, this.emptyFilterMessage); + } + private hasTodaySessions(): boolean { const startOfToday = new Date().setHours(0, 0, 0, 0); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 65bfc186494..c6cabc14a0d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -287,7 +287,7 @@ export class AgentSessionsFilter extends Disposable implements Required { +export class AgentSessionsDataSource extends Disposable implements IAsyncDataSource { private static readonly CAPPED_SESSIONS_LIMIT = 3; + private readonly _onDidGetChildren = this._register(new Emitter()); + readonly onDidGetChildren: Event = this._onDidGetChildren.event; + constructor( private readonly filter: IAgentSessionsFilter | undefined, private readonly sorter: ITreeSorter, - ) { } + ) { + super(); + } hasChildren(element: IAgentSessionsModel | AgentSessionListItem): boolean { @@ -741,6 +756,7 @@ export class AgentSessionsDataSource implements IAsyncDataSource { suite('AgentSessionsDataSource', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const ONE_DAY = 24 * 60 * 60 * 1000; const WEEK_THRESHOLD = 7 * ONE_DAY; // 7 days @@ -152,7 +152,9 @@ suite('AgentSessionsDataSource', () => { onDidChange: Event.None, groupResults: () => options.groupBy, exclude: options.exclude ?? (() => false), - getExcludes: () => ({ providers: [], states: [], archived: false, read: options.excludeRead ?? false }) + getExcludes: () => ({ providers: [], states: [], archived: false, read: options.excludeRead ?? false }), + isDefault: () => true, + reset: () => { }, }; } @@ -182,7 +184,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: undefined }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -202,7 +204,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -223,7 +225,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -244,7 +246,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -263,7 +265,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -281,7 +283,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -299,7 +301,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -319,7 +321,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -353,7 +355,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -382,7 +384,7 @@ suite('AgentSessionsDataSource', () => { test('empty sessions returns empty result', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel([]); const result = Array.from(dataSource.getChildren(mockModel)); @@ -399,7 +401,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -422,7 +424,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -456,7 +458,7 @@ suite('AgentSessionsDataSource', () => { excludeRead: true // Filtering to show only unread sessions }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -481,7 +483,7 @@ suite('AgentSessionsDataSource', () => { excludeRead: false // Not filtering to unread only }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); From 3eab7b9d79a81ff0b96a6c423bebe1d98cce5143 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:32:24 -0800 Subject: [PATCH 070/448] Bump actions/upload-artifact from 4 to 7 (#298952) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-darwin-test.yml | 6 +++--- .github/workflows/pr-linux-test.yml | 6 +++--- .github/workflows/pr-win32-test.yml | 6 +++--- .github/workflows/screenshot-test.yml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index c876d2a3782..56cd6e6ba2e 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -212,7 +212,7 @@ jobs: if: always() - name: Publish Crash Reports - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: failure() continue-on-error: true with: @@ -223,7 +223,7 @@ jobs: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - name: Publish Node Modules - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: failure() continue-on-error: true with: @@ -232,7 +232,7 @@ jobs: if-no-files-found: ignore - name: Publish Log Files - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() continue-on-error: true with: diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index df6ab20e586..7922ec107f9 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -258,7 +258,7 @@ jobs: if: always() - name: Publish Crash Reports - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: failure() continue-on-error: true with: @@ -269,7 +269,7 @@ jobs: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - name: Publish Node Modules - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: failure() continue-on-error: true with: @@ -278,7 +278,7 @@ jobs: if-no-files-found: ignore - name: Publish Log Files - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() continue-on-error: true with: diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index bd4a62d42fa..2bde317b480 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -249,7 +249,7 @@ jobs: if: always() - name: Publish Crash Reports - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: failure() continue-on-error: true with: @@ -260,7 +260,7 @@ jobs: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - name: Publish Node Modules - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: failure() continue-on-error: true with: @@ -269,7 +269,7 @@ jobs: if-no-files-found: ignore - name: Publish Log Files - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() continue-on-error: true with: diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index decfcf2a6f8..9c91702bccb 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -73,14 +73,14 @@ jobs: fi - name: Upload explorer artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: component-explorer path: /tmp/explorer-artifact/ - name: Upload screenshot report if: steps.compare.outcome == 'failure' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: screenshot-diff path: | From 146c698b5a393e85df8b74e8f974298cdc6c99f5 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 18:41:50 +0100 Subject: [PATCH 071/448] sessions - default settings tweaks (#299008) --- .../contrib/configuration/browser/configuration.contribution.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index aad4b934137..26e300b0529 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -46,6 +46,8 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.layoutControl.type': 'toggles', 'workbench.editor.useModal': 'all', 'workbench.panel.showLabels': false, + 'workbench.colorTheme': 'Experimental Dark', + 'search.quickOpen.includeHistory': false, 'window.menuStyle': 'custom', 'window.dialogStyle': 'custom', From 6c485b90d53d08c0ac9fa56205d03798d5ab3d86 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:45:17 -0800 Subject: [PATCH 072/448] chore: run npm audit fix (#298839) --- build/package-lock.json | 12 ++++---- package-lock.json | 62 ++++++++++++++++++++------------------ test/mcp/package-lock.json | 6 ++-- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index ec46db00b08..3dd620ab59e 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -2318,9 +2318,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -4526,9 +4526,9 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package-lock.json b/package-lock.json index f6af524389b..03648e4b2cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4191,9 +4191,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -4235,10 +4235,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4268,15 +4269,16 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -12403,10 +12405,11 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -15757,15 +15760,16 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -18532,9 +18536,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.105.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", - "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "version": "5.105.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", + "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", "dev": true, "license": "MIT", "dependencies": { @@ -18544,7 +18548,7 @@ "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", @@ -18562,7 +18566,7 @@ "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -18669,9 +18673,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index f3a0ceb0bff..6e7624dc0da 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -702,9 +702,9 @@ } }, "node_modules/hono": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", - "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", "license": "MIT", "engines": { "node": ">=16.9.0" From 96681dcd67d8babdf2295494e6bd1c312d44e595 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 09:51:51 -0800 Subject: [PATCH 073/448] mcp: add support for ui/download-file requests (#298838) * mcp: add support for ui/download-file requests - Adds McpUiDownloadFileRequest and McpUiDownloadFileResult types to modelContextProtocolApps.ts for MCP apps to request file downloads - Introduces IChatResponseResourceFileSystemProvider interface with an associate() method to store arbitrary data in the chat response filesystem and track it by session for cleanup - Extracts ChatResourceGroupWidget as a reusable component for rendering resource groups with save/download actions, used by both ChatToolOutputContentSubPart and MCP app downloads - Adds _handleDownloadFile() in ChatMcpAppModel to process download requests, supporting both inline EmbeddedResource and ResourceLink types from the MCP protocol - Adds download resource container in ChatMcpAppSubPart that renders downloaded resources as attachment pills with toolbar actions - Registers IChatResponseResourceFileSystemProvider as a singleton service Fixes #298836 (Commit message generated by Copilot) * mcp: add support for ui/download-file requests - Adds McpUiDownloadFileRequest and McpUiDownloadFileResult types - Introduces IChatResponseResourceFileSystemProvider interface - Extracts ChatResourceGroupWidget as reusable component - Adds _handleDownloadFile in ChatMcpAppModel - Adds download container in ChatMcpAppSubPart - Registers IChatResponseResourceFileSystemProvider singleton Fixes #298836 (Commit message generated by Copilot) * pr comments --- .../mcp/common/modelContextProtocolApps.ts | 33 +++ .../contrib/chat/browser/chat.contribution.ts | 4 +- .../chatResourceGroupWidget.ts | 241 ++++++++++++++++++ .../chatToolOutputContentSubPart.ts | 222 +--------------- .../media/chatConfirmationWidget.css | 17 ++ .../toolInvocationParts/chatMcpAppModel.ts | 55 +++- .../toolInvocationParts/chatMcpAppSubPart.ts | 26 ++ .../chatResponseResourceFileSystemProvider.ts | 67 ++++- 8 files changed, 441 insertions(+), 224 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatResourceGroupWidget.ts diff --git a/src/vs/platform/mcp/common/modelContextProtocolApps.ts b/src/vs/platform/mcp/common/modelContextProtocolApps.ts index 4569e8f25ac..86b891514e2 100644 --- a/src/vs/platform/mcp/common/modelContextProtocolApps.ts +++ b/src/vs/platform/mcp/common/modelContextProtocolApps.ts @@ -17,6 +17,7 @@ export namespace McpApps { | MCP.ReadResourceRequest | MCP.PingRequest | (McpUiOpenLinkRequest & MCP.JSONRPCRequest) + | (McpUiDownloadFileRequest & MCP.JSONRPCRequest) | (McpUiUpdateModelContextRequest & MCP.JSONRPCRequest) | (McpUiMessageRequest & MCP.JSONRPCRequest) | (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest) @@ -37,6 +38,7 @@ export namespace McpApps { | McpApps.McpUiInitializeResult | McpUiMessageResult | McpUiOpenLinkResult + | McpUiDownloadFileResult | McpUiRequestDisplayModeResult; export type HostNotification = @@ -223,6 +225,33 @@ export namespace McpApps { [key: string]: unknown; } + /** + * @description Request to download one or more files through the host. + * Uses standard MCP resource types: EmbeddedResource for inline content + * and ResourceLink for references the host resolves via resources/read. + */ + export interface McpUiDownloadFileRequest { + method: "ui/download-file"; + params: { + /** @description Resources to download, either inline or as links for the host to resolve. */ + contents: (MCP.EmbeddedResource | MCP.ResourceLink)[]; + }; + } + + /** + * @description Result from a download file request. + * @see {@link McpUiDownloadFileRequest} + */ + export interface McpUiDownloadFileResult { + /** @description True if the host rejected or failed to process the download. */ + isError?: boolean; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; + } + /** * @description Request to send a message to the host's chat interface. * @see {@link app.App.sendMessage} for the method that sends this request @@ -528,6 +557,8 @@ export namespace McpApps { updateModelContext?: McpUiSupportedContentBlockModalities; /** @description Host supports receiving content messages (ui/message) from the View. */ message?: McpUiSupportedContentBlockModalities; + /** @description Host supports file downloads (ui/download-file) from the View. */ + downloadFile?: {}; } /** @@ -734,4 +765,6 @@ export namespace McpApps { "ui/request-display-mode"; export const UPDATE_MODEL_CONTEXT_METHOD: McpUiUpdateModelContextRequest["method"] = "ui/update-model-context"; + export const DOWNLOAD_FILE_METHOD: McpUiDownloadFileRequest["method"] = + "ui/download-file"; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c1cb465e781..4fd8b274b75 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -37,7 +37,7 @@ import '../common/widget/chatColors.js'; import { IChatEditingService } from '../common/editing/chatEditingService.js'; import { IChatLayoutService } from '../common/widget/chatLayoutService.js'; import { ChatModeService, IChatMode, IChatModeService } from '../common/chatModes.js'; -import { ChatResponseResourceFileSystemProvider } from '../common/widget/chatResponseResourceFileSystemProvider.js'; +import { ChatResponseResourceFileSystemProvider, IChatResponseResourceFileSystemProvider } from '../common/widget/chatResponseResourceFileSystemProvider.js'; import { IChatService } from '../common/chatService/chatService.js'; import { ChatService } from '../common/chatService/chatServiceImpl.js'; import { IChatSessionsService } from '../common/chatSessionsService.js'; @@ -1735,7 +1735,7 @@ registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, Wo registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(ChatResponseResourceFileSystemProvider.ID, ChatResponseResourceFileSystemProvider, WorkbenchPhase.AfterRestored); +registerSingleton(IChatResponseResourceFileSystemProvider, ChatResponseResourceFileSystemProvider, InstantiationType.Eager); registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContributions, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatResourceGroupWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatResourceGroupWidget.ts new file mode 100644 index 00000000000..dcd85778a0e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatResourceGroupWidget.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { disposableTimeout } from '../../../../../../base/common/async.js'; +import { decodeBase64 } from '../../../../../../base/common/buffer.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { basename, joinPath } from '../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; +import { localize, localize2 } from '../../../../../../nls.js'; +import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; +import { IFileDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { INotificationService } from '../../../../../../platform/notification/common/notification.js'; +import { IProgressService, ProgressLocation } from '../../../../../../platform/progress/common/progress.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../../files/browser/fileConstants.js'; +import { getAttachableImageExtension } from '../../../common/model/chatModel.js'; +import { IChatRequestVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatAttachmentsContentPart } from './chatAttachmentsContentPart.js'; +import { IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js'; + +export interface IChatToolOutputResourceToolbarContext { + parts: IChatCollapsibleIODataPart[]; +} + +/** + * Delay in milliseconds before decoding base64 image data. + * This avoids expensive decode operations during scrolling. + */ +const IMAGE_DECODE_DELAY_MS = 100; + +/** + * A reusable widget for rendering a group of resource data parts (files, images) + * with attachment pills and a toolbar with save actions. + * + * Used by ChatToolOutputContentSubPart and ChatMcpAppSubPart (for download resources). + */ +export class ChatResourceGroupWidget extends Disposable { + public readonly domNode: HTMLElement; + + constructor( + parts: IChatCollapsibleIODataPart[], + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IFileService private readonly _fileService: IFileService, + ) { + super(); + + const el = dom.h('.chat-collapsible-io-resource-group', [ + dom.h('.chat-collapsible-io-resource-items@items'), + dom.h('.chat-collapsible-io-resource-actions@actions'), + ]); + + this.domNode = el.root; + this._fillInResourceGroup(parts, el.items, el.actions); + } + + private async _fillInResourceGroup(parts: IChatCollapsibleIODataPart[], itemsContainer: HTMLElement, actionsContainer: HTMLElement) { + // First pass: create entries immediately, using file placeholders for base64 images + const entries: IChatRequestVariableEntry[] = []; + const deferredImageParts: { index: number; part: IChatCollapsibleIODataPart }[] = []; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part.mimeType && getAttachableImageExtension(part.mimeType)) { + if (part.base64Value) { + // Defer base64 decode - use file placeholder for now + entries.push({ kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }); + deferredImageParts.push({ index: i, part }); + } else if (part.value) { + entries.push({ kind: 'image', id: generateUuid(), name: basename(part.uri), value: part.value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }); + } else { + const value = await this._fileService.readFile(part.uri).then(f => f.value.buffer, () => undefined); + if (!value) { + entries.push({ kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }); + } else { + entries.push({ kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }); + } + } + } else { + entries.push({ kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }); + } + } + + if (this._store.isDisposed) { + return; + } + + // Render attachments immediately with placeholders + const attachments = this._register(this._instantiationService.createInstance( + ChatAttachmentsContentPart, + { + variables: entries, + limit: 5, + contentReferences: undefined, + domNode: undefined + } + )); + + attachments.contextMenuHandler = (attachment, event) => { + const index = entries.indexOf(attachment); + const part = parts[index]; + if (part) { + event.preventDefault(); + event.stopPropagation(); + + this._contextMenuService.showContextMenu({ + menuId: MenuId.ChatToolOutputResourceContext, + menuActionOptions: { shouldForwardArgs: true }, + getAnchor: () => ({ x: event.pageX, y: event.pageY }), + getActionsContext: () => ({ parts: [part] } satisfies IChatToolOutputResourceToolbarContext), + }); + } + }; + + itemsContainer.appendChild(attachments.domNode!); + + const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, MenuId.ChatToolOutputResourceToolbar, { + menuOptions: { + shouldForwardArgs: true, + }, + })); + toolbar.context = { parts } satisfies IChatToolOutputResourceToolbarContext; + + // Second pass: decode base64 images asynchronously and update in place + if (deferredImageParts.length > 0) { + this._register(disposableTimeout(() => { + for (const { index, part } of deferredImageParts) { + try { + const value = decodeBase64(part.base64Value!).buffer; + entries[index] = { kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType!, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }; + } catch { + // Keep the file placeholder on decode failure + } + } + + // Update attachments in place + attachments.updateVariables(entries); + }, IMAGE_DECODE_DELAY_MS)); + } + } +} + + +class SaveResourcesAction extends Action2 { + public static readonly ID = 'chat.toolOutput.save'; + constructor() { + super({ + id: SaveResourcesAction.ID, + title: localize2('chat.saveResources', "Save..."), + icon: Codicon.cloudDownload, + menu: [{ + id: MenuId.ChatToolOutputResourceToolbar, + group: 'navigation', + order: 1 + }, { + id: MenuId.ChatToolOutputResourceContext, + }] + }); + } + + async run(accessor: ServicesAccessor, context: IChatToolOutputResourceToolbarContext) { + const fileDialog = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + const progressService = accessor.get(IProgressService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + const commandService = accessor.get(ICommandService); + const labelService = accessor.get(ILabelService); + const defaultFilepath = await fileDialog.defaultFilePath(); + + const savePart = async (part: IChatCollapsibleIODataPart, isFolder: boolean, uri: URI) => { + const target = isFolder ? joinPath(uri, basename(part.uri)) : uri; + try { + if (part.kind === 'data') { + await fileService.copy(part.uri, target, true); + } else { + // MCP doesn't support streaming data, so no sense trying + const contents = await fileService.readFile(part.uri); + await fileService.writeFile(target, contents.value); + } + } catch (e) { + notificationService.error(localize('chat.saveResources.error', "Failed to save {0}: {1}", basename(part.uri), e)); + } + }; + + const withProgress = async (thenReveal: URI, todo: (() => Promise)[]) => { + await progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 5_000, + title: localize('chat.saveResources.progress', "Saving resources..."), + }, async report => { + for (const task of todo) { + await task(); + report.report({ increment: 1, total: todo.length }); + } + }); + + if (workspaceContextService.isInsideWorkspace(thenReveal)) { + commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, thenReveal); + } else { + notificationService.info(localize('chat.saveResources.reveal', "Saved resources to {0}", labelService.getUriLabel(thenReveal))); + } + }; + + if (context.parts.length === 1) { + const part = context.parts[0]; + const uri = await fileDialog.pickFileToSave(joinPath(defaultFilepath, basename(part.uri))); + if (!uri) { + return; + } + await withProgress(uri, [() => savePart(part, false, uri)]); + } else { + const uris = await fileDialog.showOpenDialog({ + title: localize('chat.saveResources.title', "Pick folder to save resources"), + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: workspaceContextService.getWorkspace().folders[0]?.uri, + }); + + if (!uris?.length) { + return; + } + + await withProgress(uris[0], context.parts.map(part => () => savePart(part, true, uris[0]))); + } + } +} + +registerAction2(SaveResourcesAction); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts index 89cd89aea18..46171ef0343 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolOutputContentSubPart.ts @@ -4,39 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../../base/browser/dom.js'; -import { disposableTimeout } from '../../../../../../base/common/async.js'; -import { decodeBase64 } from '../../../../../../base/common/buffer.js'; -import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { basename, joinPath } from '../../../../../../base/common/resources.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../../base/common/uuid.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { localize, localize2 } from '../../../../../../nls.js'; -import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; -import { IFileDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { INotificationService } from '../../../../../../platform/notification/common/notification.js'; -import { IProgressService, ProgressLocation } from '../../../../../../platform/progress/common/progress.js'; -import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; -import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../../files/browser/fileConstants.js'; -import { getAttachableImageExtension } from '../../../common/model/chatModel.js'; import { IMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IMarkdownRendererService } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; -import { IChatRequestVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IChatCodeBlockInfo } from '../../chat.js'; import { CodeBlockPart, ICodeBlockData } from './codeBlockPart.js'; -import { ChatAttachmentsContentPart } from './chatAttachmentsContentPart.js'; import { IDisposableReference } from './chatCollections.js'; import { IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatCollapsibleIOPart, IChatCollapsibleIOCodePart, IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js'; +import { ChatResourceGroupWidget } from './chatResourceGroupWidget.js'; /** * A reusable component for rendering tool output consisting of code blocks and/or resources. @@ -52,8 +32,6 @@ export class ChatToolOutputContentSubPart extends Disposable { private readonly parts: ChatCollapsibleIOPart[], @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @IFileService private readonly _fileService: IFileService, @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, @IModelService private readonly modelService: IModelService, @ILanguageService private readonly languageService: ILanguageService, @@ -101,106 +79,8 @@ export class ChatToolOutputContentSubPart extends Disposable { } private addResourceGroup(parts: IChatCollapsibleIODataPart[], container: HTMLElement) { - const el = dom.h('.chat-collapsible-io-resource-group', [ - dom.h('.chat-collapsible-io-resource-items@items'), - dom.h('.chat-collapsible-io-resource-actions@actions'), - ]); - - this.fillInResourceGroup(parts, el.items, el.actions); - - container.appendChild(el.root); - return el.root; - } - - /** - * Delay in milliseconds before decoding base64 image data. - * This avoids expensive decode operations during scrolling. - */ - private static readonly IMAGE_DECODE_DELAY_MS = 100; - - private async fillInResourceGroup(parts: IChatCollapsibleIODataPart[], itemsContainer: HTMLElement, actionsContainer: HTMLElement) { - // First pass: create entries immediately, using file placeholders for base64 images - const entries: IChatRequestVariableEntry[] = []; - const deferredImageParts: { index: number; part: IChatCollapsibleIODataPart }[] = []; - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (part.mimeType && getAttachableImageExtension(part.mimeType)) { - if (part.base64Value) { - // Defer base64 decode - use file placeholder for now - entries.push({ kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }); - deferredImageParts.push({ index: i, part }); - } else if (part.value) { - entries.push({ kind: 'image', id: generateUuid(), name: basename(part.uri), value: part.value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }); - } else { - const value = await this._fileService.readFile(part.uri).then(f => f.value.buffer, () => undefined); - if (!value) { - entries.push({ kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }); - } else { - entries.push({ kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }); - } - } - } else { - entries.push({ kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }); - } - } - - if (this._store.isDisposed) { - return; - } - - // Render attachments immediately with placeholders - const attachments = this._register(this._instantiationService.createInstance( - ChatAttachmentsContentPart, - { - variables: entries, - limit: 5, - contentReferences: undefined, - domNode: undefined - } - )); - - attachments.contextMenuHandler = (attachment, event) => { - const index = entries.indexOf(attachment); - const part = parts[index]; - if (part) { - event.preventDefault(); - event.stopPropagation(); - - this._contextMenuService.showContextMenu({ - menuId: MenuId.ChatToolOutputResourceContext, - menuActionOptions: { shouldForwardArgs: true }, - getAnchor: () => ({ x: event.pageX, y: event.pageY }), - getActionsContext: () => ({ parts: [part] } satisfies IChatToolOutputResourceToolbarContext), - }); - } - }; - - itemsContainer.appendChild(attachments.domNode!); - - const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, MenuId.ChatToolOutputResourceToolbar, { - menuOptions: { - shouldForwardArgs: true, - }, - })); - toolbar.context = { parts } satisfies IChatToolOutputResourceToolbarContext; - - // Second pass: decode base64 images asynchronously and update in place - if (deferredImageParts.length > 0) { - this._register(disposableTimeout(() => { - for (const { index, part } of deferredImageParts) { - try { - const value = decodeBase64(part.base64Value!).buffer; - entries[index] = { kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType!, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }; - } catch { - // Keep the file placeholder on decode failure - } - } - - // Update attachments in place - attachments.updateVariables(entries); - }, ChatToolOutputContentSubPart.IMAGE_DECODE_DELAY_MS)); - } + const widget = this._register(this._instantiationService.createInstance(ChatResourceGroupWidget, parts)); + container.appendChild(widget.domNode); } private addCodeBlock(parts: IChatCollapsibleIOCodePart[], container: HTMLElement): void { @@ -253,97 +133,3 @@ export class ChatToolOutputContentSubPart extends Disposable { this._editorReferences.forEach(r => r.object.layout(width)); } } - -interface IChatToolOutputResourceToolbarContext { - parts: IChatCollapsibleIODataPart[]; -} - - - -class SaveResourcesAction extends Action2 { - public static readonly ID = 'chat.toolOutput.save'; - constructor() { - super({ - id: SaveResourcesAction.ID, - title: localize2('chat.saveResources', "Save As..."), - icon: Codicon.cloudDownload, - menu: [{ - id: MenuId.ChatToolOutputResourceToolbar, - group: 'navigation', - order: 1 - }, { - id: MenuId.ChatToolOutputResourceContext, - }] - }); - } - - async run(accessor: ServicesAccessor, context: IChatToolOutputResourceToolbarContext) { - const fileDialog = accessor.get(IFileDialogService); - const fileService = accessor.get(IFileService); - const notificationService = accessor.get(INotificationService); - const progressService = accessor.get(IProgressService); - const workspaceContextService = accessor.get(IWorkspaceContextService); - const commandService = accessor.get(ICommandService); - const labelService = accessor.get(ILabelService); - const defaultFilepath = await fileDialog.defaultFilePath(); - - const savePart = async (part: IChatCollapsibleIODataPart, isFolder: boolean, uri: URI) => { - const target = isFolder ? joinPath(uri, basename(part.uri)) : uri; - try { - if (part.kind === 'data') { - await fileService.copy(part.uri, target, true); - } else { - // MCP doesn't support streaming data, so no sense trying - const contents = await fileService.readFile(part.uri); - await fileService.writeFile(target, contents.value); - } - } catch (e) { - notificationService.error(localize('chat.saveResources.error', "Failed to save {0}: {1}", basename(part.uri), e)); - } - }; - - const withProgress = async (thenReveal: URI, todo: (() => Promise)[]) => { - await progressService.withProgress({ - location: ProgressLocation.Notification, - delay: 5_000, - title: localize('chat.saveResources.progress', "Saving resources..."), - }, async report => { - for (const task of todo) { - await task(); - report.report({ increment: 1, total: todo.length }); - } - }); - - if (workspaceContextService.isInsideWorkspace(thenReveal)) { - commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, thenReveal); - } else { - notificationService.info(localize('chat.saveResources.reveal', "Saved resources to {0}", labelService.getUriLabel(thenReveal))); - } - }; - - if (context.parts.length === 1) { - const part = context.parts[0]; - const uri = await fileDialog.pickFileToSave(joinPath(defaultFilepath, basename(part.uri))); - if (!uri) { - return; - } - await withProgress(uri, [() => savePart(part, false, uri)]); - } else { - const uris = await fileDialog.showOpenDialog({ - title: localize('chat.saveResources.title', "Pick folder to save resources"), - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - defaultUri: workspaceContextService.getWorkspace().folders[0]?.uri, - }); - - if (!uris?.length) { - return; - } - - await withProgress(uris[0], context.parts.map(part => () => savePart(part, true, uris[0]))); - } - } -} - -registerAction2(SaveResourcesAction); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css index cd5343e662e..02dfb90b545 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css @@ -193,6 +193,23 @@ } } +.mcp-app-downloads { + margin-top: 8px; + + .chat-collapsible-io-resource-group { + animation: mcpDownloadFadeIn 300ms ease-in; + } +} + +.mcp-app-downloads:empty { + display: none; +} + +@keyframes mcpDownloadFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + .chat-confirmation-widget2 { margin-bottom: 8px; border: 1px solid var(--vscode-chat-requestBorder); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts index 7541799ba29..4c83b8ac18a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts @@ -17,12 +17,15 @@ import { autorun, autorunSelfDisposable, IObservable, observableValue } from '.. import { basename } from '../../../../../../../base/common/resources.js'; import { isFalsyOrWhitespace } from '../../../../../../../base/common/strings.js'; import { hasKey, isDefined } from '../../../../../../../base/common/types.js'; +import { URI } from '../../../../../../../base/common/uri.js'; import { localize } from '../../../../../../../nls.js'; +import { IChatResponseResourceFileSystemProvider } from '../../../../common/widget/chatResponseResourceFileSystemProvider.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../../../../platform/product/common/productService.js'; import { IStorageService } from '../../../../../../../platform/storage/common/storage.js'; + import { IMcpAppResourceContent, McpToolCallUI } from '../../../../../mcp/browser/mcpToolCallUI.js'; import { McpResourceURI } from '../../../../../mcp/common/mcpTypes.js'; import { MCP } from '../../../../../mcp/common/modelContextProtocol.js'; @@ -32,6 +35,7 @@ import { IChatRequestVariableEntry } from '../../../../common/attachments/chatVa import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../../common/chatService/chatService.js'; import { isToolResultInputOutputDetails, IToolResult } from '../../../../common/tools/languageModelToolsService.js'; import { IChatWidgetService } from '../../../chat.js'; +import { IChatCollapsibleIODataPart } from '../chatToolInputOutputContentPart.js'; import { IMcpAppRenderData } from './chatMcpAppSubPart.js'; /** Storage key for persistent webview origins */ @@ -84,6 +88,10 @@ export class ChatMcpAppModel extends Disposable { private readonly _onDidChangeHeight = this._register(new Emitter()); public readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; + /** Accumulated download resource parts from ui/download-file calls */ + private readonly _downloadParts = observableValue(this, []); + public readonly downloadParts: IObservable = this._downloadParts; + /** Full host context for the MCP App */ public readonly hostContext: IObservable; @@ -97,6 +105,7 @@ export class ChatMcpAppModel extends Disposable { @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IWebviewService private readonly _webviewService: IWebviewService, @IStorageService storageService: IStorageService, + @IChatResponseResourceFileSystemProvider private readonly _chatResponseResourceFsProvider: IChatResponseResourceFileSystemProvider, @ILogService private readonly _logService: ILogService, @IProductService private readonly _productService: IProductService, @IOpenerService private readonly _openerService: IOpenerService, @@ -437,6 +446,10 @@ export class ChatMcpAppModel extends Disposable { result = await this._handleOpenLink(request.params); break; + case 'ui/download-file': + result = await this._handleDownloadFile(request.params); + break; + case 'ui/request-display-mode': // VS Code only supports inline display mode result = { mode: 'inline' } satisfies McpApps.McpUiRequestDisplayModeResult; @@ -543,7 +556,8 @@ export class ChatMcpAppModel extends Disposable { resourceLink: {}, resource: {}, structuredContent: {}, - } + }, + downloadFile: {}, }, hostContext: this.hostContext.get(), } satisfies Required; @@ -682,6 +696,45 @@ export class ChatMcpAppModel extends Disposable { widget?.delegateScrollFromMouseWheelEvent(evt as IMouseWheelEvent); } + private async _handleDownloadFile(params: McpApps.McpUiDownloadFileRequest['params']): Promise { + const newParts: IChatCollapsibleIODataPart[] = []; + let hadError = false; + + for (const content of params.contents) { + try { + if (content.type === 'resource') { + // EmbeddedResource — associate inline content with the chat response FS + const resource = content.resource; + const parsed = URI.parse(resource.uri); + + const data: Uint8Array | { base64: string } = hasKey(resource, { text: true }) + ? new TextEncoder().encode(resource.text) + : { base64: resource.blob }; + + const uri = this._chatResponseResourceFsProvider.associate(this.renderData.sessionResource, data, basename(parsed)); + newParts.push({ kind: 'data', mimeType: resource.mimeType, uri }); + } else if (content.type === 'resource_link') { + // ResourceLink — create a part with an MCP resource URI, resolved lazily on save + const mcpUri = McpResourceURI.fromServer( + { id: this.renderData.serverDefinitionId, label: '' }, + content.uri, + ); + newParts.push({ kind: 'data', mimeType: content.mimeType, uri: mcpUri }); + } + } catch (error) { + hadError = true; + this._logService.warn('[MCP App] Failed to process ui/download-file content', error); + } + } + + if (newParts.length > 0) { + const existing = this._downloadParts.get(); + this._downloadParts.set([...existing, ...newParts], undefined); + } + + return hadError ? { isError: true } : {}; + } + private async _handleOpenLink(params: McpApps.McpUiOpenLinkRequest['params']): Promise { const ok = await this._openerService.open(params.url); return { isError: !ok }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts index cf8f4d62ffa..db87356f004 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts @@ -21,6 +21,7 @@ import { IChatCodeBlockInfo } from '../../../chat.js'; import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { ChatErrorWidget } from '../chatErrorContentPart.js'; import { ChatProgressSubPart } from '../chatProgressContentPart.js'; +import { ChatResourceGroupWidget } from '../chatResourceGroupWidget.js'; import { ChatMcpAppModel, McpAppLoadState } from './chatMcpAppModel.js'; import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; @@ -63,6 +64,12 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { /** Current error node */ private _errorNode: HTMLElement | undefined; + /** Container for download resource pills */ + private readonly _downloadContainer: HTMLElement; + + /** Current resource group widget for downloads */ + private readonly _downloadWidget = this._register(new MutableDisposable()); + constructor( toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, onDidRemount: Event, @@ -81,6 +88,10 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { this._webviewContainer.style.height = '300px'; // Initial height, will be updated by model this.domNode.appendChild(this._webviewContainer); + // Download container — below webview, for ui/download-file resources + this._downloadContainer = dom.$('div.mcp-app-downloads'); + this.domNode.appendChild(this._downloadContainer); + const targetWindow = dom.getWindow(this.domNode); const getMaxHeight = () => maxWebviewHeightPct * targetWindow.innerHeight; const maxHeight = observableValue('mcpAppMaxHeight', getMaxHeight()); @@ -110,6 +121,21 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { this._updateContainerHeight(); })); + // Observe download parts and render resource group widget + this._register(autorun(reader => { + const parts = this._model.downloadParts.read(reader); + if (parts.length === 0) { + this._downloadWidget.clear(); + dom.clearNode(this._downloadContainer); + return; + } + + dom.clearNode(this._downloadContainer); + const widget = this._instantiationService.createInstance(ChatResourceGroupWidget, parts); + this._downloadWidget.value = widget; + this._downloadContainer.appendChild(widget.domNode); + })); + this._register(onDidRemount(() => { this._model.remount(); })); diff --git a/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts b/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts index d1d069424dd..48596accfdf 100644 --- a/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts @@ -6,21 +6,37 @@ import { decodeBase64, VSBuffer } from '../../../../../base/common/buffer.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { newWriteableStream, ReadableStreamEvents } from '../../../../../base/common/stream.js'; import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileService, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat } from '../../../../../platform/files/common/files.js'; -import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ChatResponseResource } from '../model/chatModel.js'; import { IChatService, IChatToolInvocation, IChatToolInvocationSerialized } from '../chatService/chatService.js'; import { isToolResultInputOutputDetails } from '../tools/languageModelToolsService.js'; +export const IChatResponseResourceFileSystemProvider = createDecorator('chatResponseResourceFileSystemProvider'); + +export interface IChatResponseResourceFileSystemProvider { + readonly _serviceBrand: undefined; + + /** + * Associates arbitrary data with a URI in the chat response resource filesystem. + * The data is scoped to the given session and automatically cleaned up when + * the session is disposed. + * Returns a URI that can later be read via the file service. + */ + associate(sessionResource: URI, data: Uint8Array | { base64: string }, name?: string): URI; +} + export class ChatResponseResourceFileSystemProvider extends Disposable implements - IWorkbenchContribution, + IChatResponseResourceFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability { - public static readonly ID = 'workbench.contrib.chatResponseResourceFileSystemProvider'; + declare readonly _serviceBrand: undefined; public readonly onDidChangeCapabilities = Event.None; public readonly onDidChangeFile = Event.None; @@ -32,12 +48,47 @@ export class ChatResponseResourceFileSystemProvider extends Disposable implement | FileSystemProviderCapabilities.FileAtomicRead | FileSystemProviderCapabilities.FileReadWrite; + /** In-memory store for data associated via {@link associate}, keyed by URI. */ + private readonly _associated = new ResourceMap(); + + /** Tracks which associated URIs belong to which session, for cleanup on dispose. */ + private readonly _sessionAssociations = new ResourceMap(); + constructor( @IChatService private readonly chatService: IChatService, @IFileService private readonly _fileService: IFileService ) { super(); this._register(this._fileService.registerProvider(ChatResponseResource.scheme, this)); + this._register(this.chatService.onDidDisposeSession(e => { + for (const sessionResource of e.sessionResource) { + const uris = this._sessionAssociations.get(sessionResource); + if (uris) { + for (const uri of uris) { + this._associated.delete(uri); + } + this._sessionAssociations.delete(sessionResource); + } + } + })); + } + + associate(sessionResource: URI, data: Uint8Array | { base64: string }, name?: string): URI { + const id = generateUuid(); + const uri = URI.from({ + scheme: ChatResponseResource.scheme, + path: `/assoc/${id}` + (name ? `/${name}` : ''), + }); + this._associated.set(uri, data); + + let set = this._sessionAssociations.get(sessionResource); + if (!set) { + set = new ResourceSet(); + this._sessionAssociations.set(sessionResource, set); + } + set.add(uri); + + return uri; } readFile(resource: URI): Promise { @@ -108,6 +159,16 @@ export class ChatResponseResourceFileSystemProvider extends Disposable implement } private lookupURI(uri: URI): Uint8Array | Promise { + const associated = this._associated.get(uri); + if (associated) { + if (associated instanceof Uint8Array) { + return associated; + } + const decoded = decodeBase64(associated.base64).buffer; + this._associated.set(uri, decoded); + return decoded; + } + const { result, index } = this.findMatchingInvocation(uri); const details = IChatToolInvocation.resultDetails(result); if (!isToolResultInputOutputDetails(details)) { From 611bd83d58812a459ef5f79f952b9a882302dd1f Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Tue, 3 Mar 2026 18:11:10 +0000 Subject: [PATCH 074/448] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extensions/theme-2026/themes/2026-dark.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index c56d58b2a8d..4e720722e8f 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -430,7 +430,8 @@ "settings": { "foreground": "#CDD9E5", "background": "#F47067", - "fontStyle": "italic underline" + "fontStyle": "italic underline", + "content": "^M" } }, { From 0380f89c2bc6f4b7def3b96c9ddda2763152a1b8 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 3 Mar 2026 10:15:29 -0800 Subject: [PATCH 075/448] Fix cut off corner border --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 455e1671b77..ff44e93ef7d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -818,6 +818,8 @@ have to be updated for changes to the rules above, or to support more deeply nes /* top padding is inside the editor widget */ width: 100%; position: relative; + /* Prevent contents from covering border corner */ + overflow: hidden; } /* Context usage widget container - positioned in the bottom toolbar */ From 8175067b9c35b985bccefac12686b4b44d4a69b3 Mon Sep 17 00:00:00 2001 From: Rohan Santhosh Date: Wed, 4 Mar 2026 02:18:45 +0800 Subject: [PATCH 076/448] docs: fix duplicated wording in proposed API comment (#298522) docs: fix duplicated wording in proposed API comment\n\nSigned-off-by: Rohan Santhosh --- .../vscode.proposed.languageModelToolSupportsModel.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts b/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts index ab481a3d9d8..7d3b546e9c2 100644 --- a/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts @@ -42,7 +42,7 @@ declare module 'vscode' { * Registers a language model tool along with its definition. Unlike {@link lm.registerTool}, * this does not require the tool to be present first in the extension's `package.json` contributions. * - * Multiple tools may be registered with the the same name using the API. In any given context, + * Multiple tools may be registered with the same name using the API. In any given context, * the most specific tool (based on the {@link LanguageModelToolDefinition.models}) will be used. * * @param definition The definition of the tool to register. From 29d5097f9b65f89081b4504f5a2c5bfa06c8a57e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 3 Mar 2026 13:19:10 -0500 Subject: [PATCH 077/448] support multiple questions coming in for chat (#299006) fix #297408 --- .../media/chatQuestionCarousel.css | 3 + .../chat/browser/widget/chatListRenderer.ts | 5 +- .../browser/widget/input/chatInputPart.ts | 119 +++++++++++------- 3 files changed, 78 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 28f9603b669..00e0f587a8f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -31,6 +31,9 @@ .interactive-session .interactive-input-part > .chat-question-carousel-widget-container { width: 100%; position: relative; + display: flex; + flex-direction: column; + gap: 8px; } /* container and header */ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index e4fc306046d..e5952be47f3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -2178,8 +2178,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); - private readonly _chatQuestionCarouselWidget = this._register(new MutableDisposable()); - private readonly _chatQuestionCarouselDisposables = this._register(new DisposableStore()); - private _currentQuestionCarouselResponseId: string | undefined; - private _currentQuestionCarouselSessionResource: URI | undefined; + private readonly _chatQuestionCarouselWidgets = this._register(new DisposableMap()); + private readonly _questionCarouselResponseIds = new Map(); + private readonly _questionCarouselSessionResources = new Map(); private _hasQuestionCarouselContextKey: IContextKey | undefined; private readonly _chatEditingTodosDisposables = this._register(new DisposableStore()); private _lastEditingSessionResource: URI | undefined; @@ -1919,7 +1917,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.refreshChatSessionPickers(); this.tryUpdateWidgetController(); this.updateContextUsageWidget(); - if (this._currentQuestionCarouselSessionResource && (!e.currentSessionResource || !isEqual(this._currentQuestionCarouselSessionResource, e.currentSessionResource))) { + let hasMatchingResource = false; + if (e.currentSessionResource) { + for (const r of this._questionCarouselSessionResources.values()) { + if (isEqual(r, e.currentSessionResource)) { + hasMatchingResource = true; + break; + } + } + } + if (this._questionCarouselSessionResources.size > 0 && (!e.currentSessionResource || !hasMatchingResource)) { this.clearQuestionCarousel(); } @@ -2585,60 +2592,74 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge renderQuestionCarousel(carousel: IChatQuestionCarousel, context: IChatContentPartRenderContext, options: IChatQuestionCarouselOptions): ChatQuestionCarouselPart { - if (this._chatQuestionCarouselWidget.value) { - const existingCarousel = this._chatQuestionCarouselWidget.value; - const existingResolveId = existingCarousel.carousel.resolveId; - if (existingResolveId && carousel.resolveId && existingResolveId === carousel.resolveId) { - return existingCarousel; - } + const carouselKey = carousel.resolveId ?? `${isResponseVM(context.element) ? context.element.requestId : ''}_${context.contentIndex}`; - // Complete the old carousel's completion promise as skipped before clearing - // This prevents the askQuestions tool from hanging when parallel subagents invoke it - const oldCarousel = existingCarousel.carousel; - if (oldCarousel instanceof ChatQuestionCarouselData && !oldCarousel.completion.isSettled) { - oldCarousel.completion.complete({ answers: undefined }); - } - - this.clearQuestionCarousel(); + // If a carousel with the same key already exists, return it + const existing = this._chatQuestionCarouselWidgets.get(carouselKey); + if (existing) { + return existing; } - // track the response id and session - this._currentQuestionCarouselResponseId = isResponseVM(context.element) ? context.element.requestId : undefined; - this._currentQuestionCarouselSessionResource = isResponseVM(context.element) ? context.element.sessionResource : undefined; + // Track the response id and session for this carousel + if (isResponseVM(context.element)) { + this._questionCarouselResponseIds.set(carouselKey, context.element.requestId); + this._questionCarouselSessionResources.set(carouselKey, context.element.sessionResource); + } - const part = this._chatQuestionCarouselDisposables.add( - this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, options) - ); - this._chatQuestionCarouselWidget.value = part; + const part = this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, options); + this._chatQuestionCarouselWidgets.set(carouselKey, part); this._hasQuestionCarouselContextKey?.set(true); - dom.clearNode(this.chatQuestionCarouselContainer); dom.append(this.chatQuestionCarouselContainer, part.domNode); return part; } - clearQuestionCarousel(responseId?: string): void { - if (responseId && this._currentQuestionCarouselResponseId !== responseId) { - return; + clearQuestionCarousel(responseId?: string, resolveId?: string): void { + if (resolveId !== undefined) { + // Remove a specific carousel by resolveId + const part = this._chatQuestionCarouselWidgets.get(resolveId); + if (part) { + part.domNode.remove(); + this._chatQuestionCarouselWidgets.deleteAndDispose(resolveId); + } + this._questionCarouselResponseIds.delete(resolveId); + this._questionCarouselSessionResources.delete(resolveId); + } else if (responseId !== undefined) { + // Remove all carousels associated with a given responseId + for (const [key, rid] of this._questionCarouselResponseIds) { + if (rid === responseId) { + const part = this._chatQuestionCarouselWidgets.get(key); + if (part) { + part.domNode.remove(); + this._chatQuestionCarouselWidgets.deleteAndDispose(key); + } + this._questionCarouselResponseIds.delete(key); + this._questionCarouselSessionResources.delete(key); + } + } + } else { + // Clear all carousels + this._chatQuestionCarouselWidgets.clearAndDisposeAll(); + this._questionCarouselResponseIds.clear(); + this._questionCarouselSessionResources.clear(); + dom.clearNode(this.chatQuestionCarouselContainer); } - this._chatQuestionCarouselDisposables.clear(); - this._chatQuestionCarouselWidget.clear(); - this._currentQuestionCarouselResponseId = undefined; - this._currentQuestionCarouselSessionResource = undefined; - this._hasQuestionCarouselContextKey?.set(false); - dom.clearNode(this.chatQuestionCarouselContainer); - } - get questionCarouselResponseId(): string | undefined { - return this._currentQuestionCarouselResponseId; + this._hasQuestionCarouselContextKey?.set(this._chatQuestionCarouselWidgets.size > 0); } get questionCarousel(): ChatQuestionCarouselPart | undefined { - return this._chatQuestionCarouselWidget.value; + // Return the focused carousel, or the first one + for (const part of this._chatQuestionCarouselWidgets.values()) { + if (part.hasFocus()) { + return part; + } + } + return this._chatQuestionCarouselWidgets.size > 0 ? this._chatQuestionCarouselWidgets.values().next().value : undefined; } focusQuestionCarousel(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; + const carousel = this.questionCarousel; if (carousel) { carousel.focus(); return true; @@ -2647,17 +2668,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } isQuestionCarouselFocused(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; - return carousel?.hasFocus() ?? false; + for (const part of this._chatQuestionCarouselWidgets.values()) { + if (part.hasFocus()) { + return true; + } + } + return false; } navigateToPreviousQuestion(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; + const carousel = this.questionCarousel; return carousel?.navigateToPreviousQuestion() ?? false; } navigateToNextQuestion(): boolean { - const carousel = this._chatQuestionCarouselWidget.value; + const carousel = this.questionCarousel; return carousel?.navigateToNextQuestion() ?? false; } From 4471d7e9d1b0843b35f88b99c5271245df9f9972 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 3 Mar 2026 10:37:28 -0800 Subject: [PATCH 078/448] Support displaying hooks by target, compat refactor (#298798) --- .../api/common/extHostChatAgents2.ts | 4 +- .../api/common/extHostTypeConverters.ts | 4 +- src/vs/workbench/api/common/extHostTypes.ts | 2 +- .../aiCustomizationListWidget.ts | 5 +- .../aiCustomizationManagementEditor.ts | 53 ++---- .../chatSessions/chatSessions.contribution.ts | 2 +- .../contrib/chat/browser/chatSlashCommands.ts | 2 +- .../chat/browser/promptSyntax/hookActions.ts | 111 +++++++---- .../chat/browser/promptSyntax/hookUtils.ts | 7 +- .../promptSyntax/newPromptFileActions.ts | 4 +- .../promptToolsCodeLensProvider.ts | 4 +- .../tools/languageModelToolsService.ts | 2 +- .../chatContentParts/chatHookContentPart.ts | 4 +- .../chat/browser/widget/chatListRenderer.ts | 2 +- .../browser/widget/input/chatInputPart.ts | 2 +- .../input/editor/chatInputCompletions.ts | 3 +- .../widget/input/modePickerActionItem.ts | 3 +- .../contrib/chat/common/chatModes.ts | 3 +- .../chat/common/chatService/chatService.ts | 2 +- .../common/chatService/chatServiceImpl.ts | 4 +- .../chat/common/chatSessionsService.ts | 2 +- .../chat/common/participants/chatAgents.ts | 4 +- .../common/participants/chatSlashCommands.ts | 2 +- .../chat/common/plugins/agentPluginService.ts | 3 +- .../common/promptSyntax/hookClaudeCompat.ts | 24 +-- .../common/promptSyntax/hookCompatibility.ts | 3 +- .../promptSyntax/hookCopilotCliCompat.ts | 5 +- .../chat/common/promptSyntax/hookSchema.ts | 172 +++--------------- .../chat/common/promptSyntax/hookTypes.ts | 122 +++++++++++++ .../promptHeaderAutocompletion.ts | 4 +- .../languageProviders/promptHovers.ts | 4 +- .../languageProviders/promptValidator.ts | 4 +- .../common/promptSyntax/promptFileParser.ts | 2 +- .../chat/common/promptSyntax/promptTypes.ts | 6 + .../promptSyntax/service/promptsService.ts | 13 +- .../service/promptsServiceImpl.ts | 41 ++--- .../tools/builtinTools/runSubagentTool.ts | 4 +- .../promptHeaderAutocompletion.test.ts | 4 +- .../languageProviders/promptHovers.test.ts | 4 +- .../languageProviders/promptValidator.test.ts | 4 +- .../chat/test/common/chatModeService.test.ts | 3 +- .../test/common/mockChatSessionsService.ts | 2 +- .../promptSyntax/hookClaudeCompat.test.ts | 2 +- .../promptSyntax/hookCompatibility.test.ts | 2 +- .../service/promptsService.test.ts | 6 +- .../builtinTools/runSubagentTool.test.ts | 3 +- src/vscode-dts/vscode.proposed.chatHooks.d.ts | 2 +- 47 files changed, 342 insertions(+), 328 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/hookTypes.ts diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 06553d2cb0f..7c5acd5cf88 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -22,7 +22,7 @@ import { ILogService } from '../../../platform/log/common/log.js'; import { isChatViewTitleActionContext } from '../../contrib/chat/common/actions/chatActions.js'; import { IChatAgentRequest, IChatAgentResult, IChatAgentResultTimings, UserSelectedTools } from '../../contrib/chat/common/participants/chatAgents.js'; import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService/chatService.js'; -import { IChatRequestHooks } from '../../contrib/chat/common/promptSyntax/hookSchema.js'; +import { ChatRequestHooks } from '../../contrib/chat/common/promptSyntax/hookSchema.js'; import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; @@ -456,7 +456,7 @@ interface InFlightChatRequest { requestId: string; extRequest: vscode.ChatRequest; extension: IRelaxedExtensionDescription; - hooks?: IChatRequestHooks; + hooks?: ChatRequestHooks; yieldRequested: boolean; } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 841a35e48d2..61a6ce830c2 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -47,7 +47,7 @@ import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js' import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; -import { IChatRequestHooks, IHookCommand, resolveEffectiveCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js'; +import { ChatRequestHooks, IHookCommand, resolveEffectiveCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js'; import { IToolInvocationContext, IToolResult, IToolResultInputOutputDetails, IToolResultOutputDetails, ToolDataSource, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; @@ -4098,7 +4098,7 @@ export namespace SourceControlInputBoxValidationType { } export namespace ChatRequestHooksConverter { - export function to(hooks: IChatRequestHooks): vscode.ChatRequestHooks { + export function to(hooks: ChatRequestHooks): vscode.ChatRequestHooks { const result: Record = {}; for (const [hookType, commands] of Object.entries(hooks)) { if (!commands || commands.length === 0) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 56f75132142..bd97ee3ffbb 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -30,7 +30,7 @@ import { SnippetString } from './extHostTypes/snippetString.js'; import { SymbolKind, SymbolTag } from './extHostTypes/symbolInformation.js'; import { TextEdit } from './extHostTypes/textEdit.js'; import { WorkspaceEdit } from './extHostTypes/workspaceEdit.js'; -import { HookTypeValue } from '../../contrib/chat/common/promptSyntax/hookSchema.js'; +import { HookTypeValue } from '../../contrib/chat/common/promptSyntax/hookTypes.js'; export { CodeActionKind } from './extHostTypes/codeActionKind.js'; export { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 1e9cccddcf8..474df4e970b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -42,7 +42,8 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { IPathService } from '../../../../services/path/common/pathService.js'; import { generateCustomizationDebugReport } from './aiCustomizationDebugPanel.js'; import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js'; -import { HOOK_TYPES, formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js'; +import { formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js'; +import { HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Schemas } from '../../../../../base/common/network.js'; import { OS } from '../../../../../base/common/platform.js'; @@ -793,7 +794,7 @@ export class AICustomizationListWidget extends Disposable { if (hooks.size > 0) { parsedHooks = true; for (const [hookType, entry] of hooks) { - const hookMeta = HOOK_TYPES.find(h => h.id === hookType); + const hookMeta = HOOK_METADATA[hookType]; for (let i = 0; i < entry.hooks.length; i++) { const hook = entry.hooks[i]; const cmdLabel = formatHookCommandLabel(hook, OS); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 7b3d437b0ef..e3b8f65df50 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -26,7 +26,7 @@ import { IListVirtualDelegate, IListRenderer } from '../../../../../base/browser import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { basename, isEqual, joinPath } from '../../../../../base/common/resources.js'; +import { basename, isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { registerColor } from '../../../../../platform/theme/common/colorRegistry.js'; import { PANEL_BORDER } from '../../../../common/theme.js'; @@ -47,7 +47,7 @@ import { } from './aiCustomizationManagement.js'; import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from './aiCustomizationIcons.js'; import { ChatModelsWidget } from '../chatManagement/chatModelsWidget.js'; -import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../promptSyntax/newPromptFileActions.js'; import { showConfigureHooksQuickPick } from '../promptSyntax/hookActions.js'; @@ -60,12 +60,8 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditorOptions.js'; import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { HOOKS_SOURCE_FOLDER } from '../../common/promptSyntax/config/promptFileLocations.js'; -import { COPILOT_CLI_HOOK_TYPE_MAP } from '../../common/promptSyntax/hookSchema.js'; import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js'; import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; @@ -194,7 +190,6 @@ export class AICustomizationManagementEditor extends EditorPane { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, - @IFileService private readonly fileService: IFileService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IHoverService private readonly hoverService: IHoverService, ) { @@ -583,15 +578,21 @@ export class AICustomizationManagementEditor extends EditorPane { if (type === PromptsType.hook) { if (this.workspaceService.isSessionsWindow) { - // Sessions: directly create a Copilot CLI format hooks file - await this.createCopilotCliHookFile(); - } else { - // Core: show the configure hooks quick pick + // Sessions: show hooks filtered to Copilot CLI (GitHub Copilot) hook types await this.instantiationService.invokeFunction(showConfigureHooksQuickPick, { openEditor: async (resource) => { await this.showEmbeddedEditor(resource, basename(resource), true); return; }, + target: Target.GitHubCopilot, + }); + } else { + // Core: use the default core behaviour + await this.instantiationService.invokeFunction(showConfigureHooksQuickPick, { + openEditor: async (resource) => { + await this.showEmbeddedEditor(resource, basename(resource), true); + return; + } }); } return; @@ -624,36 +625,6 @@ export class AICustomizationManagementEditor extends EditorPane { void this.listWidget.refresh(); } - /** - * Ensures a Copilot CLI format hooks file exists (.github/hooks/hooks.json), - * then opens the configure hooks quick pick. - */ - private async createCopilotCliHookFile(): Promise { - const projectRoot = this.workspaceService.getActiveProjectRoot(); - if (!projectRoot) { - return; - } - - const hookFileUri = joinPath(projectRoot, HOOKS_SOURCE_FOLDER, 'hooks.json'); - - // Create the file with all hook events if it doesn't exist - try { - await this.fileService.stat(hookFileUri); - } catch { - // Derive hook event names from the schema so new events are automatically included - const hooks: Record = {}; - for (const eventName of Object.keys(COPILOT_CLI_HOOK_TYPE_MAP)) { - hooks[eventName] = [{ type: 'command', bash: '' }]; - } - const hooksContent = { version: 1, hooks }; - const jsonContent = JSON.stringify(hooksContent, null, '\t'); - await this.fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); - } - - await this.showEmbeddedEditor(hookFileUri, basename(hookFileUri), true); - void this.listWidget.refresh(); - } - override updateStyles(): void { const borderColor = this.theme.getColor(aiCustomizationManagementSashBorder); if (borderColor) { diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 4f53329ca1b..10dc9a17393 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -50,7 +50,7 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { LocalChatSessionUri } from '../../common/model/chatUri.js'; import { assertNever } from '../../../../../base/common/assert.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { Target } from '../../common/promptSyntax/service/promptsService.js'; +import { Target } from '../../common/promptSyntax/promptTypes.js'; const extensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatSessions', diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 9b379603530..d1cc0bba474 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -32,7 +32,7 @@ import { globalAutoApproveDescription } from './tools/languageModelToolsService.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; -import { Target } from '../common/promptSyntax/service/promptsService.js'; +import { Target } from '../common/promptSyntax/promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; export class ChatSlashCommandsContribution extends Disposable { diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index 00255f74508..db3d6f6acc2 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -19,11 +19,12 @@ import { Action2, registerAction2 } from '../../../../../platform/actions/common import { Codicon } from '../../../../../base/common/codicons.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; -import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; -import { HOOK_TYPES, HookType, getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js'; +import { HOOK_METADATA, HOOKS_BY_TARGET, HookType, IHookTypeMeta } from '../../common/promptSyntax/hookTypes.js'; +import { getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js'; import { getCopilotCliHookTypeName, resolveCopilotCliHookType } from '../../common/promptSyntax/hookCopilotCliCompat.js'; import { getHookSourceFormat, HookSourceFormat, buildNewHookEntry } from '../../common/promptSyntax/hookCompatibility.js'; import { getClaudeHookTypeName, resolveClaudeHookType } from '../../common/promptSyntax/hookClaudeCompat.js'; @@ -38,7 +39,7 @@ import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browse import { Range } from '../../../../../editor/common/core/range.js'; import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; -import { OS } from '../../../../../base/common/platform.js'; +import { OperatingSystem, OS } from '../../../../../base/common/platform.js'; /** * Action ID for the `Configure Hooks` action. @@ -46,7 +47,8 @@ import { OS } from '../../../../../base/common/platform.js'; const CONFIGURE_HOOKS_ACTION_ID = 'workbench.action.chat.configure.hooks'; interface IHookTypeQuickPickItem extends IQuickPickItem { - readonly hookType: typeof HOOK_TYPES[number]; + readonly hookType: HookType; + readonly hookTypeMeta: IHookTypeMeta; } interface IHookQuickPickItem extends IQuickPickItem { @@ -296,15 +298,17 @@ const enum Step { } /** - * Optional callbacks for customizing the hook creation and opening behaviour. + * Optional callbacks and settings for customizing the hook creation and opening behaviour. * The agentic editor passes these to open hooks in the embedded editor and * track worktree files for auto-commit. */ -export interface IHookQuickPickCallbacks { +export interface IHookQuickPickOptions { /** Override how the hook file is opened. If not provided, uses editorService.openEditor. */ readonly openEditor?: (resource: URI, options?: { selection?: ITextEditorSelection }) => Promise; /** Called after a new hook file is created on disk. */ readonly onHookFileCreated?: (uri: URI) => void; + /** Filter the displayed hook types to those supported by the given target. */ + readonly target?: Target; } /** @@ -313,7 +317,7 @@ export interface IHookQuickPickCallbacks { */ export async function showConfigureHooksQuickPick( accessor: ServicesAccessor, - callbacks?: IHookQuickPickCallbacks, + options?: IHookQuickPickOptions, ): Promise { const promptsService = accessor.get(IPromptsService); const quickInputService = accessor.get(IQuickInputService); @@ -379,18 +383,52 @@ export async function showConfigureHooksQuickPick( while (true) { switch (step) { case Step.SelectHookType: { - // Step 1: Show all lifecycle events with hook counts - const hookTypeItems: IHookTypeQuickPickItem[] = HOOK_TYPES.map(hookType => { - const count = hookCountByType.get(hookType.id) ?? 0; + // Step 1: Show lifecycle events with hook counts, filtered by target + const makeItem = ([hookType, meta]: [HookType, IHookTypeMeta]): IHookTypeQuickPickItem => { + const count = hookCountByType.get(hookType) ?? 0; const countLabel = count > 0 ? ` (${count})` : ''; return { - label: `${hookType.label}${countLabel}`, - description: hookType.description, - hookType + label: `${meta.label}${countLabel}`, + description: meta.description, + hookType, + hookTypeMeta: meta }; - }); + }; - picker.items = hookTypeItems; + let pickerItems: (IHookTypeQuickPickItem | IQuickPickSeparator)[]; + + if (options?.target) { + // Filtered to a specific target + const targetHookTypes = new Set(Object.values(HOOKS_BY_TARGET[options.target])); + pickerItems = (Object.entries(HOOK_METADATA) as [HookType, IHookTypeMeta][]) + .filter(([hookType]) => targetHookTypes.has(hookType)) + .map(makeItem); + } else { + // No target: group into Default (shared), VS Code Only, Copilot CLI Only + const vscodeTypes = new Set(Object.values(HOOKS_BY_TARGET[Target.VSCode])); + const copilotTypes = new Set(Object.values(HOOKS_BY_TARGET[Target.GitHubCopilot])); + const allEntries = Object.entries(HOOK_METADATA) as [HookType, IHookTypeMeta][]; + + const shared = allEntries.filter(([h]) => vscodeTypes.has(h) && copilotTypes.has(h)); + const vscodeOnly = allEntries.filter(([h]) => vscodeTypes.has(h) && !copilotTypes.has(h)); + const copilotOnly = allEntries.filter(([h]) => !vscodeTypes.has(h) && copilotTypes.has(h)); + + pickerItems = []; + if (shared.length > 0) { + pickerItems.push({ type: 'separator', label: localize('hookSection.default', "Local/Copilot CLI Agents") }); + pickerItems.push(...shared.map(makeItem)); + } + if (vscodeOnly.length > 0) { + pickerItems.push({ type: 'separator', label: localize('hookSection.vscodeOnly', "Local Agents") }); + pickerItems.push(...vscodeOnly.map(makeItem)); + } + if (copilotOnly.length > 0) { + pickerItems.push({ type: 'separator', label: localize('hookSection.copilotCliOnly', "Copilot CLI Agents") }); + pickerItems.push(...copilotOnly.map(makeItem)); + } + } + + picker.items = pickerItems; picker.value = ''; picker.placeholder = localize('commands.hooks.selectEvent.placeholder', 'Select a lifecycle event'); picker.title = localize('commands.hooks.title', 'Hooks'); @@ -411,7 +449,7 @@ export async function showConfigureHooksQuickPick( case Step.SelectHook: { // Filter hooks by the selected type - const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType!.hookType.id); + const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType!.hookType); // Step 2: Show "Add new hook" + existing hooks of this type const hookItems: (IHookQuickPickItem | IQuickPickSeparator)[] = []; @@ -447,7 +485,7 @@ export async function showConfigureHooksQuickPick( picker.items = hookItems; picker.value = ''; picker.placeholder = localize('commands.hooks.selectHook.placeholder', 'Select a hook to open or add a new one'); - picker.title = selectedHookType!.hookType.label; + picker.title = selectedHookType!.hookTypeMeta.label; picker.buttons = [backButton]; const result = await awaitPick(picker, backButton); @@ -488,8 +526,8 @@ export async function showConfigureHooksQuickPick( } picker.hide(); - if (callbacks?.openEditor) { - await callbacks.openEditor(entry.fileUri, { selection }); + if (options?.openEditor) { + await options.openEditor(entry.fileUri, { selection }); } else { await editorService.openEditor({ resource: entry.fileUri, @@ -566,12 +604,12 @@ export async function showConfigureHooksQuickPick( picker.hide(); await addHookToFile( selectedFile.fileUri, - selectedHookType!.hookType.id as HookType, + selectedHookType!.hookType, fileService, editorService, notificationService, bulkEditService, - callbacks?.openEditor, + options?.openEditor, ); return; } @@ -698,12 +736,12 @@ export async function showConfigureHooksQuickPick( store.dispose(); await addHookToFile( hookFileUri, - selectedHookType!.hookType.id as HookType, + selectedHookType!.hookType, fileService, editorService, notificationService, bulkEditService, - callbacks?.openEditor, + options?.openEditor, ); return; } @@ -711,13 +749,24 @@ export async function showConfigureHooksQuickPick( // Detect if new file is a Claude hooks file based on its path const newFileFormat = getHookSourceFormat(hookFileUri); const isClaudeNewFile = newFileFormat === HookSourceFormat.Claude; + const isCopilotCliOnly = !isClaudeNewFile + && !new Set(Object.values(HOOKS_BY_TARGET[Target.VSCode])).has(selectedHookType!.hookType) + && new Set(Object.values(HOOKS_BY_TARGET[Target.GitHubCopilot])).has(selectedHookType!.hookType); const hookTypeKey = isClaudeNewFile - ? (getClaudeHookTypeName(selectedHookType!.hookType.id as HookType) ?? selectedHookType!.hookType.id) - : selectedHookType!.hookType.id; - const newFileHookEntry = buildNewHookEntry(newFileFormat); + ? (getClaudeHookTypeName(selectedHookType!.hookType) ?? selectedHookType!.hookType) + : isCopilotCliOnly + ? (getCopilotCliHookTypeName(selectedHookType!.hookType) ?? selectedHookType!.hookType) + : selectedHookType!.hookType; + const newFileHookEntry = isCopilotCliOnly + ? { type: 'command', [targetOS === OperatingSystem.Windows ? 'powershell' : 'bash']: '' } + : buildNewHookEntry(newFileFormat); + const commandFieldKey = isCopilotCliOnly + ? (targetOS === OperatingSystem.Windows ? 'powershell' : 'bash') + : 'command'; // Create new hook file with the selected hook type - const hooksContent = { + const hooksContent: Record = { + ...(isCopilotCliOnly ? { version: 1 } : {}), hooks: { [hookTypeKey]: [ newFileHookEntry @@ -728,15 +777,15 @@ export async function showConfigureHooksQuickPick( const jsonContent = JSON.stringify(hooksContent, null, '\t'); await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); - callbacks?.onHookFileCreated?.(hookFileUri); + options?.onHookFileCreated?.(hookFileUri); // Find the selection for the new hook's command field - const selection = findHookCommandSelection(jsonContent, hookTypeKey, 0, 'command'); + const selection = findHookCommandSelection(jsonContent, hookTypeKey, 0, commandFieldKey); // Open editor with selection store.dispose(); - if (callbacks?.openEditor) { - await callbacks.openEditor(hookFileUri, { selection }); + if (options?.openEditor) { + await options.openEditor(hookFileUri, { selection }); } else { await editorService.openEditor({ resource: hookFileUri, diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts index e6dd6668f35..e5eac07cdb7 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts @@ -10,7 +10,8 @@ import { IPromptsService } from '../../common/promptSyntax/service/promptsServic import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { formatHookCommandLabel, HOOK_TYPES, HookType, IHookCommand } from '../../common/promptSyntax/hookSchema.js'; +import { formatHookCommandLabel, IHookCommand } from '../../common/promptSyntax/hookSchema.js'; +import { HOOK_METADATA, HookType } from '../../common/promptSyntax/hookTypes.js'; import { parseHooksFromFile, parseHooksIgnoringDisableAll } from '../../common/promptSyntax/hookCompatibility.js'; import * as nls from '../../../../../nls.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; @@ -161,7 +162,7 @@ export async function parseAllHookFiles( const { hooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome); for (const [hookType, { hooks: commands, originalId }] of hooks) { - const hookTypeMeta = HOOK_TYPES.find(h => h.id === hookType); + const hookTypeMeta = HOOK_METADATA[hookType]; if (!hookTypeMeta) { continue; } @@ -199,7 +200,7 @@ export async function parseAllHookFiles( const { hooks } = parseHooksIgnoringDisableAll(uri, json, workspaceRootUri, userHome); for (const [hookType, { hooks: commands, originalId }] of hooks) { - const hookTypeMeta = HOOK_TYPES.find(h => h.id === hookType); + const hookTypeMeta = HOOK_METADATA[hookType]; if (!hookTypeMeta) { continue; } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index fc1d34ba099..54a379abecc 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -16,7 +16,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ILogService } from '../../../../../platform/log/common/log.js'; import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { getLanguageIdForPromptsType, PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { getLanguageIdForPromptsType, PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../../platform/userDataSync/common/userDataSync.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { CONFIGURE_SYNC_COMMAND_ID } from '../../../../services/userDataSync/common/userDataSync.js'; @@ -26,7 +26,7 @@ import { askForPromptFileName } from './pickers/askForPromptName.js'; import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js'; -import { Target, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { getTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js'; /** diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts index ac323f512e4..e231b63d6d7 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts @@ -14,8 +14,8 @@ import { CommandsRegistry } from '../../../../../platform/commands/common/comman import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { showToolsPicker } from '../actions/chatToolPicker.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; -import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType } from '../../common/promptSyntax/promptTypes.js'; -import { IPromptsService, Target } from '../../common/promptSyntax/service/promptsService.js'; +import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; +import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; import { registerEditorFeature } from '../../../../../editor/common/editorFeatures.js'; import { PromptFileRewriter } from './promptFileRewriter.js'; import { Range } from '../../../../../editor/common/core/range.js'; diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 41ef02090b8..13eb89e07cb 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -45,7 +45,7 @@ import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; import { chatSessionResourceToId } from '../../common/model/chatUri.js'; -import { HookType } from '../../common/promptSyntax/hookSchema.js'; +import { HookType } from '../../common/promptSyntax/hookTypes.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, IExternalPreToolUseHookResult, ILanguageModelToolsService, IPreparedToolInvocation, isToolSet, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolResultInputOutputDetails, IToolSet, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolInvocationPresentation, toolMatchesModel, ToolSet, ToolSetForModel, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts index 2ed104c1de8..e7f8800ecee 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatHookContentPart.ts @@ -10,14 +10,14 @@ import { IHoverService } from '../../../../../../platform/hover/browser/hover.js import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IChatHookPart } from '../../../common/chatService/chatService.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; -import { HookType, HOOK_TYPES, HookTypeValue } from '../../../common/promptSyntax/hookSchema.js'; +import { HookType, HOOK_METADATA, HookTypeValue } from '../../../common/promptSyntax/hookTypes.js'; import { ChatTreeItem } from '../../chat.js'; import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import './media/chatHookContentPart.css'; function getHookTypeLabel(hookType: HookTypeValue): string { - return HOOK_TYPES.find(hook => hook.id === hookType)?.label ?? hookType; + return HOOK_METADATA[hookType as HookType]?.label ?? hookType; } export class ChatHookContentPart extends ChatCollapsibleContentPart implements IChatContentPart { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index e5952be47f3..8a52ad08d8b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -107,7 +107,7 @@ import { isEqual } from '../../../../../base/common/resources.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { ChatHookContentPart } from './chatContentParts/chatHookContentPart.js'; import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; -import { HookType } from '../../common/promptSyntax/hookSchema.js'; +import { HookType } from '../../common/promptSyntax/hookTypes.js'; import { ChatQuestionCarouselAutoReply } from './chatQuestionCarouselAutoReply.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { AccessibilityWorkbenchSettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 7cecc618954..1a4b90632fb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -124,7 +124,7 @@ import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionIte import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; -import { Target } from '../../../common/promptSyntax/service/promptsService.js'; +import { Target } from '../../../common/promptSyntax/promptTypes.js'; import { EnhancedModelPickerActionItem } from './modelPickerActionItem2.js'; const $ = dom.$; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index a03b216363c..0452f785a63 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -54,7 +54,8 @@ import { IDynamicVariable } from '../../../../common/attachments/chatVariables.j import { ChatAgentLocation, ChatModeKind, isSupportedChatFileScheme } from '../../../../common/constants.js'; import { isToolSet } from '../../../../common/tools/languageModelToolsService.js'; import { IChatSessionsService } from '../../../../common/chatSessionsService.js'; -import { IPromptsService, Target } from '../../../../common/promptSyntax/service/promptsService.js'; +import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; +import { Target } from '../../../../common/promptSyntax/promptTypes.js'; import { ChatSubmitAction, IChatExecuteActionContext } from '../../../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../../../chat.js'; import { resizeImage } from '../../../chatImageUtils.js'; 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 773f896f6e8..fbd4a16a579 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -28,7 +28,8 @@ import { IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { isOrganizationPromptFile } from '../../../common/promptSyntax/utils/promptsServiceUtils.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; -import { PromptsStorage, Target } from '../../../common/promptSyntax/service/promptsService.js'; +import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { Target } from '../../../common/promptSyntax/promptTypes.js'; import { getOpenChatActionIdForMode } from '../../actions/chatActions.js'; import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index f6501831c4c..8b0e548eaf2 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -20,7 +20,8 @@ import { IChatAgentService } from './participants/chatAgents.js'; import { ChatContextKeys } from './actions/chatContextKeys.js'; import { ChatConfiguration, ChatModeKind } from './constants.js'; import { IHandOff, isTarget } from './promptSyntax/promptFileParser.js'; -import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, ICustomAgentVisibility, IPromptsService, isCustomAgentVisibility, PromptsStorage, Target } from './promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, ICustomAgentVisibility, IPromptsService, isCustomAgentVisibility, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { Target } from './promptSyntax/promptTypes.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { hash } from '../../../../base/common/hash.js'; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 868b0be875f..4d1fd79be9f 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -14,7 +14,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { hasKey } from '../../../../../base/common/types.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { HookTypeValue } from '../promptSyntax/hookSchema.js'; +import { HookTypeValue } from '../promptSyntax/hookTypes.js'; import { ISelection } from '../../../../../editor/common/core/selection.js'; import { Command, Location, TextEdit } from '../../../../../editor/common/languages.js'; import { FileType } from '../../../../../platform/files/common/files.js'; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index c2393a4b9cd..692aa3470f0 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -53,7 +53,7 @@ import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../langua import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; import { IPromptsService } from '../promptSyntax/service/promptsService.js'; -import { IChatRequestHooks } from '../promptSyntax/hookSchema.js'; +import { ChatRequestHooks } from '../promptSyntax/hookSchema.js'; const serializedChatKey = 'interactive.sessions'; @@ -947,7 +947,7 @@ export class ChatService extends Disposable implements IChatService { let detectedCommand: IChatAgentCommand | undefined; // Collect hooks from hook .json files - let collectedHooks: IChatRequestHooks | undefined; + let collectedHooks: ChatRequestHooks | undefined; let hasDisabledClaudeHooks = false; try { const hooksInfo = await this.promptsService.getHooks(token, model.sessionResource); diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index c609524139d..da4c1b50db3 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -15,7 +15,7 @@ import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './participa import { IChatEditingSession } from './editing/chatEditingService.js'; import { IChatModel, IChatRequestVariableData, ISerializableChatModelInputState } from './model/chatModel.js'; import { IChatProgress, IChatService, IChatSessionTiming } from './chatService/chatService.js'; -import { Target } from './promptSyntax/service/promptsService.js'; +import { Target } from './promptSyntax/promptTypes.js'; export const enum ChatSessionStatus { Failed = 0, diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 8a92fdb502f..817ebbb18b8 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -21,7 +21,7 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { ChatContextKeys } from '../actions/chatContextKeys.js'; import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestModeInstructions, IChatRequestVariableData, ISerializableChatAgentData } from '../model/chatModel.js'; -import { IChatRequestHooks } from '../promptSyntax/hookSchema.js'; +import { ChatRequestHooks } from '../promptSyntax/hookSchema.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from '../chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; @@ -153,7 +153,7 @@ export interface IChatAgentRequest { * Collected hooks configuration for this request. * Contains all hooks defined in hooks .json files, organized by hook type. */ - hooks?: IChatRequestHooks; + hooks?: ChatRequestHooks; /** * Whether any hooks are enabled for this request. */ diff --git a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts index 50f145ccf10..26cf4257f0a 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts @@ -13,7 +13,7 @@ import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { URI } from '../../../../../base/common/uri.js'; -import { Target } from '../promptSyntax/service/promptsService.js'; +import { Target } from '../promptSyntax/promptTypes.js'; //#region slash service, commands etc diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index 81ba54b263f..bbc65030621 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -10,7 +10,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { SyncDescriptor0 } from '../../../../../platform/instantiation/common/descriptors.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMcpServerConfiguration } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; -import { HookType, IHookCommand } from '../promptSyntax/hookSchema.js'; +import { IHookCommand } from '../promptSyntax/hookSchema.js'; +import { HookType } from '../promptSyntax/hookTypes.js'; import { IMarketplacePlugin } from './pluginMarketplaceService.js'; export const IAgentPluginService = createDecorator('agentPluginService'); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts index eb567363a8e..6776c6a2e28 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts @@ -4,23 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../base/common/uri.js'; -import { HookType, IHookCommand, toHookType, resolveHookCommand } from './hookSchema.js'; - -/** - * Maps Claude hook type names to our abstract HookType. - * Claude uses PascalCase and slightly different names. - * @see https://docs.anthropic.com/en/docs/claude-code/hooks - */ -export const CLAUDE_HOOK_TYPE_MAP: Record = { - 'SessionStart': HookType.SessionStart, - 'UserPromptSubmit': HookType.UserPromptSubmit, - 'PreToolUse': HookType.PreToolUse, - 'PostToolUse': HookType.PostToolUse, - 'PreCompact': HookType.PreCompact, - 'SubagentStart': HookType.SubagentStart, - 'SubagentStop': HookType.SubagentStop, - 'Stop': HookType.Stop, -}; +import { toHookType, resolveHookCommand, IHookCommand } from './hookSchema.js'; +import { HOOKS_BY_TARGET, HookType } from './hookTypes.js'; +import { Target } from './promptTypes.js'; /** * Cached inverse mapping from HookType to Claude hook type name. @@ -31,7 +17,7 @@ let _hookTypeToClaudeName: Map | undefined; function getHookTypeToClaudeNameMap(): Map { if (!_hookTypeToClaudeName) { _hookTypeToClaudeName = new Map(); - for (const [claudeName, hookType] of Object.entries(CLAUDE_HOOK_TYPE_MAP)) { + for (const [claudeName, hookType] of Object.entries(HOOKS_BY_TARGET[Target.Claude])) { _hookTypeToClaudeName.set(hookType, claudeName); } } @@ -42,7 +28,7 @@ function getHookTypeToClaudeNameMap(): Map { * Resolves a Claude hook type name to our abstract HookType. */ export function resolveClaudeHookType(name: string): HookType | undefined { - return CLAUDE_HOOK_TYPE_MAP[name]; + return HOOKS_BY_TARGET[Target.Claude][name]; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts index d00fd26cb1e..64e46956b17 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts @@ -5,9 +5,10 @@ import { URI } from '../../../../../base/common/uri.js'; import { basename, dirname } from '../../../../../base/common/path.js'; -import { HookType, IHookCommand, toHookType } from './hookSchema.js'; +import { IHookCommand, toHookType } from './hookSchema.js'; import { parseClaudeHooks, extractHookCommandsFromItem } from './hookClaudeCompat.js'; import { resolveCopilotCliHookType } from './hookCopilotCliCompat.js'; +import { HookType } from './hookTypes.js'; /** * Represents a hook source with its original and normalized properties. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts index 587771544b2..9bf9c6b1076 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { COPILOT_CLI_HOOK_TYPE_MAP, HookType } from './hookSchema.js'; +import { HOOKS_BY_TARGET, HookType } from './hookTypes.js'; +import { Target } from './promptTypes.js'; + +const COPILOT_CLI_HOOK_TYPE_MAP: Record = HOOKS_BY_TARGET[Target.GitHubCopilot]; /** * Cached inverse mapping from HookType to Copilot CLI hook type name. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index 62fee88c20e..0d69dce53bf 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts @@ -10,82 +10,8 @@ import { joinPath } from '../../../../../base/common/resources.js'; import { isAbsolute } from '../../../../../base/common/path.js'; import { untildify } from '../../../../../base/common/labels.js'; import { OperatingSystem } from '../../../../../base/common/platform.js'; - -/** - * Enum of available hook types that can be configured in hooks .json - */ -export enum HookType { - SessionStart = 'SessionStart', - UserPromptSubmit = 'UserPromptSubmit', - PreToolUse = 'PreToolUse', - PostToolUse = 'PostToolUse', - PreCompact = 'PreCompact', - SubagentStart = 'SubagentStart', - SubagentStop = 'SubagentStop', - Stop = 'Stop', -} - -/** - * Maps Copilot CLI hook type names to our abstract HookType. - * Copilot CLI uses camelCase names. - */ -export const COPILOT_CLI_HOOK_TYPE_MAP = { - 'sessionStart': HookType.SessionStart, - 'userPromptSubmitted': HookType.UserPromptSubmit, - 'preToolUse': HookType.PreToolUse, - 'postToolUse': HookType.PostToolUse, -} as const satisfies Record; - -/** - * String literal type derived from HookType enum values. - */ -export type HookTypeValue = `${HookType}`; - -/** - * Metadata for hook types including localized labels and descriptions - */ -export const HOOK_TYPES = [ - { - id: HookType.SessionStart, - label: nls.localize('hookType.sessionStart.label', "Session Start"), - description: nls.localize('hookType.sessionStart.description', "Executed when a new agent session begins.") - }, - { - id: HookType.UserPromptSubmit, - label: nls.localize('hookType.userPromptSubmit.label', "User Prompt Submit"), - description: nls.localize('hookType.userPromptSubmit.description', "Executed when the user submits a prompt to the agent.") - }, - { - id: HookType.PreToolUse, - label: nls.localize('hookType.preToolUse.label', "Pre-Tool Use"), - description: nls.localize('hookType.preToolUse.description', "Executed before the agent uses any tool.") - }, - { - id: HookType.PostToolUse, - label: nls.localize('hookType.postToolUse.label', "Post-Tool Use"), - description: nls.localize('hookType.postToolUse.description', "Executed after a tool completes execution successfully.") - }, - { - id: HookType.PreCompact, - label: nls.localize('hookType.preCompact.label', "Pre-Compact"), - description: nls.localize('hookType.preCompact.description', "Executed before the agent compacts the conversation context.") - }, - { - id: HookType.SubagentStart, - label: nls.localize('hookType.subagentStart.label', "Subagent Start"), - description: nls.localize('hookType.subagentStart.description', "Executed when a subagent is started.") - }, - { - id: HookType.SubagentStop, - label: nls.localize('hookType.subagentStop.label', "Subagent Stop"), - description: nls.localize('hookType.subagentStop.description', "Executed when a subagent stops.") - }, - { - id: HookType.Stop, - label: nls.localize('hookType.stop.label', "Stop"), - description: nls.localize('hookType.stop.description', "Executed when the agent stops.") - } -] as const; +import { HookType, HOOKS_BY_TARGET, HOOK_METADATA } from './hookTypes.js'; +import { Target } from './promptTypes.js'; /** * A single hook command configuration. @@ -116,22 +42,15 @@ export interface IHookCommand { * Collected hooks for a chat request, organized by hook type. * This is passed to the extension host so it knows what hooks are available. */ -export interface IChatRequestHooks { - readonly [HookType.SessionStart]?: readonly IHookCommand[]; - readonly [HookType.UserPromptSubmit]?: readonly IHookCommand[]; - readonly [HookType.PreToolUse]?: readonly IHookCommand[]; - readonly [HookType.PostToolUse]?: readonly IHookCommand[]; - readonly [HookType.PreCompact]?: readonly IHookCommand[]; - readonly [HookType.SubagentStart]?: readonly IHookCommand[]; - readonly [HookType.SubagentStop]?: readonly IHookCommand[]; - readonly [HookType.Stop]?: readonly IHookCommand[]; -} +export type ChatRequestHooks = { + readonly [K in HookType]?: readonly IHookCommand[]; +}; /** * JSON Schema for GitHub Copilot hook configuration files. * Hooks enable executing custom shell commands at strategic points in an agent's workflow. */ -const hookCommandSchema: IJSONSchema = { +const vscodeHookCommandSchema: IJSONSchema = { type: 'object', additionalProperties: true, required: ['type'], @@ -185,46 +104,26 @@ const hookCommandSchema: IJSONSchema = { const hookArraySchema: IJSONSchema = { type: 'array', - items: hookCommandSchema + items: vscodeHookCommandSchema }; /** - * Hook properties for the VS Code / PascalCase format. + * Builds JSON Schema hook properties for a given target by looking up + * the hook keys from HOOKS_BY_TARGET and descriptions from HOOK_METADATA. */ -const vscodeHookProperties: { [key in HookType]: IJSONSchema } = { - SessionStart: { - ...hookArraySchema, - description: nls.localize('hookFile.sessionStart', 'Executed when a new agent session begins. Use to initialize environments, log session starts, validate project state, or set up temporary resources.') - }, - UserPromptSubmit: { - ...hookArraySchema, - description: nls.localize('hookFile.userPromptSubmit', 'Executed when the user submits a prompt to the agent. Use to log user requests for auditing and usage analysis.') - }, - PreToolUse: { - ...hookArraySchema, - description: nls.localize('hookFile.preToolUse', 'Executed before the agent uses any tool. This is the most powerful hook as it can approve or deny tool executions. Use to block dangerous commands, enforce security policies, require approval for sensitive operations, or log tool usage.') - }, - PostToolUse: { - ...hookArraySchema, - description: nls.localize('hookFile.postToolUse', 'Executed after a tool completes execution successfully. Use to log execution results, track usage statistics, generate audit trails, or monitor performance.') - }, - PreCompact: { - ...hookArraySchema, - description: nls.localize('hookFile.preCompact', 'Executed before the agent compacts the conversation context. Use to save conversation state, export important information, or prepare for context reduction.') - }, - SubagentStart: { - ...hookArraySchema, - description: nls.localize('hookFile.subagentStart', 'Executed when a subagent is started. Use to log subagent spawning, track nested agent usage, or initialize subagent-specific resources.') - }, - SubagentStop: { - ...hookArraySchema, - description: nls.localize('hookFile.subagentStop', 'Executed when a subagent stops. Use to log subagent completion, cleanup subagent resources, or aggregate subagent results.') - }, - Stop: { - ...hookArraySchema, - description: nls.localize('hookFile.stop', 'Executed when the agent session stops. Use to cleanup resources, generate final reports, or send completion notifications.') - } -}; +function buildHookProperties(target: Target, arraySchema: IJSONSchema): Record { + return Object.fromEntries( + Object.entries(HOOKS_BY_TARGET[target]).map(([key, hookType]) => [ + key, + { ...arraySchema, description: HOOK_METADATA[hookType]?.description } + ]) + ); +} + +/** + * Hook properties for the VS Code format. + */ +const vscodeHookProperties: Record = buildHookProperties(Target.VSCode, hookArraySchema); /** * Hook command schema for the Copilot CLI format. @@ -276,27 +175,9 @@ const copilotCliHookArraySchema: IJSONSchema = { }; /** - * Hook properties for the Copilot CLI / camelCase format. - * Maps from the Copilot CLI hook type names defined in COPILOT_CLI_HOOK_TYPE_MAP. + * Hook properties for the Copilot CLI format. */ -const copilotCliHookProperties: { [key in keyof typeof COPILOT_CLI_HOOK_TYPE_MAP]: IJSONSchema } = { - sessionStart: { - ...copilotCliHookArraySchema, - description: nls.localize('hookFile.cli.sessionStart', 'Executed when a new agent session begins.') - }, - userPromptSubmitted: { - ...copilotCliHookArraySchema, - description: nls.localize('hookFile.cli.userPromptSubmitted', 'Executed when the user submits a prompt to the agent.') - }, - preToolUse: { - ...copilotCliHookArraySchema, - description: nls.localize('hookFile.cli.preToolUse', 'Executed before the agent uses any tool. Can approve or deny tool executions.') - }, - postToolUse: { - ...copilotCliHookArraySchema, - description: nls.localize('hookFile.cli.postToolUse', 'Executed after a tool completes execution successfully.') - }, -}; +const copilotCliHookProperties: Record = buildHookProperties(Target.GitHubCopilot, copilotCliHookArraySchema); export const hookFileSchema: IJSONSchema = { $schema: 'http://json-schema.org/draft-07/schema#', @@ -369,11 +250,6 @@ export const hookFileSchema: IJSONSchema = { */ export const HOOK_SCHEMA_URI = 'vscode://schemas/hooks'; -/** - * Glob pattern for hook files. - */ -export const HOOK_FILE_GLOB = '.github/hooks/*.json'; - /** * Normalizes a raw hook type identifier to the canonical HookType enum value. * Only matches exact enum values. For tool-specific naming conventions (e.g., Claude, Copilot CLI), diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookTypes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookTypes.ts new file mode 100644 index 00000000000..66d28c22876 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookTypes.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from '../../../../../nls.js'; +import { Target } from './promptTypes.js'; + +/** + * Enum of hook types across all targets. For the set of supported hooks per target, see HOOKS_BY_TARGET. + */ +export enum HookType { + SessionStart = 'SessionStart', + SessionEnd = 'SessionEnd', + UserPromptSubmit = 'UserPromptSubmit', + PreToolUse = 'PreToolUse', + PostToolUse = 'PostToolUse', + PreCompact = 'PreCompact', + SubagentStart = 'SubagentStart', + SubagentStop = 'SubagentStop', + Stop = 'Stop', + ErrorOccurred = 'ErrorOccurred', +} + +/** + * String literal type derived from HookType enum values. + */ +export type HookTypeValue = `${HookType}`; + +export const HOOKS_BY_TARGET: Record> = { + // see https://code.visualstudio.com/docs/copilot/customization/hooks#_hook-lifecycle-events + [Target.VSCode]: { + 'SessionStart': HookType.SessionStart, + 'UserPromptSubmit': HookType.UserPromptSubmit, + 'PreToolUse': HookType.PreToolUse, + 'PostToolUse': HookType.PostToolUse, + 'PreCompact': HookType.PreCompact, + 'SubagentStart': HookType.SubagentStart, + 'SubagentStop': HookType.SubagentStop, + 'Stop': HookType.Stop, + }, + // see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks#types-of-hooks + [Target.GitHubCopilot]: { + 'sessionStart': HookType.SessionStart, + 'sessionEnd': HookType.SessionEnd, + 'userPromptSubmitted': HookType.UserPromptSubmit, + 'preToolUse': HookType.PreToolUse, + 'postToolUse': HookType.PostToolUse, + 'agentStop': HookType.Stop, + 'subagentStop': HookType.SubagentStop, + 'errorOccurred': HookType.ErrorOccurred + }, + // see https://docs.anthropic.com/en/docs/claude-code/hooks + [Target.Claude]: { + 'SessionStart': HookType.SessionStart, + 'UserPromptSubmit': HookType.UserPromptSubmit, + 'PreToolUse': HookType.PreToolUse, + 'PostToolUse': HookType.PostToolUse, + 'PreCompact': HookType.PreCompact, + 'SubagentStart': HookType.SubagentStart, + 'SubagentStop': HookType.SubagentStop, + 'Stop': HookType.Stop, + }, + // if no target, just list all known hook types. + [Target.Undefined]: Object.fromEntries( + Object.values(HookType).map(h => [h, h]) + ) as Record +}; + +/** + * Metadata for a hook type including localized label and description. + */ +export interface IHookTypeMeta { + readonly label: string; + readonly description: string; +} + +/** + * Metadata for hook types including localized labels and descriptions + */ +export const HOOK_METADATA: { [key in HookType]: IHookTypeMeta } = { + [HookType.SessionStart]: { + label: nls.localize('hookType.sessionStart.label', "Session Start"), + description: nls.localize('hookType.sessionStart.description', "Executed when a new agent session begins.") + }, + [HookType.UserPromptSubmit]: { + label: nls.localize('hookType.userPromptSubmit.label', "User Prompt Submit"), + description: nls.localize('hookType.userPromptSubmit.description', "Executed when the user submits a prompt to the agent.") + }, + [HookType.PreToolUse]: { + label: nls.localize('hookType.preToolUse.label', "Pre-Tool Use"), + description: nls.localize('hookType.preToolUse.description', "Executed before the agent uses any tool.") + }, + [HookType.PostToolUse]: { + label: nls.localize('hookType.postToolUse.label', "Post-Tool Use"), + description: nls.localize('hookType.postToolUse.description', "Executed after a tool completes execution successfully.") + }, + [HookType.PreCompact]: { + label: nls.localize('hookType.preCompact.label', "Pre-Compact"), + description: nls.localize('hookType.preCompact.description', "Executed before the agent compacts the conversation context.") + }, + [HookType.SubagentStart]: { + label: nls.localize('hookType.subagentStart.label', "Subagent Start"), + description: nls.localize('hookType.subagentStart.description', "Executed when a subagent is started.") + }, + [HookType.SubagentStop]: { + label: nls.localize('hookType.subagentStop.label', "Subagent Stop"), + description: nls.localize('hookType.subagentStop.description', "Executed when a subagent stops.") + }, + [HookType.Stop]: { + label: nls.localize('hookType.stop.label', "Stop"), + description: nls.localize('hookType.stop.description', "Executed when the agent stops.") + }, + [HookType.SessionEnd]: { + label: nls.localize('hookType.sessionEnd.label', "Session End"), + description: nls.localize('hookType.sessionEnd.description', "Executed when an agent session ends.") + }, + [HookType.ErrorOccurred]: { + label: nls.localize('hookType.errorOccurred.label', "Error Occurred"), + description: nls.localize('hookType.errorOccurred.description', "Executed when an error occurs during the agent session.") + } +}; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 5e60a57cf89..66f81749bf5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -12,8 +12,8 @@ import { ITextModel } from '../../../../../../editor/common/model.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService } from '../../tools/languageModelToolsService.js'; import { IChatModeService } from '../../chatModes.js'; -import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; -import { IPromptsService, Target } from '../service/promptsService.js'; +import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; +import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; import { ClaudeHeaderAttributes, ISequenceValue, IValue, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { getAttributeDescription, getTarget, getValidAttributeNames, claudeAgentAttributes, claudeRulesAttributes, knownClaudeTools, knownGithubCopilotTools, IValueEntry } from './promptValidator.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 8fe87c45a10..c223dc31451 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -13,8 +13,8 @@ import { localize } from '../../../../../../nls.js'; import { ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, isToolSet, IToolSet } from '../../tools/languageModelToolsService.js'; import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; -import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; -import { IPromptsService, Target } from '../service/promptsService.js'; +import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; +import { IPromptsService } from '../service/promptsService.js'; import { ClaudeHeaderAttributes, IHeaderAttribute, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { getAttributeDescription, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptValidator.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index daf123f3151..838dfadb33c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -15,13 +15,13 @@ import { ChatMode, IChatMode, IChatModeService } from '../../chatModes.js'; import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, SpecedToolAliases } from '../../tools/languageModelToolsService.js'; -import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; +import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { GithubPromptHeaderAttributes, ISequenceValue, IHeaderAttribute, IScalarValue, parseCommaSeparatedList, ParsedPromptFile, PromptHeader, PromptHeaderAttributes, IValue } from '../promptFileParser.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { IPromptsService, Target } from '../service/promptsService.js'; +import { IPromptsService } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { AGENTS_SOURCE_FOLDER, isInClaudeAgentsFolder, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 0de7e30f982..42ed43f0300 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { parse, YamlNode, YamlParseError } from '../../../../../base/common/yaml.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { PositionOffsetTransformer } from '../../../../../editor/common/core/text/positionToOffsetImpl.js'; -import { Target } from './service/promptsService.js'; +import { Target } from './promptTypes.js'; export class PromptFileParser { constructor() { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts index 8c4d0cbc58a..c51a7123aef 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts @@ -92,3 +92,9 @@ export function isValidPromptType(type: string): type is PromptsType { return Object.values(PromptsType).includes(type as PromptsType); } +export enum Target { + VSCode = 'vscode', + GitHubCopilot = 'github-copilot', + Claude = 'claude', + Undefined = 'undefined', +} 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 73f48099a13..66d01e1d7af 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -11,11 +11,11 @@ import { ITextModel } from '../../../../../../editor/common/model.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IChatModeInstructions, IVariableReference } from '../../chatModes.js'; -import { PromptsType } from '../promptTypes.js'; +import { PromptsType, Target } from '../promptTypes.js'; import { IHandOff, ParsedPromptFile } from '../promptFileParser.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; -import { IChatRequestHooks } from '../hookSchema.js'; import { IResolvedPromptSourceFolder } from '../config/promptFileLocations.js'; +import { ChatRequestHooks } from '../hookSchema.js'; /** * Entry emitted by the prompts service when discovery logging occurs. @@ -168,13 +168,6 @@ export function isCustomAgentVisibility(obj: unknown): obj is ICustomAgentVisibi return typeof v.userInvocable === 'boolean' && typeof v.agentInvocable === 'boolean'; } -export enum Target { - VSCode = 'vscode', - GitHubCopilot = 'github-copilot', - Claude = 'claude', - Undefined = 'undefined', -} - export interface ICustomAgent { /** * URI of a custom agent file. @@ -354,7 +347,7 @@ export interface IPromptDiscoveryInfo { } export interface IConfiguredHooksInfo { - readonly hooks: IChatRequestHooks; + readonly hooks: ChatRequestHooks; readonly hasDisabledClaudeHooks: boolean; } 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 6c566c8bbdc..05f8827bd0f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -30,13 +30,14 @@ import { IUserDataProfileService } from '../../../../../services/userDataProfile import { IVariableReference } from '../../chatModes.js'; import { PromptsConfig } from '../config/config.js'; import { AGENT_MD_FILENAME, CLAUDE_CONFIG_FOLDER, CLAUDE_LOCAL_MD_FILENAME, CLAUDE_MD_FILENAME, getCleanPromptName, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; -import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; +import { PROMPT_LANGUAGE_ID, PromptsType, Target, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; -import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPluginPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, IPromptSourceFolderResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, Target, IPromptDiscoveryLogEntry } from './promptsService.js'; +import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPluginPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, IPromptSourceFolderResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, IPromptDiscoveryLogEntry } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { IChatRequestHooks, IHookCommand, HookType } from '../hookSchema.js'; +import { ChatRequestHooks, IHookCommand } from '../hookSchema.js'; +import { HookType } from '../hookTypes.js'; import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; @@ -1222,16 +1223,7 @@ export class PromptsService extends Disposable implements IPromptsService { const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path; let hasDisabledClaudeHooks = false; - const collectedHooks: Record = { - [HookType.SessionStart]: [], - [HookType.UserPromptSubmit]: [], - [HookType.PreToolUse]: [], - [HookType.PostToolUse]: [], - [HookType.PreCompact]: [], - [HookType.SubagentStart]: [], - [HookType.SubagentStop]: [], - [HookType.Stop]: [], - }; + const collectedHooks = new Map(); const defaultFolder = this.workspaceService.getWorkspace().folders[0]; @@ -1266,7 +1258,12 @@ export class PromptsService extends Disposable implements IPromptsService { for (const [hookType, { hooks: commands }] of hooks) { for (const command of commands) { - collectedHooks[hookType].push(command); + let bucket = collectedHooks.get(hookType); + if (!bucket) { + bucket = []; + collectedHooks.set(hookType, bucket); + } + bucket.push(command); this.logger.trace(`[PromptsService] Collected ${hookType} hook from ${hookFile.uri} (format: ${format})`); } } @@ -1279,21 +1276,23 @@ export class PromptsService extends Disposable implements IPromptsService { const plugins = this.agentPluginService.plugins.get(); for (const plugin of plugins) { for (const hook of plugin.hooks.get()) { - collectedHooks[hook.type].push(...hook.hooks); + let bucket = collectedHooks.get(hook.type); + if (!bucket) { + bucket = []; + collectedHooks.set(hook.type, bucket); + } + bucket.push(...hook.hooks); } } // Check if any hooks were collected - const hasHooks = Object.values(collectedHooks).some(arr => arr.length > 0); - if (!hasHooks) { + if (collectedHooks.size === 0) { this.logger.trace('[PromptsService] No valid hooks collected.'); return undefined; } - // Build the result, only including hook types that have entries - const result: IChatRequestHooks = Object.fromEntries( - Object.entries(collectedHooks).filter(([_, commands]) => commands.length > 0) - ) as IChatRequestHooks; + // Build the result + const result: ChatRequestHooks = Object.fromEntries(collectedHooks) as ChatRequestHooks; this.logger.trace(`[PromptsService] Collected hooks: ${JSON.stringify(Object.keys(result))}`); return { hooks: result, hasDisabledClaudeHooks }; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 5db3195bbdb..e5fd0a7a691 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -23,7 +23,7 @@ import { ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; -import { IChatRequestHooks } from '../../promptSyntax/hookSchema.js'; +import { ChatRequestHooks } from '../../promptSyntax/hookSchema.js'; import { ICustomAgent, IPromptsService } from '../../promptSyntax/service/promptsService.js'; import { isBuiltinAgent } from '../../promptSyntax/utils/promptsServiceUtils.js'; import { @@ -252,7 +252,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { await computer.collect(variableSet, token); // Collect hooks from hook .json files - let collectedHooks: IChatRequestHooks | undefined; + let collectedHooks: ChatRequestHooks | undefined; try { const info = await this.promptsService.getHooks(token, invocation.context.sessionResource); collectedHooks = info?.hooks; diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index 8fdd981ea71..1daf164ddcd 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -19,12 +19,12 @@ import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../ import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; import { IChatModeService } from '../../../../common/chatModes.js'; import { PromptHeaderAutocompletion } from '../../../../common/promptSyntax/languageProviders/promptHeaderAutocompletion.js'; -import { ICustomAgent, IPromptsService, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { getLanguageIdForPromptsType, PromptsType, Target } from '../../../../common/promptSyntax/promptTypes.js'; import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts index 6f2a25d8ce3..f90f6d45707 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -18,14 +18,14 @@ import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constan import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; import { PromptHoverProvider } from '../../../../common/promptSyntax/languageProviders/promptHovers.js'; -import { IPromptsService, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js'; +import { IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { getLanguageIdForPromptsType, PromptsType, Target } from '../../../../common/promptSyntax/promptTypes.js'; import { MockChatModeService } from '../../../common/mockChatModeService.js'; import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; -import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; suite('PromptHoverProvider', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index e3cc440de63..cd985b7ba74 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -23,9 +23,9 @@ import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../ import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { PromptValidator } from '../../../../common/promptSyntax/languageProviders/promptValidator.js'; -import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { PromptsType, Target } from '../../../../common/promptSyntax/promptTypes.js'; import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; -import { ICustomAgent, IPromptsService, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { MockChatModeService } from '../../../common/mockChatModeService.js'; import { MockPromptsService } from '../../../common/promptSyntax/service/mockPromptsService.js'; diff --git a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts index f7745a0b6fa..c8e5aa27404 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -19,8 +19,9 @@ import { TestStorageService } from '../../../../test/common/workbenchTestService import { IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatMode, ChatModeService } from '../../common/chatModes.js'; import { ChatModeKind } from '../../common/constants.js'; -import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage, Target } from '../../common/promptSyntax/service/promptsService.js'; +import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { MockPromptsService } from './promptSyntax/service/mockPromptsService.js'; +import { Target } from '../../common/promptSyntax/promptTypes.js'; class TestChatAgentService implements Partial { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 0136bfb9393..7ee70008f52 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -13,7 +13,7 @@ import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../commo import { IChatModel } from '../../common/model/chatModel.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; -import { Target } from '../../common/promptSyntax/service/promptsService.js'; +import { Target } from '../../common/promptSyntax/promptTypes.js'; export class MockChatSessionsService implements IChatSessionsService { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts index 76b2ac54b07..2ba30ef5848 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { HookType } from '../../../common/promptSyntax/hookSchema.js'; +import { HookType } from '../../../common/promptSyntax/hookTypes.js'; import { parseClaudeHooks, resolveClaudeHookType, getClaudeHookTypeName, extractHookCommandsFromItem } from '../../../common/promptSyntax/hookClaudeCompat.js'; import { getHookSourceFormat, HookSourceFormat, buildNewHookEntry } from '../../../common/promptSyntax/hookCompatibility.js'; import { URI } from '../../../../../../base/common/uri.js'; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts index ede5eeb5e52..7fd7eee9304 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { HookType } from '../../../common/promptSyntax/hookSchema.js'; +import { HookType } from '../../../common/promptSyntax/hookTypes.js'; import { parseCopilotHooks, parseHooksFromFile, HookSourceFormat } from '../../../common/promptSyntax/hookCompatibility.js'; import { URI } from '../../../../../../base/common/uri.js'; 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 d77772465b4..6130e821b99 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 @@ -40,8 +40,8 @@ import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '../../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { AGENTS_SOURCE_FOLDER, CLAUDE_CONFIG_FOLDER, HOOKS_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js'; -import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { ExtensionAgentSourceType, ICustomAgent, IPromptFileContext, IPromptsService, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType, Target } from '../../../../common/promptSyntax/promptTypes.js'; +import { ExtensionAgentSourceType, ICustomAgent, IPromptFileContext, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js'; import { mockFiles } from '../testUtils/mockFilesystem.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js'; @@ -50,7 +50,7 @@ import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../servic import { IExtensionService } from '../../../../../../services/extensions/common/extensions.js'; import { IRemoteAgentService } from '../../../../../../services/remote/common/remoteAgentService.js'; import { ChatModeKind } from '../../../../common/constants.js'; -import { HookType } from '../../../../common/promptSyntax/hookSchema.js'; +import { HookType } from '../../../../common/promptSyntax/hookTypes.js'; import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js'; diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts index 2efa4f1af16..79d99695eb5 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -16,7 +16,8 @@ import { IChatService } from '../../../../common/chatService/chatService.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../../common/languageModels.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { IProductService } from '../../../../../../../platform/product/common/productService.js'; -import { ICustomAgent, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ICustomAgent, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { Target } from '../../../../common/promptSyntax/promptTypes.js'; import { MockPromptsService } from '../../promptSyntax/service/mockPromptsService.js'; import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; diff --git a/src/vscode-dts/vscode.proposed.chatHooks.d.ts b/src/vscode-dts/vscode.proposed.chatHooks.d.ts index eec28002b77..85323b85156 100644 --- a/src/vscode-dts/vscode.proposed.chatHooks.d.ts +++ b/src/vscode-dts/vscode.proposed.chatHooks.d.ts @@ -10,7 +10,7 @@ declare module 'vscode' { /** * The type of hook to execute. */ - export type ChatHookType = 'SessionStart' | 'UserPromptSubmit' | 'PreToolUse' | 'PostToolUse' | 'PreCompact' | 'SubagentStart' | 'SubagentStop' | 'Stop'; + export type ChatHookType = 'SessionStart' | 'SessionEnd' | 'UserPromptSubmit' | 'PreToolUse' | 'PostToolUse' | 'PreCompact' | 'SubagentStart' | 'SubagentStop' | 'Stop' | 'ErrorOccurred'; /** * A resolved hook command ready for execution. From ffc4f9dcb3028b4cbcba7d35573110eebe9af3af Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:57:52 -0800 Subject: [PATCH 079/448] Reapply 8e445caeffff66b8920466770121e4b53343ebea Switching to a slightly older postcss version to avoid the official build issue --- .eslint-ignore | 2 - build/gulpfile.extensions.ts | 12 - build/lib/extensions.ts | 208 +- .../server/build/javaScriptLibraryLoader.js | 132 -- .../json-language-features/server/.npmignore | 1 - extensions/mangle-loader.js | 66 - extensions/media-preview/.vscodeignore | 1 - .../mermaid-chat-features/.vscodeignore | 2 - extensions/shared.webpack.config.mjs | 209 -- package-lock.json | 1447 ------------- package.json | 9 - test/monaco/package-lock.json | 1894 ++++++++++++++++- test/monaco/package.json | 12 +- 13 files changed, 1906 insertions(+), 2089 deletions(-) delete mode 100644 extensions/html-language-features/server/build/javaScriptLibraryLoader.js delete mode 100644 extensions/mangle-loader.js delete mode 100644 extensions/shared.webpack.config.mjs diff --git a/.eslint-ignore b/.eslint-ignore index 4736eb5621d..8b8cdd1c2c7 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -18,8 +18,6 @@ **/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts **/extensions/terminal-suggest/third_party/** **/extensions/typescript-language-features/test-workspace/** -**/extensions/typescript-language-features/extension.webpack.config.js -**/extensions/typescript-language-features/extension-browser.webpack.config.js **/extensions/typescript-language-features/package-manager/node-maintainer/** **/extensions/vscode-api-tests/testWorkspace/** **/extensions/vscode-api-tests/testWorkspace2/** diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index 8f9ac9b2b21..e0137816c8c 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -309,13 +309,6 @@ async function buildWebExtensions(isWatch: boolean): Promise { { ignore: ['**/node_modules'] } ); - // Find all webpack configs, excluding those that will be esbuilt - const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p))); - const webpackConfigLocations = (await nodeUtil.promisify(glob)( - path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), - { ignore: ['**/node_modules'] } - )).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath))); - const promises: Promise[] = []; // Esbuild for extensions @@ -330,10 +323,5 @@ async function buildWebExtensions(isWatch: boolean): Promise { ); } - // Run webpack for remaining extensions - if (webpackConfigLocations.length > 0) { - promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })))); - } - await Promise.all(promises); } diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 5710f4d6919..aacf25cbbc1 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -20,10 +20,8 @@ import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; -import webpack from 'webpack'; import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; -import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; @@ -32,8 +30,8 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const root = path.dirname(path.dirname(import.meta.dirname)); -const commit = getVersion(root); -const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +// const commit = getVersion(root); +// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -65,32 +63,24 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { +function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb ? 'esbuild.browser.mts' : 'esbuild.mts'; - const webpackConfigFileName = forWeb - ? `extension-browser.webpack.config.js` - : `extension.webpack.config.js`; - const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); - const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step + // Esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), ...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)), ); isBundled = true; - } else if (hasWebpack) { - input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); - isBundled = true; } else { input = fromLocalNormal(extensionPath); } @@ -122,132 +112,6 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean) return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } -function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { - const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); - const webpack = require('webpack'); - const webpackGulp = require('webpack-stream'); - const result = es.through(); - - const packagedDependencies: string[] = []; - const stripOutSourceMaps: string[] = []; - const packageJsonConfig = require(path.join(extensionPath, 'package.json')); - if (packageJsonConfig.dependencies) { - const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); - const webpackRootConfig = webpackConfig.default; - for (const key in webpackRootConfig.externals) { - if (key in packageJsonConfig.dependencies) { - packagedDependencies.push(key); - } - } - - if (webpackConfig.StripOutSourceMaps) { - for (const filePath of webpackConfig.StripOutSourceMaps) { - stripOutSourceMaps.push(filePath); - } - } - } - - // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar - // to vsce.PackageManager.Yarn. - // A static analysis showed there are no webpack externals that are dependencies of the current - // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list - // as a temporary workaround. - vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { - const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ - path: filePath, - stat: fs.statSync(filePath), - base: extensionPath, - contents: fs.createReadStream(filePath) - })); - - // check for a webpack configuration files, then invoke webpack - // and merge its output with the files stream. - const webpackConfigLocations = (glob.sync( - path.join(extensionPath, '**', webpackConfigFileName), - { ignore: ['**/node_modules'] } - ) as string[]); - const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { - - const webpackDone = (err: Error | undefined, stats: any) => { - fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); - if (err) { - result.emit('error', err); - } - const { compilation } = stats; - if (compilation.errors.length > 0) { - result.emit('error', compilation.errors.join('\n')); - } - if (compilation.warnings.length > 0) { - result.emit('error', compilation.warnings.join('\n')); - } - }; - - const exportedConfig = require(webpackConfigPath).default; - return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { - const webpackConfig = { - ...config, - ...{ mode: 'production' } - }; - if (disableMangle) { - if (Array.isArray(config.module.rules)) { - for (const rule of config.module.rules) { - if (Array.isArray(rule.use)) { - for (const use of rule.use) { - if (String(use.loader).endsWith('mangle-loader.js')) { - use.options.disabled = true; - } - } - } - } - } - } - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data: File) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - if (path.extname(data.basename) === '.js') { - if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); - } else { - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); - } - } - - this.emit('data', data); - })); - }); - }); - - es.merge(...webpackStreams, es.readArray(files)) - // .pipe(es.through(function (data) { - // // debug - // console.log('out', data.path, data.contents.length); - // this.emit('data', data); - // })) - .pipe(result); - - }).catch(err => { - console.error(extensionPath); - console.error(packagedDependencies); - result.emit('error', err); - }); - - return result.pipe(createStatsStream(path.basename(extensionPath))); -} function fromLocalNormal(extensionPath: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -649,70 +513,6 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); -export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { - const webpack = require('webpack') as typeof import('webpack'); - - const webpackConfigs: webpack.Configuration[] = []; - - for (const { configPath, outputRoot } of webpackConfigLocations) { - const configOrFnOrArray = require(configPath).default; - function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { - for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { - const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; - if (outputRoot) { - config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!)); - } - webpackConfigs.push(config); - } - } - addConfig(configOrFnOrArray); - } - - function reporter(fullStats: any) { - if (Array.isArray(fullStats.children)) { - for (const stats of fullStats.children) { - const outputPath = stats.outputPath; - if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); - const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); - } - if (Array.isArray(stats.errors)) { - stats.errors.forEach((error: any) => { - fancyLog.error(error); - }); - } - if (Array.isArray(stats.warnings)) { - stats.warnings.forEach((warning: any) => { - fancyLog.warn(warning); - }); - } - } - } - } - return new Promise((resolve, reject) => { - if (isWatch) { - webpack(webpackConfigs).watch({}, (err, stats) => { - if (err) { - reject(); - } else { - reporter(stats?.toJson()); - } - }); - } else { - webpack(webpackConfigs).run((err, stats) => { - if (err) { - fancyLog.error(err); - reject(); - } else { - reporter(stats?.toJson()); - resolve(); - } - }); - } - }); -} - export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise { function reporter(stdError: string, script: string) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); diff --git a/extensions/html-language-features/server/build/javaScriptLibraryLoader.js b/extensions/html-language-features/server/build/javaScriptLibraryLoader.js deleted file mode 100644 index b8b0f8c4eb6..00000000000 --- a/extensions/html-language-features/server/build/javaScriptLibraryLoader.js +++ /dev/null @@ -1,132 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// a webpack loader that bundles all library definitions (d.ts) for the embedded JavaScript engine. - -const path = require('path'); -const fs = require('fs'); - -const TYPESCRIPT_LIB_SOURCE = path.join(__dirname, '../../../node_modules/typescript/lib'); -const JQUERY_DTS = path.join(__dirname, '../lib/jquery.d.ts'); - -module.exports = function () { - function getFileName(name) { - return (name === '' ? 'lib.d.ts' : `lib.${name}.d.ts`); - } - function readLibFile(name) { - var srcPath = path.join(TYPESCRIPT_LIB_SOURCE, getFileName(name)); - return fs.readFileSync(srcPath).toString(); - } - - var queue = []; - var in_queue = {}; - - var enqueue = function (name) { - if (in_queue[name]) { - return; - } - in_queue[name] = true; - queue.push(name); - }; - - enqueue('es2020.full'); - - var result = []; - while (queue.length > 0) { - var name = queue.shift(); - var contents = readLibFile(name); - var lines = contents.split(/\r\n|\r|\n/); - - var outputLines = []; - for (let i = 0; i < lines.length; i++) { - let m = lines[i].match(/\/\/\/\s*= 0; i--) { - strResult += `"${result[i].name}": ${result[i].output},\n`; - } - strResult += `\n};` - - strResult += `export function loadLibrary(name: string) : string {\n return libs[name] || ''; \n}`; - - return strResult; -} - -/** - * Escape text such that it can be used in a javascript string enclosed by double quotes (") - */ -function escapeText(text) { - // See http://www.javascriptkit.com/jsref/escapesequence.shtml - var _backspace = '\b'.charCodeAt(0); - var _formFeed = '\f'.charCodeAt(0); - var _newLine = '\n'.charCodeAt(0); - var _nullChar = 0; - var _carriageReturn = '\r'.charCodeAt(0); - var _tab = '\t'.charCodeAt(0); - var _verticalTab = '\v'.charCodeAt(0); - var _backslash = '\\'.charCodeAt(0); - var _doubleQuote = '"'.charCodeAt(0); - - var startPos = 0, chrCode, replaceWith = null, resultPieces = []; - - for (var i = 0, len = text.length; i < len; i++) { - chrCode = text.charCodeAt(i); - switch (chrCode) { - case _backspace: - replaceWith = '\\b'; - break; - case _formFeed: - replaceWith = '\\f'; - break; - case _newLine: - replaceWith = '\\n'; - break; - case _nullChar: - replaceWith = '\\0'; - break; - case _carriageReturn: - replaceWith = '\\r'; - break; - case _tab: - replaceWith = '\\t'; - break; - case _verticalTab: - replaceWith = '\\v'; - break; - case _backslash: - replaceWith = '\\\\'; - break; - case _doubleQuote: - replaceWith = '\\"'; - break; - } - if (replaceWith !== null) { - resultPieces.push(text.substring(startPos, i)); - resultPieces.push(replaceWith); - startPos = i + 1; - replaceWith = null; - } - } - resultPieces.push(text.substring(startPos, len)); - return resultPieces.join(''); -} diff --git a/extensions/json-language-features/server/.npmignore b/extensions/json-language-features/server/.npmignore index 960a01cc7b5..f85ce05804a 100644 --- a/extensions/json-language-features/server/.npmignore +++ b/extensions/json-language-features/server/.npmignore @@ -6,5 +6,4 @@ test/ tsconfig.json .gitignore package-lock.json -extension.webpack.config.js vscode-json-languageserver-*.tgz diff --git a/extensions/mangle-loader.js b/extensions/mangle-loader.js deleted file mode 100644 index ed32a85e633..00000000000 --- a/extensions/mangle-loader.js +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -const fs = require('fs'); -const webpack = require('webpack'); -const fancyLog = require('fancy-log'); -const ansiColors = require('ansi-colors'); -const { Mangler } = require('../build/lib/mangle/index.js'); - -/** - * Map of project paths to mangled file contents - * - * @type {Map>>} - */ -const mangleMap = new Map(); - -/** - * @param {string} projectPath - */ -function getMangledFileContents(projectPath) { - let entry = mangleMap.get(projectPath); - if (!entry) { - const log = (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data); - log(`Mangling ${projectPath}`); - const ts2tsMangler = new Mangler(projectPath, log, { mangleExports: true, manglePrivateFields: true }); - entry = ts2tsMangler.computeNewFileContents(); - mangleMap.set(projectPath, entry); - } - - return entry; -} - -/** - * @type {webpack.LoaderDefinitionFunction} - */ -module.exports = async function (source, sourceMap, meta) { - if (this.mode !== 'production') { - // Only enable mangling in production builds - return source; - } - if (true) { - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - return source; - } - const options = this.getOptions(); - if (options.disabled) { - // Dynamically disabled - return source; - } - - if (source !== fs.readFileSync(this.resourcePath).toString()) { - // File content has changed by previous webpack steps. - // Skip mangling. - return source; - } - - const callback = this.async(); - - const fileContentsMap = await getMangledFileContents(options.configFile); - - const newContents = fileContentsMap.get(this.resourcePath); - callback(null, newContents?.out ?? source, sourceMap, meta); -}; diff --git a/extensions/media-preview/.vscodeignore b/extensions/media-preview/.vscodeignore index 8621eb9e9f4..ca6d6ff79d7 100644 --- a/extensions/media-preview/.vscodeignore +++ b/extensions/media-preview/.vscodeignore @@ -7,4 +7,3 @@ out/** cgmanifest.json package-lock.json preview-src/** -webpack.config.js diff --git a/extensions/mermaid-chat-features/.vscodeignore b/extensions/mermaid-chat-features/.vscodeignore index 4722e586990..485bbd8df38 100644 --- a/extensions/mermaid-chat-features/.vscodeignore +++ b/extensions/mermaid-chat-features/.vscodeignore @@ -1,8 +1,6 @@ src/** -extension.webpack.config.js esbuild.* cgmanifest.json package-lock.json -webpack.config.js tsconfig*.json .gitignore diff --git a/extensions/shared.webpack.config.mjs b/extensions/shared.webpack.config.mjs deleted file mode 100644 index 12b1ea522a4..00000000000 --- a/extensions/shared.webpack.config.mjs +++ /dev/null @@ -1,209 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import path from 'node:path'; -import fs from 'node:fs'; -import merge from 'merge-options'; -import CopyWebpackPlugin from 'copy-webpack-plugin'; -import webpack from 'webpack'; -import { createRequire } from 'node:module'; - -/** @typedef {import('webpack').Configuration} WebpackConfig **/ - -const require = createRequire(import.meta.url); - -const tsLoaderOptions = { - compilerOptions: { - 'sourceMap': true, - }, - onlyCompileBundledFiles: true, -}; - -function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfig) { - const defaultConfig = { - mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') - target: 'node', // extensions run in a node context - node: { - __dirname: false // leave the __dirname-behaviour intact - }, - - resolve: { - conditionNames: ['import', 'require', 'node-addons', 'node'], - mainFields: ['module', 'main'], - extensions: ['.ts', '.js'], // support ts-files and js-files - extensionAlias: { - // this is needed to resolve dynamic imports that now require the .js extension - '.js': ['.js', '.ts'], - } - }, - module: { - rules: [{ - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - // configure TypeScript loader: - // * enable sources maps for end-to-end source maps - loader: 'ts-loader', - options: tsLoaderOptions - }, - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - // { - // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), - // options: { - // configFile: path.join(extConfig.context, 'tsconfig.json') - // }, - // }, - ] - }] - }, - externals: { - 'electron': 'commonjs electron', // ignored to avoid bundling from node_modules - 'vscode': 'commonjs vscode', // ignored because it doesn't exist, - 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module - '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use - '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module - '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module - '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module - }, - output: { - // all output goes into `dist`. - // packaging depends on that and this must always be like it - filename: '[name].js', - path: path.join(extConfig.context, 'dist'), - libraryTarget: 'commonjs', - }, - // yes, really source maps - devtool: 'source-map', - plugins: nodePlugins(extConfig.context), - }; - - return merge(defaultConfig, extConfig); -} - -/** - * - * @param {string} context - */ -function nodePlugins(context) { - // Need to find the top-most `package.json` file - const folderName = path.relative(import.meta.dirname, context).split(/[\\\/]/)[0]; - const pkgPath = path.join(import.meta.dirname, folderName, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - const id = `${pkg.publisher}.${pkg.name}`; - return [ - new CopyWebpackPlugin({ - patterns: [ - { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } - ] - }) - ]; -} -/** - * @typedef {{ - * configFile?: string - * }} AdditionalBrowserConfig - */ - -function withBrowserDefaults(/**@type WebpackConfig & { context: string }*/extConfig, /** @type AdditionalBrowserConfig */ additionalOptions = {}) { - /** @type WebpackConfig */ - const defaultConfig = { - mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') - target: 'webworker', // extensions run in a webworker context - resolve: { - mainFields: ['browser', 'module', 'main'], - extensions: ['.ts', '.js'], // support ts-files and js-files - fallback: { - 'path': require.resolve('path-browserify'), - 'os': require.resolve('os-browserify'), - 'util': require.resolve('util') - }, - extensionAlias: { - // this is needed to resolve dynamic imports that now require the .js extension - '.js': ['.js', '.ts'], - }, - }, - module: { - rules: [{ - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - // configure TypeScript loader: - // * enable sources maps for end-to-end source maps - loader: 'ts-loader', - options: { - ...tsLoaderOptions, - // ...(additionalOptions ? {} : { configFile: additionalOptions.configFile }), - } - }, - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - // { - // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), - // options: { - // configFile: path.join(extConfig.context, additionalOptions?.configFile ?? 'tsconfig.json') - // }, - // }, - ] - }, { - test: /\.wasm$/, - type: 'asset/inline' - }] - }, - externals: { - 'vscode': 'commonjs vscode', // ignored because it doesn't exist, - 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module - '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use - '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module - '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module - '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module - }, - performance: { - hints: false - }, - output: { - // all output goes into `dist`. - // packaging depends on that and this must always be like it - filename: '[name].js', - path: path.join(extConfig.context, 'dist', 'browser'), - libraryTarget: 'commonjs', - }, - // yes, really source maps - devtool: 'source-map', - plugins: browserPlugins(extConfig.context) - }; - - return merge(defaultConfig, extConfig); -} - -/** - * - * @param {string} context - */ -function browserPlugins(context) { - // Need to find the top-most `package.json` file - // const folderName = path.relative(__dirname, context).split(/[\\\/]/)[0]; - // const pkgPath = path.join(__dirname, folderName, 'package.json'); - // const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - // const id = `${pkg.publisher}.${pkg.name}`; - return [ - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 - }), - new CopyWebpackPlugin({ - patterns: [ - { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } - ] - }), - new webpack.DefinePlugin({ - 'process.platform': JSON.stringify('web'), - 'process.env': JSON.stringify({}), - 'process.env.BROWSER_ENV': JSON.stringify('true') - }) - ]; -} - -export default withNodeDefaults; -export { withNodeDefaults as node, withBrowserDefaults as browser, nodePlugins, browserPlugins }; diff --git a/package-lock.json b/package-lock.json index 03648e4b2cc..03536ba0360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,6 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", - "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -99,8 +98,6 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.6.0", @@ -110,7 +107,6 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", - "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -151,17 +147,12 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", - "style-loader": "^3.3.2", "tar": "^7.5.9", - "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4", - "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, @@ -823,15 +814,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", - "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@electron/get": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", @@ -1374,28 +1356,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", @@ -2371,17 +2331,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2608,17 +2557,6 @@ "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", "dev": true }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.7", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", @@ -3816,167 +3754,6 @@ "hasInstallScript": true, "license": "MIT" }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", @@ -3984,50 +3761,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -4156,20 +3889,6 @@ "addons/*" ] }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4203,19 +3922,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", - "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4251,55 +3957,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -4923,15 +4580,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5420,24 +5068,6 @@ } } }, - "node_modules/chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/chrome-trace-event/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -5636,41 +5266,6 @@ "node": ">= 0.10" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clone-deep/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -5782,12 +5377,6 @@ "color-support": "bin.js" } }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5988,73 +5577,6 @@ "is-plain-object": "^5.0.0" } }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", - "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -6167,32 +5689,6 @@ "source-map-resolve": "^0.6.0" } }, - "node_modules/css-loader": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz", - "integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -6234,18 +5730,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -6614,18 +6098,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -6840,15 +6312,6 @@ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -6902,30 +6365,6 @@ "node": ">=6" } }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7892,12 +7331,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, "node_modules/fastq": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", @@ -7945,44 +7378,6 @@ "node": ">=16.0.0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -8715,13 +8110,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/glob-watcher": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", @@ -10862,18 +10250,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -10925,22 +10301,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -11572,37 +10932,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -11730,12 +11059,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -12191,34 +11514,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -12254,12 +11549,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.clone": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", - "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", - "dev": true - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -12278,12 +11567,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -12677,34 +11960,6 @@ "timers-ext": "^0.1.7" } }, - "node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/memory-fs/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -12739,13 +11994,6 @@ "node": ">=4" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -13206,25 +12454,6 @@ "dev": true, "optional": true }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -13291,12 +12520,6 @@ "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -14316,15 +13539,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -14425,70 +13639,6 @@ "node": ">=16.20.0" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/playwright": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", @@ -14584,114 +13734,6 @@ "node": ">=0.10.0" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", - "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", - "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -14825,12 +13867,6 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY= sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true - }, "node_modules/pseudo-localization": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/pseudo-localization/-/pseudo-localization-2.4.0.tgz", @@ -15420,27 +14456,6 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", @@ -15739,61 +14754,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -16031,27 +14991,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallow-clone/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16525,16 +15464,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-resolve": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", @@ -17008,22 +15937,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-loader": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", - "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -17310,78 +16223,6 @@ "node": ">=6.0.0" } }, - "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -17709,35 +16550,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/ts-morph": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz", @@ -18509,20 +17321,6 @@ "dev": true, "license": "MIT" }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -18535,245 +17333,6 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, - "node_modules/webpack": { - "version": "5.105.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", - "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-cli/node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-cli/node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", - "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", - "dev": true, - "dependencies": { - "fancy-log": "^1.3.3", - "lodash.clone": "^4.3.2", - "lodash.some": "^4.2.2", - "memory-fs": "^0.5.0", - "plugin-error": "^1.0.1", - "supports-color": "^8.1.1", - "through": "^2.3.8", - "vinyl": "^2.2.1" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "webpack": "^5.21.2" - } - }, - "node_modules/webpack-stream/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/webpack-stream/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/webpack-stream/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -18824,12 +17383,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, "node_modules/windows-foreground-love": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/windows-foreground-love/-/windows-foreground-love-0.6.1.tgz", diff --git a/package.json b/package.json index 7bc8f3f17d2..2cbaf453e16 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,6 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", - "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -169,8 +168,6 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.6.0", @@ -180,7 +177,6 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", - "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -221,17 +217,12 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", - "style-loader": "^3.3.2", "tar": "^7.5.9", - "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4", - "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 513d33eeb34..e1e44348099 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -8,11 +8,79 @@ "name": "test-monaco", "version": "1.0.0", "license": "MIT", + "dependencies": { + "postcss": "^8.5.6" + }, "devDependencies": { "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "warnings-to-errors-webpack-plugin": "^2.3.0" + "css-loader": "^6.9.1", + "file-loader": "^6.2.0", + "style-loader": "^3.3.2", + "warnings-to-errors-webpack-plugin": "^2.3.0", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@types/chai": { @@ -21,6 +89,42 @@ "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", "dev": true }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/junit-report-builder": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", @@ -28,6 +132,312 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -80,6 +490,91 @@ "playwright": ">1.0.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -106,6 +601,122 @@ "node": "*" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -118,6 +729,273 @@ "node": ">=0.12" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -128,6 +1006,174 @@ "node": "*" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/junit-report-builder": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz", @@ -143,6 +1189,58 @@ "node": ">=16" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -166,6 +1264,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -176,6 +1304,104 @@ "mustache": "bin/mustache" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -189,9 +1415,261 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -202,6 +1680,196 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -211,6 +1879,61 @@ "node": ">=4" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/warnings-to-errors-webpack-plugin": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/warnings-to-errors-webpack-plugin/-/warnings-to-errors-webpack-plugin-2.3.0.tgz", @@ -220,6 +1943,173 @@ "webpack": "^2.2.0-rc || ^3 || ^4 || ^5" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", + "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/test/monaco/package.json b/test/monaco/package.json index c7373919431..89902f2304f 100644 --- a/test/monaco/package.json +++ b/test/monaco/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "compile": "node ../../node_modules/typescript/bin/tsc", - "bundle-webpack": "node ../../node_modules/webpack/bin/webpack --config ./webpack.config.js --bail", + "bundle-webpack": "webpack --config ./webpack.config.js --bail", "esm-check": "node esm-check/esm-check.js", "test": "node runner.js" }, @@ -14,6 +14,14 @@ "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "warnings-to-errors-webpack-plugin": "^2.3.0" + "css-loader": "^6.9.1", + "file-loader": "^6.2.0", + "style-loader": "^3.3.2", + "warnings-to-errors-webpack-plugin": "^2.3.0", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "postcss": "^8.5.6" } } From 2f76a2d97226f50eb529669084382b8276ba92ba Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:08:01 -0800 Subject: [PATCH 080/448] Polish question carousel (#298377) * Polish question carousel: keyboard nav, badge styling, focus outlines * chat: prioritize selected single-select answer over freeform draft * Enhance question carousel with tab navigation and review functionality * Polish question carousel: stack title/desc, plain numbers, multiline Q:/A: summary, Cmd+Enter submit * Polish question carousel titles and execute controls * Address PR feedback: guard checkmark on actual answers, restore queue/steer, deduplicate format helper, fix JSDoc * Fix carousel tests: update selectors for tab-bar UI * Fix remaining carousel tests from main merge * Add ARIA tabpanel pattern and clear tab indicators on dispose * Adjust tab bar padding-left to 4px for multi-question carousels * Fix CI: non-null assertion and remove unused constants * fix: show steer/queue submenu during question carousel when text is typed - Cancel button now hides when input has text during question carousel (matches behavior during regular in-progress requests) - Queue/steer submenu now appears during question carousel and tool confirmation states, since requestInProgress is false in those states - Removed unused requestInProgressWithoutInput and pendingToolCall vars --- extensions/theme-2026/themes/2026-dark.json | 2 +- extensions/theme-2026/themes/styles.css | 16 + .../browser/actions/chatExecuteActions.ts | 20 +- .../chat/browser/actions/chatQueueActions.ts | 7 +- .../chatQuestionCarouselPart.ts | 532 ++++++++++++------ .../media/chatQuestionCarousel.css | 319 ++++++----- .../tools/builtinTools/askQuestionsTool.ts | 23 +- .../chatQuestionCarouselPart.test.ts | 96 ++-- .../builtinTools/askQuestionsTool.test.ts | 18 +- 9 files changed, 630 insertions(+), 403 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4e720722e8f..032cdd12cf3 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -53,7 +53,7 @@ "badge.background": "#3994BCF0", "badge.foreground": "#FFFFFF", "progressBar.background": "#878889", - "list.activeSelectionBackground": "#3994BC26", + "list.activeSelectionBackground": "#262728", "list.activeSelectionForeground": "#ededed", "list.inactiveSelectionBackground": "#2C2D2E", "list.inactiveSelectionForeground": "#ededed", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index af8bc218032..050f706aa1e 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -81,6 +81,22 @@ /* Chat Widget */ +.monaco-workbench .interactive-session .chat-question-carousel-container { + border-radius: var(--radius-lg); +} + +.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; +} + +.monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { + border-radius: 0 0 var(--radius-lg) var(--radius-lg); +} + +.monaco-workbench.vs .interactive-session .chat-input-container { + box-shadow: inset var(--shadow-sm); +} .monaco-workbench .part.panel .interactive-session, .monaco-workbench .part.auxiliarybar .interactive-session { position: relative; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 29fb309c4db..4d7b9f7fa8f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -185,15 +185,8 @@ const requestInProgressOrPendingToolCall = ContextKeyExpr.or( ChatContextKeys.Editing.hasToolConfirmation, ChatContextKeys.Editing.hasQuestionCarousel, ); -const requestInProgressWithoutInput = ContextKeyExpr.and( - ChatContextKeys.requestInProgress, - ChatContextKeys.inputHasText.negate(), -); -const pendingToolCall = ContextKeyExpr.or( - ChatContextKeys.Editing.hasToolConfirmation, - ChatContextKeys.Editing.hasQuestionCarousel, -); const whenNotInProgress = ChatContextKeys.requestInProgress.negate(); +const whenNoRequestOrPendingToolCall = requestInProgressOrPendingToolCall!.negate(); export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; @@ -202,7 +195,7 @@ export class ChatSubmitAction extends SubmitAction { const menuCondition = ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask); const precondition = ContextKeyExpr.and( ChatContextKeys.inputHasText, - whenNotInProgress, + whenNoRequestOrPendingToolCall, ChatContextKeys.chatSessionOptionsValid, ); @@ -231,7 +224,7 @@ export class ChatSubmitAction extends SubmitAction { id: MenuId.ChatExecute, order: 4, when: ContextKeyExpr.and( - whenNotInProgress, + whenNoRequestOrPendingToolCall, menuCondition, ChatContextKeys.withinEditSessionDiff.negate(), ), @@ -247,7 +240,7 @@ export class ChatSubmitAction extends SubmitAction { order: 4, when: ContextKeyExpr.and( ContextKeyExpr.or(ctxHasEditorModification.negate(), ChatContextKeys.inputHasText), - whenNotInProgress, + whenNoRequestOrPendingToolCall, ChatContextKeys.requestInProgress.negate(), menuCondition ), @@ -664,7 +657,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { constructor() { const notInProgressOrEditing = ContextKeyExpr.and( - ContextKeyExpr.or(whenNotInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), + ContextKeyExpr.or(whenNoRequestOrPendingToolCall, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.QueueOrSteer) ); @@ -846,7 +839,8 @@ export class CancelAction extends Action2 { menu: [{ id: MenuId.ChatExecute, when: ContextKeyExpr.and( - ContextKeyExpr.or(requestInProgressWithoutInput, pendingToolCall), + requestInProgressOrPendingToolCall, + ChatContextKeys.inputHasText.negate(), ChatContextKeys.remoteJobCreating.negate(), ChatContextKeys.currentlyEditing.negate(), ), diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index 6606748d653..701177fb30d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -18,7 +18,12 @@ import { IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; const queuingActionsPresent = ContextKeyExpr.and( - ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)), + ContextKeyExpr.or( + ChatContextKeys.requestInProgress, + ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer), + ChatContextKeys.Editing.hasQuestionCarousel, + ChatContextKeys.Editing.hasToolConfirmation, + ), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent), ); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 5ce3306ae08..996b73a63f3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -10,6 +10,7 @@ import { Emitter, Event } from '../../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { isMacintosh } from '../../../../../../base/common/platform.js'; import { hasKey } from '../../../../../../base/common/types.js'; import { localize } from '../../../../../../nls.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; @@ -27,12 +28,9 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import './media/chatQuestionCarousel.css'; -const PREVIOUS_QUESTION_ACTION_ID = 'workbench.action.chat.previousQuestion'; -const NEXT_QUESTION_ACTION_ID = 'workbench.action.chat.nextQuestion'; export interface IChatQuestionCarouselOptions { onSubmit: (answers: Map | undefined) => void; shouldAutoFocus?: boolean; @@ -46,17 +44,15 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private _currentIndex = 0; private readonly _answers = new Map(); + private readonly _explicitlyAnsweredQuestionIds = new Set(); private _questionContainer: HTMLElement | undefined; private _closeButtonContainer: HTMLElement | undefined; + private _tabBar: HTMLElement | undefined; + private _tabItems: HTMLElement[] = []; + private readonly _questionTabIndicators = new Map(); + private _reviewIndex = -1; private _footerRow: HTMLElement | undefined; - private _stepIndicator: HTMLElement | undefined; - private _navigationButtons: HTMLElement | undefined; - private _prevButton: Button | undefined; - private _nextButton: Button | undefined; - private readonly _nextButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable()); - private _submitButton: Button | undefined; - private readonly _submitButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable()); private _skipAllButton: Button | undefined; private _isSkipped = false; @@ -83,7 +79,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent @IHoverService private readonly _hoverService: IHoverService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); @@ -135,7 +130,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._interactiveUIStore.value = interactiveStore; // Question container + const questionPanelId = `question-panel-${this.carousel.questions[0]?.id ?? 'default'}`; this._questionContainer = dom.$('.chat-question-carousel-content'); + this._questionContainer.setAttribute('role', 'tabpanel'); + this._questionContainer.id = questionPanelId; this.domNode.append(this._questionContainer); // Close/skip button (X) - placed in header row, only shown when allowSkip is true @@ -150,49 +148,75 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._skipAllButton = skipAllButton; } - // Footer row with step indicator and navigation buttons - this._footerRow = dom.$('.chat-question-footer-row'); + const isSingleQuestion = this.carousel.questions.length === 1; - // Step indicator (e.g., "2/4") on the left - this._stepIndicator = dom.$('.chat-question-step-indicator'); - this._footerRow.appendChild(this._stepIndicator); + if (!isSingleQuestion) { + this._reviewIndex = this.carousel.questions.length; - // Navigation controls (< >) - placed in footer row - this._navigationButtons = dom.$('.chat-question-carousel-nav'); - this._navigationButtons.setAttribute('role', 'navigation'); - this._navigationButtons.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); + // Multi-question: Create tab bar with question tabs and Review tab + this._tabBar = dom.$('.chat-question-tab-bar'); + const tabList = dom.$('.chat-question-tabs'); + tabList.setAttribute('role', 'tablist'); + tabList.setAttribute('aria-label', localize('chat.questionCarousel.tabBarLabel', 'Questions')); + this._tabBar.appendChild(tabList); - // Group prev/next buttons together - const arrowsContainer = dom.$('.chat-question-nav-arrows'); + this.carousel.questions.forEach((question, index) => { + const tab = dom.$('.chat-question-tab'); + tab.setAttribute('role', 'tab'); + tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false'); + tab.tabIndex = index === 0 ? 0 : -1; + tab.id = `question-tab-${question.id}-${index}`; + tab.setAttribute('aria-controls', questionPanelId); - const previousLabel = localize('previous', 'Previous'); - const previousLabelWithKeybinding = this.getLabelWithKeybinding(previousLabel, PREVIOUS_QUESTION_ACTION_ID); - const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); - prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev'); - prevButton.label = `$(${Codicon.chevronLeft.id})`; - prevButton.element.setAttribute('aria-label', previousLabelWithKeybinding); - interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabelWithKeybinding })); - this._prevButton = prevButton; + const displayTitle = this.getQuestionText(question.title); + const tabIndicator = dom.$('.chat-question-tab-indicator.codicon'); + const tabLabel = dom.$('span.chat-question-tab-label'); + tabLabel.textContent = displayTitle; + tab.append(tabIndicator, tabLabel); + tab.setAttribute('aria-label', displayTitle); + this._questionTabIndicators.set(question.id, tabIndicator); - const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); - nextButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-next'); - nextButton.label = `$(${Codicon.chevronRight.id})`; - this._nextButton = nextButton; + interactiveStore.add(dom.addDisposableListener(tab, dom.EventType.CLICK, () => { + this.saveCurrentAnswer(); + this._currentIndex = index; + this.renderCurrentQuestion(true); + tab.focus(); + })); - const submitButton = interactiveStore.add(new Button(this._navigationButtons, { ...defaultButtonStyles })); - submitButton.element.classList.add('chat-question-submit-button'); - submitButton.label = localize('submit', 'Submit'); - this._submitButton = submitButton; + tabList.appendChild(tab); + this._tabItems.push(tab); + }); - this._navigationButtons.appendChild(arrowsContainer); - this._footerRow.appendChild(this._navigationButtons); - this.domNode.append(this._footerRow); + // Review tab + const reviewTab = dom.$('.chat-question-tab.no-icon'); + reviewTab.setAttribute('role', 'tab'); + reviewTab.setAttribute('aria-selected', 'false'); + reviewTab.tabIndex = -1; + reviewTab.id = 'question-tab-review'; + reviewTab.setAttribute('aria-controls', questionPanelId); + const reviewLabel = localize('chat.questionCarousel.review', 'Review'); + reviewTab.textContent = reviewLabel; + reviewTab.setAttribute('aria-label', reviewLabel); + interactiveStore.add(dom.addDisposableListener(reviewTab, dom.EventType.CLICK, () => { + this.saveCurrentAnswer(); + this._currentIndex = this._reviewIndex; + this.renderCurrentQuestion(true); + reviewTab.focus(); + })); + tabList.appendChild(reviewTab); + this._tabItems.push(reviewTab); + // Controls container for close button only + if (this._closeButtonContainer) { + const controlsContainer = dom.$('.chat-question-tab-controls'); + controlsContainer.appendChild(this._closeButtonContainer); + this._tabBar.appendChild(controlsContainer); + } + + this.domNode.insertBefore(this._tabBar, this._questionContainer!); + } // Register event listeners - interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1))); - interactiveStore.add(nextButton.onDidClick(() => this.navigate(1))); - interactiveStore.add(submitButton.onDidClick(() => this.submit())); if (this._skipAllButton) { interactiveStore.add(this._skipAllButton.onDidClick(() => this.ignore())); } @@ -204,6 +228,37 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent e.preventDefault(); e.stopPropagation(); this.ignore(); + } else if (!isSingleQuestion && (event.keyCode === KeyCode.RightArrow || event.keyCode === KeyCode.LeftArrow)) { + // Arrow L/R navigates tabs from anywhere in the carousel, + // except when focus is in a text input or textarea (where arrows move cursor) + const target = e.target as HTMLElement; + const isTextInput = target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'; + const isTextarea = target.tagName === 'TEXTAREA'; + if (!isTextInput && !isTextarea) { + e.preventDefault(); + e.stopPropagation(); + const totalTabs = this._tabItems.length; // includes Review tab + if (event.keyCode === KeyCode.RightArrow) { + if (this._currentIndex < totalTabs - 1) { + this.saveCurrentAnswer(); + this._currentIndex++; + this.renderCurrentQuestion(true); + this._tabItems[this._currentIndex]?.focus(); + } + } else { + if (this._currentIndex > 0) { + this.saveCurrentAnswer(); + this._currentIndex--; + this.renderCurrentQuestion(true); + this._tabItems[this._currentIndex]?.focus(); + } + } + } + } else if (event.keyCode === KeyCode.Enter && (event.metaKey || event.ctrlKey)) { + // Cmd/Ctrl+Enter submits immediately from anywhere + e.preventDefault(); + e.stopPropagation(); + this.submit(); } else if (event.keyCode === KeyCode.Enter && !event.shiftKey) { // Handle Enter key for text inputs and freeform textareas, not radio/checkbox or buttons // Buttons have their own Enter/Space handling via Button class @@ -229,6 +284,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private saveCurrentAnswer(): void { const currentQuestion = this.carousel.questions[this._currentIndex]; + if (!currentQuestion) { + return; // Review tab or out of bounds + } const answer = this.getCurrentAnswer(); if (answer !== undefined) { this._answers.set(currentQuestion.id, answer); @@ -268,14 +326,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private handleNextOrSubmit(): void { this.saveCurrentAnswer(); + const currentQuestion = this.carousel.questions[this._currentIndex]; + if (currentQuestion && this.getCurrentAnswer() !== undefined) { + this._explicitlyAnsweredQuestionIds.add(currentQuestion.id); + this.updateQuestionTabIndicators(); + } if (this._currentIndex < this.carousel.questions.length - 1) { // Move to next question this._currentIndex++; this.persistDraftState(); this.renderCurrentQuestion(true); + } else if (this.carousel.questions.length > 1) { + // Multi-question: navigate to Review tab + this._currentIndex = this._reviewIndex; + this.renderCurrentQuestion(true); + this._tabItems[this._currentIndex]?.focus(); } else { - // Submit + // Single question: submit directly this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -286,6 +354,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private submit(): void { this.saveCurrentAnswer(); + const currentQuestion = this.carousel.questions[this._currentIndex]; + if (currentQuestion) { + this._explicitlyAnsweredQuestionIds.add(currentQuestion.id); + } this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -336,19 +408,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._singleSelectItems.clear(); this._multiSelectCheckboxes.clear(); this._freeformTextareas.clear(); - this._nextButtonHover.value = undefined; - this._submitButtonHover.value = undefined; // Clear references to disposed elements - this._prevButton = undefined; - this._nextButton = undefined; - this._submitButton = undefined; this._skipAllButton = undefined; this._questionContainer = undefined; - this._navigationButtons = undefined; this._closeButtonContainer = undefined; + this._tabBar = undefined; + this._tabItems = []; + this._questionTabIndicators.clear(); + this._reviewIndex = -1; this._footerRow = undefined; - this._stepIndicator = undefined; + this._explicitlyAnsweredQuestionIds.clear(); } /** @@ -512,7 +582,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } private renderCurrentQuestion(focusContainerForScreenReader: boolean = false): void { - if (!this._questionContainer || !this._prevButton || !this._nextButton || !this._submitButton) { + if (!this._questionContainer) { return; } @@ -526,60 +596,102 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._multiSelectCheckboxes.clear(); this._freeformTextareas.clear(); + // Remove footer if it exists from a previous Review render + if (this._footerRow) { + this._footerRow.remove(); + this._footerRow = undefined; + } + // Clear previous content dom.clearNode(this._questionContainer); + const isSingleQuestion = this.carousel.questions.length === 1; + const isReview = !isSingleQuestion && this._currentIndex === this._reviewIndex; + + // Update tab bar active state for multi-question carousels + if (!isSingleQuestion) { + this._tabItems.forEach((tab, index) => { + const isActive = index === this._currentIndex; + tab.classList.toggle('active', isActive); + tab.setAttribute('aria-selected', String(isActive)); + tab.tabIndex = isActive ? 0 : -1; + }); + // Link the panel to the active tab for screen readers + const activeTab = this._tabItems[this._currentIndex]; + if (activeTab) { + this._questionContainer.setAttribute('aria-labelledby', activeTab.id); + } + this.updateQuestionTabIndicators(); + } + + if (isReview) { + this.renderReviewPanel(questionRenderStore); + } else { + this.renderQuestionPanel(questionRenderStore, isSingleQuestion); + } + + // Update aria-label to reflect the current question + this._updateAriaLabel(); + + // In screen reader mode, focus the container and announce the question + if (focusContainerForScreenReader && this._accessibilityService.isScreenReaderOptimized()) { + this._focusContainerAndAnnounce(); + } + + this._onDidChangeHeight.fire(); + } + + /** + * Renders a question panel (title, message, input) inside the question container. + */ + private renderQuestionPanel(questionRenderStore: DisposableStore, isSingleQuestion: boolean): void { const question = this.carousel.questions[this._currentIndex]; - if (!question) { + if (!question || !this._questionContainer) { return; } - // Render question header row with title and close button - const headerRow = dom.$('.chat-question-header-row'); - const titleRow = dom.$('.chat-question-title-row'); + // Render question header row with title and close button (single question only) + if (isSingleQuestion) { + const headerRow = dom.$('.chat-question-header-row'); + const titleRow = dom.$('.chat-question-title-row'); - // Render question title (short header) in the header bar as plain text - if (question.title) { - const title = dom.$('.chat-question-title'); - const questionText = question.title; - const messageContent = this.getQuestionText(questionText); + if (question.title) { + const title = dom.$('.chat-question-title'); + const questionText = question.title; + const messageContent = this.getQuestionText(questionText); - title.setAttribute('aria-label', messageContent); + title.setAttribute('aria-label', messageContent); - if (question.message !== undefined) { - const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); - const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); - title.appendChild(renderedTitle.element); - } else { - // Check for subtitle in parentheses at the end - const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); - if (parenMatch) { - // Main title (bold) - const mainTitle = dom.$('span.chat-question-title-main'); - mainTitle.textContent = parenMatch[1]; - title.appendChild(mainTitle); - - // Subtitle in parentheses (normal weight) - const subtitle = dom.$('span.chat-question-title-subtitle'); - subtitle.textContent = ' ' + parenMatch[2]; - title.appendChild(subtitle); + if (question.message !== undefined) { + const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); + const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); + title.appendChild(renderedTitle.element); } else { - title.textContent = messageContent; + const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); + if (parenMatch) { + const mainTitle = dom.$('span.chat-question-title-main'); + mainTitle.textContent = parenMatch[1]; + title.appendChild(mainTitle); + + const subtitle = dom.$('span.chat-question-title-subtitle'); + subtitle.textContent = ' ' + parenMatch[2]; + title.appendChild(subtitle); + } else { + title.textContent = messageContent; + } } + titleRow.appendChild(title); } - titleRow.appendChild(title); + + if (this._closeButtonContainer) { + titleRow.appendChild(this._closeButtonContainer); + } + + headerRow.appendChild(titleRow); + this._questionContainer.appendChild(headerRow); } - // Add close button to header row (if allowSkip is enabled) - if (this._closeButtonContainer) { - titleRow.appendChild(this._closeButtonContainer); - } - - headerRow.appendChild(titleRow); - - this._questionContainer.appendChild(headerRow); - - // Render full question text below the header row (supports multi-line and markdown) + // Render full question text below the header row if (question.message) { const messageEl = dom.$('.chat-question-message'); if (isMarkdownString(question.message)) { @@ -591,56 +703,79 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._questionContainer.appendChild(messageEl); } - const isSingleQuestion = this.carousel.questions.length === 1; - // Update step indicator in footer - if (this._stepIndicator) { - this._stepIndicator.textContent = `${this._currentIndex + 1}/${this.carousel.questions.length}`; - this._stepIndicator.style.display = isSingleQuestion ? 'none' : ''; - } - // Render input based on question type const inputContainer = dom.$('.chat-question-input-container'); this.renderInput(inputContainer, question); this._questionContainer.appendChild(inputContainer); - - // Update navigation button states (prevButton and nextButton are guaranteed non-null from guard above) - this._prevButton!.enabled = this._currentIndex > 0; - this._prevButton!.element.style.display = isSingleQuestion ? 'none' : ''; - - // Keep navigation arrows stable and disable next on the last question - const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1; - const submitLabel = localize('submit', 'Submit'); - const nextLabel = localize('next', 'Next'); - const nextLabelWithKeybinding = this.getLabelWithKeybinding(nextLabel, NEXT_QUESTION_ACTION_ID); - this._nextButton!.label = `$(${Codicon.chevronRight.id})`; - this._nextButton!.enabled = !isLastQuestion; - this._nextButton!.element.setAttribute('aria-label', nextLabelWithKeybinding); - this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabelWithKeybinding }); - - this._submitButton!.enabled = isLastQuestion; - this._submitButton!.element.style.display = isLastQuestion ? '' : 'none'; - this._submitButton!.element.setAttribute('aria-label', submitLabel); - this._submitButtonHover.value = isLastQuestion - ? this._hoverService.setupDelayedHover(this._submitButton!.element, { content: submitLabel }) - : undefined; - - // Update aria-label to reflect the current question - this._updateAriaLabel(); - - // In screen reader mode, focus the container and announce the question - // This must happen after all render calls to avoid focus being stolen - if (focusContainerForScreenReader && this._accessibilityService.isScreenReaderOptimized()) { - this._focusContainerAndAnnounce(); - } - - this._onDidChangeHeight.fire(); } - private getLabelWithKeybinding(label: string, actionId: string): string { - const keybindingLabel = this._keybindingService.lookupKeybinding(actionId, this._contextKeyService)?.getLabel(); - return keybindingLabel - ? localize('chat.questionCarousel.labelWithKeybinding', '{0} ({1})', label, keybindingLabel) - : label; + /** + * Renders the review panel with a summary of all answers and a submit footer. + */ + private renderReviewPanel(questionRenderStore: DisposableStore): void { + if (!this._questionContainer) { + return; + } + + // Render inline review summary. + // If no explicit answers exist yet, show a single empty-state label. + // If some explicit answers exist, show all questions and mark missing ones as not answered yet. + const summaryContainer = dom.$('.chat-question-carousel-summary'); + const answeredCount = this.carousel.questions.filter(q => this._explicitlyAnsweredQuestionIds.has(q.id)).length; + + if (answeredCount === 0) { + const emptyLabel = dom.$('div.chat-question-summary-empty'); + emptyLabel.textContent = localize('chat.questionCarousel.noQuestionsAnsweredYet', 'No questions answered yet'); + summaryContainer.appendChild(emptyLabel); + this._questionContainer.appendChild(summaryContainer); + } else { + for (const question of this.carousel.questions) { + const summaryItem = dom.$('.chat-question-summary-item'); + + const questionRow = dom.$('div.chat-question-summary-label'); + const questionText = question.message ?? question.title; + let labelText = typeof questionText === 'string' ? questionText : questionText.value; + labelText = labelText.replace(/[:\s]+$/, ''); + questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); + summaryItem.appendChild(questionRow); + + const hasExplicitAnswer = this._explicitlyAnsweredQuestionIds.has(question.id); + const answer = this._answers.get(question.id); + + if (hasExplicitAnswer && answer !== undefined) { + const formattedAnswer = this.formatAnswerForSummary(question, answer); + const answerRow = dom.$('div.chat-question-summary-answer'); + answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); + summaryItem.appendChild(answerRow); + } else { + const unanswered = dom.$('div.chat-question-summary-unanswered'); + unanswered.textContent = localize('chat.questionCarousel.notAnsweredYet', 'Not answered yet'); + summaryItem.appendChild(unanswered); + } + + summaryContainer.appendChild(summaryItem); + } + + this._questionContainer.appendChild(summaryContainer); + } + + // Footer with Submit/Cancel appears only once at least one question is answered. + if (answeredCount > 0) { + this._footerRow = dom.$('.chat-question-footer-row'); + + const hint = dom.$('span.chat-question-submit-hint'); + hint.textContent = isMacintosh + ? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit') + : localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit'); + this._footerRow.appendChild(hint); + + const submitButton = questionRenderStore.add(new Button(this._footerRow, { ...defaultButtonStyles })); + submitButton.element.classList.add('chat-question-submit-button'); + submitButton.label = localize('submit', 'Submit'); + questionRenderStore.add(submitButton.onDidClick(() => this.submit())); + + this.domNode.append(this._footerRow); + } } private renderInput(container: HTMLElement, question: IChatQuestion): void { @@ -726,7 +861,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const listItems: HTMLElement[] = []; const indicators: HTMLElement[] = []; - const updateSelection = (newIndex: number) => { + const updateSelection = (newIndex: number, isUserInitiated: boolean = false) => { // Update visual state listItems.forEach((item, i) => { const isSelected = i === newIndex; @@ -745,6 +880,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (data) { data.selectedIndex = newIndex; } + if (isUserInitiated) { + this.updateQuestionTabIndicators(); + } this.saveCurrentAnswer(); }; @@ -773,12 +911,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { - const titleSpan = dom.$('span.chat-question-list-label-title'); + const titleSpan = dom.$('div.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); - const descSpan = dom.$('span.chat-question-list-label-desc'); - descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3); + const descSpan = dom.$('div.chat-question-list-label-desc'); + descSpan.textContent = option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { label.textContent = option.label; @@ -794,7 +932,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._inputBoxes.add(dom.addDisposableListener(listItem, dom.EventType.CLICK, (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - updateSelection(index); + updateSelection(index, true); const freeform = this._freeformTextareas.get(question.id); if (freeform) { freeform.value = ''; @@ -840,9 +978,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // clear when we start typing in freeform this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { if (freeformTextarea.value.length > 0) { - updateSelection(-1); - } else { - this.saveCurrentAnswer(); + updateSelection(-1, true); + } + })); + + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.UpArrow && freeformTextarea.selectionStart === 0 && freeformTextarea.selectionEnd === 0 && listItems.length) { + e.preventDefault(); + const lastIndex = listItems.length - 1; + updateSelection(lastIndex, true); + listItems[lastIndex].focus(); } })); @@ -861,6 +1007,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (event.keyCode === KeyCode.DownArrow) { e.preventDefault(); + if (data.selectedIndex >= listItems.length - 1) { + updateSelection(-1); + freeformTextarea.focus(); + return; + } newIndex = Math.min(data.selectedIndex + 1, listItems.length - 1); } else if (event.keyCode === KeyCode.UpArrow) { e.preventDefault(); @@ -876,17 +1027,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const numberIndex = event.keyCode - KeyCode.Digit1; if (numberIndex < listItems.length) { e.preventDefault(); - updateSelection(numberIndex); + updateSelection(numberIndex, true); } else if (numberIndex === listItems.length) { e.preventDefault(); - updateSelection(-1); + updateSelection(-1, true); freeformTextarea.focus(); } return; } if (newIndex !== data.selectedIndex && newIndex >= 0) { - updateSelection(newIndex); + updateSelection(newIndex, true); } })); @@ -973,12 +1124,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { - const titleSpan = dom.$('span.chat-question-list-label-title'); + const titleSpan = dom.$('div.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); - const descSpan = dom.$('span.chat-question-list-label-desc'); - descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3); + const descSpan = dom.$('div.chat-question-list-label-desc'); + descSpan.textContent = option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { label.textContent = option.label; @@ -996,6 +1147,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._inputBoxes.add(checkbox.onChange(() => { listItem.classList.toggle('checked', checkbox.checked); listItem.setAttribute('aria-selected', String(checkbox.checked)); + this.updateQuestionTabIndicators(); this.saveCurrentAnswer(); })); @@ -1043,6 +1195,18 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const autoResize = this.setupTextareaAutoResize(freeformTextarea); this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => this.saveCurrentAnswer())); + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.UpArrow && freeformTextarea.selectionStart === 0 && freeformTextarea.selectionEnd === 0 && listItems.length) { + e.preventDefault(); + focusedIndex = listItems.length - 1; + listItems[focusedIndex].focus(); + } + })); + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { + this.updateQuestionTabIndicators(); + })); + freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); @@ -1058,6 +1222,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (event.keyCode === KeyCode.DownArrow) { e.preventDefault(); + if (focusedIndex >= listItems.length - 1) { + freeformTextarea.focus(); + return; + } focusedIndex = Math.min(focusedIndex + 1, listItems.length - 1); listItems[focusedIndex].focus(); } else if (event.keyCode === KeyCode.UpArrow) { @@ -1126,19 +1294,20 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (data && data.selectedIndex >= 0) { selectedValue = question.options?.[data.selectedIndex]?.value; } - // Find default option if nothing selected (defaultValue is the option id) + + // For single-select: freeform takes priority over selection. + const freeformTextarea = this._freeformTextareas.get(question.id); + const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; + if (freeformValue) { + return { selectedValue: undefined, freeformValue }; + } + + // Find default option if nothing selected and no freeform text (defaultValue is the option id) if (selectedValue === undefined && typeof question.defaultValue === 'string') { const defaultOption = question.options?.find(opt => opt.id === question.defaultValue); selectedValue = defaultOption?.value; } - // For single-select: if freeform is provided, use ONLY freeform (ignore selection) - const freeformTextarea = this._freeformTextareas.get(question.id); - const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; - if (freeformValue) { - // Freeform takes priority - ignore selectedValue - return { selectedValue: undefined, freeformValue }; - } if (selectedValue !== undefined) { return { selectedValue, freeformValue: undefined }; } @@ -1209,35 +1378,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const summaryItem = dom.$('.chat-question-summary-item'); - // Category label (use same text as shown in question UI: message ?? title) - const questionLabel = dom.$('span.chat-question-summary-label'); + // Question row with Q: prefix + const questionRow = dom.$('div.chat-question-summary-label'); const questionText = question.message ?? question.title; let labelText = typeof questionText === 'string' ? questionText : questionText.value; - // Remove trailing colons and whitespace to avoid double colons (CSS adds ': ') labelText = labelText.replace(/[:\s]+$/, ''); - questionLabel.textContent = labelText; - summaryItem.appendChild(questionLabel); + questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); + summaryItem.appendChild(questionRow); - // Format answer with title and description parts + // Answer row with A: prefix const formattedAnswer = this.formatAnswerForSummary(question, answer); - const separatorIndex = formattedAnswer.indexOf(' - '); - - if (separatorIndex !== -1) { - // Answer title (bold) - const answerTitle = dom.$('span.chat-question-summary-answer-title'); - answerTitle.textContent = formattedAnswer.substring(0, separatorIndex); - summaryItem.appendChild(answerTitle); - - // Answer description (normal) - const answerDesc = dom.$('span.chat-question-summary-answer-desc'); - answerDesc.textContent = ' - ' + formattedAnswer.substring(separatorIndex + 3); - summaryItem.appendChild(answerDesc); - } else { - // Just the answer value (bold) - const answerValue = dom.$('span.chat-question-summary-answer-title'); - answerValue.textContent = formattedAnswer; - summaryItem.appendChild(answerValue); - } + const answerRow = dom.$('div.chat-question-summary-answer'); + answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); + summaryItem.appendChild(answerRow); summaryContainer.appendChild(summaryItem); } @@ -1296,6 +1449,21 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return renderAsPlaintext(md); } + + + private updateQuestionTabIndicators(): void { + for (const question of this.carousel.questions) { + const indicator = this._questionTabIndicators.get(question.id); + if (!indicator) { + continue; + } + + const hasExplicitAnswer = this._explicitlyAnsweredQuestionIds.has(question.id); + indicator.classList.toggle('codicon-check', hasExplicitAnswer); + indicator.classList.toggle('codicon-circle-filled', !hasExplicitAnswer); + } + } + hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { // does not have same content when it is not skipped and is active and we stop the response if (!this._isSkipped && !this.carousel.isUsed && isResponseVM(element) && element.isComplete) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 00e0f587a8f..06842836e88 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -12,15 +12,19 @@ .interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-container { margin: 0; border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-editor-background); - border-radius: 4px; + background-color: var(--vscode-chat-list-background); + border-radius: var(--vscode-cornerRadius-large); +} + +.interactive-session .interactive-input-part.compact > .chat-question-carousel-widget-container .chat-question-carousel-container { + border-radius: var(--vscode-cornerRadius-small); } /* general questions styling */ .interactive-session .chat-question-carousel-container { margin: 8px 0; border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-large); display: flex; flex-direction: column; overflow: hidden; @@ -40,15 +44,13 @@ .interactive-session .chat-question-carousel-container .chat-question-carousel-content { display: flex; flex-direction: column; - background: var(--vscode-chat-requestBackground); - padding: 8px 16px 10px 16px; + background: var(--vscode-chat-list-background); overflow: hidden; .chat-question-header-row { display: flex; flex-direction: column; - background: var(--vscode-chat-requestBackground); - padding: 0 16px 10px 16px; + background: var(--vscode-chat-list-background); overflow: hidden; .chat-question-title-row { @@ -57,6 +59,8 @@ align-items: center; gap: 8px; min-width: 0; + padding: 4px 8px 4px 16px; + border-bottom: 1px solid var(--vscode-chat-requestBorder); } .chat-question-title { @@ -67,13 +71,6 @@ font-weight: 500; font-size: var(--vscode-chat-font-size-body-s); margin: 0; - padding-top: 4px; - padding-bottom: 4px; - margin-left: -16px; - margin-right: -16px; - padding-left: 16px; - padding-right: 16px; - border-bottom: 1px solid var(--vscode-chat-requestBorder); .rendered-markdown { a { @@ -108,36 +105,35 @@ width: 22px; height: 22px; padding: 0; - border: none; + border: none !important; + box-shadow: none !important; background: transparent !important; - color: var(--vscode-foreground) !important; + color: var(--vscode-icon-foreground) !important; } .monaco-button.chat-question-close:hover:not(.disabled) { background: var(--vscode-toolbar-hoverBackground) !important; } } + } - .chat-question-message { - padding-top: 8px; - font-size: var(--vscode-chat-font-size-body-s); - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.4; + .chat-question-message { + word-wrap: break-word; + overflow-wrap: break-word; + padding: 16px; - .rendered-markdown { - a { - color: var(--vscode-textLink-foreground); - } + .rendered-markdown { + a { + color: var(--vscode-textLink-foreground); + } - a:hover, - a:active { - color: var(--vscode-textLink-activeForeground); - } + a:hover, + a:active { + color: var(--vscode-textLink-activeForeground); + } - p { - margin: 0; - } + p { + margin: 0; } } } @@ -147,41 +143,30 @@ .interactive-session .chat-question-carousel-container .chat-question-input-container { display: flex; flex-direction: column; - margin-top: 4px; + padding-bottom: 12px; min-width: 0; /* some hackiness to get the focus looking right */ - .chat-question-list-item:focus:not(.selected), + .chat-question-list-item:focus, + .chat-question-list-item:focus-visible, .chat-question-list:focus { outline: none; } - .chat-question-list:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; - } - - .chat-question-list:focus-within .chat-question-list-item.selected { - outline-width: 1px; - outline-style: solid; - outline-offset: -1px; - outline-color: var(--vscode-focusBorder); - } - .chat-question-list { display: flex; flex-direction: column; - gap: 3px; outline: none; - padding: 4px 0; + margin: 0 8px; + padding: 0 0 4px 0; .chat-question-list-item { display: flex; align-items: flex-start; - gap: 8px; - padding: 3px 8px; + gap: 12px; + padding: 8px 8px 8px 12px; cursor: pointer; - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-medium); user-select: none; .chat-question-list-indicator { @@ -192,6 +177,7 @@ justify-content: center; flex-shrink: 0; margin-left: auto; + margin-top: 2px; } .chat-question-list-indicator.codicon-check { @@ -204,11 +190,13 @@ flex: 1; word-wrap: break-word; overflow-wrap: break-word; - padding-top: 2px; + display: flex; + flex-direction: column; } .chat-question-list-label-title { - font-weight: 600; + font-weight: 500; + line-height: 1.4; } .chat-question-list-label-desc { @@ -240,11 +228,7 @@ } .chat-question-list-number { - background-color: transparent; color: var(--vscode-list-activeSelectionForeground); - border-color: var(--vscode-list-activeSelectionForeground); - border-bottom-color: var(--vscode-list-activeSelectionForeground); - box-shadow: none; } } @@ -263,11 +247,11 @@ } .chat-question-freeform { - margin-left: 8px; display: flex; flex-direction: row; align-items: center; - gap: 8px; + margin: 0px 8px 0 20px; + gap: 12px; .chat-question-freeform-number { height: fit-content; @@ -283,11 +267,11 @@ width: 100%; min-height: 24px; max-height: 200px; - padding: 3px 8px; - border: 1px solid var(--vscode-input-border, var(--vscode-chat-requestBorder)); + padding: 0; + border: none; background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-medium); resize: none; font-family: var(--vscode-chat-font-family, inherit); font-size: var(--vscode-chat-font-size-body-s); @@ -297,53 +281,90 @@ } .chat-question-freeform-textarea:focus { - outline: 1px solid var(--vscode-focusBorder); - border-color: var(--vscode-focusBorder); + outline: none; } .chat-question-freeform-textarea::placeholder { color: var(--vscode-input-placeholderForeground); } + &:focus-within .chat-question-freeform-number { + color: var(--vscode-list-activeSelectionForeground); + } + } /* todo: change to use keybinding service so we don't have to recreate this */ .chat-question-list-number, .chat-question-freeform-number { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 14px; - padding: 0px 4px; - border-style: solid; - border-width: 1px; - border-radius: 3px; font-size: 11px; - font-weight: normal; - background-color: var(--vscode-keybindingLabel-background); - color: var(--vscode-keybindingLabel-foreground); - border-color: var(--vscode-keybindingLabel-border); - border-bottom-color: var(--vscode-keybindingLabel-bottomBorder); - box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow); + color: var(--vscode-descriptionForeground); flex-shrink: 0; + line-height: 1rem; } } -/* footer with step indicator and nav buttons */ -.interactive-session .chat-question-carousel-container .chat-question-footer-row { +/* tab bar for multi-question carousels */ +.interactive-session .chat-question-carousel-container .chat-question-tab-bar { display: flex; - justify-content: space-between; align-items: center; - padding: 4px 16px; - border-top: 1px solid var(--vscode-chat-requestBorder); - background: var(--vscode-chat-requestBackground); + gap: 2px; + padding: 4px 8px 4px 4px; + border-bottom: 1px solid var(--vscode-chat-requestBorder); + background: var(--vscode-chat-list-background); - .chat-question-step-indicator { - font-size: var(--vscode-chat-font-size-body-s); - color: var(--vscode-descriptionForeground); + .chat-question-tabs { + display: flex; + align-items: center; + gap: 2px; + flex: 1; + min-width: 0; + overflow-x: auto; } - .chat-question-carousel-nav { + .chat-question-tab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 10px 2px 8px; + border-radius: var(--vscode-cornerRadius-medium); + font-size: var(--vscode-chat-font-size-body-s); + cursor: pointer; + font-weight: 500; + white-space: nowrap; + user-select: none; + color: var(--vscode-descriptionForeground); + outline: none; + } + + .chat-question-tab .chat-question-tab-indicator { + font-size: 10px; + line-height: 1; + } + + .chat-question-tab .chat-question-tab-indicator.codicon-circle-filled { + color: var(--vscode-textLink-foreground); + } + + .chat-question-tab.no-icon { + padding: 2px 8px; + } + + .chat-question-tab:hover { + background: var(--vscode-list-hoverBackground); + color: var(--vscode-foreground); + } + + .chat-question-tab.active { + background: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + } + + .chat-question-tab:focus-visible { + outline: none; + } + + .chat-question-tab-controls { display: flex; align-items: center; gap: 4px; @@ -351,34 +372,7 @@ margin-left: auto; } - .chat-question-nav-arrows { - display: flex; - align-items: center; - gap: 4px; - } - - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow { - min-width: 22px; - width: 22px; - height: 22px; - padding: 0; - border: none; - } - - /* Secondary buttons (prev, next) use gray secondary background */ - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev, - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next { - background: var(--vscode-button-secondaryBackground) !important; - color: var(--vscode-button-secondaryForeground) !important; - } - - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev:hover:not(.disabled), - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next:hover:not(.disabled) { - background: var(--vscode-button-secondaryHoverBackground) !important; - } - - /* Dedicated submit button uses primary background */ - .chat-question-carousel-nav .monaco-button.chat-question-submit-button { + .chat-question-tab-controls .monaco-button.chat-question-submit-button { background: var(--vscode-button-background) !important; color: var(--vscode-button-foreground) !important; height: 22px; @@ -386,20 +380,66 @@ padding: 0 8px; } - .chat-question-carousel-nav .monaco-button.chat-question-submit-button:hover:not(.disabled) { + .chat-question-tab-controls .monaco-button.chat-question-submit-button:hover:not(.disabled) { background: var(--vscode-button-hoverBackground) !important; } - /* Close button uses transparent background */ - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close { - background: transparent !important; - color: var(--vscode-foreground) !important; + .chat-question-close-container { + flex-shrink: 0; + + .monaco-button.chat-question-close { + min-width: 22px; + width: 22px; + height: 22px; + padding: 0; + border: none !important; + box-shadow: none !important; + background: transparent !important; + color: var(--vscode-foreground) !important; + } + + .monaco-button.chat-question-close:hover:not(.disabled) { + background: var(--vscode-toolbar-hoverBackground) !important; + } + } +} + +/* footer with submit and cancel buttons */ +.interactive-session .chat-question-carousel-container .chat-question-footer-row { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-top: 1px solid var(--vscode-chat-requestBorder); + background: var(--vscode-chat-list-background); + + .chat-question-submit-hint { + font-size: 11px; + color: var(--vscode-descriptionForeground); } - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close:hover:not(.disabled) { - background: var(--vscode-toolbar-hoverBackground) !important; + .monaco-button.chat-question-submit-button { + background: var(--vscode-button-background) !important; + color: var(--vscode-button-foreground) !important; + height: 22px; + width: auto; + flex: 0 0 auto; + min-width: auto; + padding: 0 8px; } + .monaco-button.chat-question-submit-button:hover:not(.disabled) { + background: var(--vscode-button-hoverBackground) !important; + } + + .monaco-button.chat-question-cancel-button { + height: 22px; + width: auto; + flex: 0 0 auto; + min-width: auto; + padding: 0 8px; + } } /* summary (after finished) */ @@ -407,13 +447,11 @@ display: flex; flex-direction: column; gap: 8px; - padding: 8px; + padding: 16px; .chat-question-summary-item { display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: baseline; + flex-direction: column; gap: 0; font-size: var(--vscode-chat-font-size-body-s); } @@ -424,19 +462,7 @@ overflow-wrap: break-word; } - .chat-question-summary-label::after { - content: ': '; - white-space: pre; - } - - .chat-question-summary-answer-title { - color: var(--vscode-foreground); - font-weight: 600; - word-wrap: break-word; - overflow-wrap: break-word; - } - - .chat-question-summary-answer-desc { + .chat-question-summary-answer { color: var(--vscode-foreground); word-wrap: break-word; overflow-wrap: break-word; @@ -447,4 +473,15 @@ font-style: italic; font-size: var(--vscode-chat-font-size-body-s); } + + .chat-question-summary-empty { + color: var(--vscode-descriptionForeground); + font-size: var(--vscode-chat-font-size-body-s); + padding: 0; + } + + .chat-question-summary-unanswered { + color: var(--vscode-descriptionForeground); + font-style: italic; + } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index bf7dd424e25..5cca23e2761 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -53,6 +53,22 @@ function truncateToLimit(value: string | undefined, limit: number): string | und return value; } +export function formatHeaderForDisplay(header: string): string { + const normalized = header + .trim() + .replace(/[_-]+/g, ' ') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') + .replace(/\s+/g, ' ') + .trim(); + + if (!normalized) { + return header; + } + + return normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase(); +} + export interface IQuestionOption { readonly label: string; readonly description?: string; @@ -195,7 +211,7 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { throw new CancellationError(); } - progress.report({ message: localize('askQuestionsTool.progress', 'Analyzing your answers...') }); + progress.report({ message: localize('askQuestionsTool.progress', 'Reviewing your answers') }); const converted = this.convertCarouselAnswers(questions, answerResult?.answers, idToHeaderMap); const { answeredCount, skippedCount, freeTextCount, recommendedAvailableCount, recommendedSelectedCount } = this.collectMetrics(questions, converted); @@ -300,8 +316,9 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { const internalId = generateUuid(); idToHeaderMap.set(internalId, question.header); - // Truncate header for display only - const displayTitle = truncateToLimit(question.header, HardLimits.header) ?? question.header; + // Format + truncate header for display only; preserve original header for answer correlation + const formattedHeader = formatHeaderForDisplay(question.header); + const displayTitle = truncateToLimit(formattedHeader, HardLimits.header) ?? formattedHeader; return { id: internalId, diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 10045c7dce3..f6520b66350 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -58,9 +58,7 @@ suite('ChatQuestionCarouselPart', () => { createWidget(carousel); assert.ok(widget.domNode.classList.contains('chat-question-carousel-container')); - assert.ok(widget.domNode.querySelector('.chat-question-header-row')); assert.ok(widget.domNode.querySelector('.chat-question-carousel-content')); - assert.ok(widget.domNode.querySelector('.chat-question-carousel-nav')); }); test('renders question title', () => { @@ -125,7 +123,7 @@ suite('ChatQuestionCarouselPart', () => { assert.strictEqual(messageEl?.querySelector('.rendered-markdown'), null, 'plain string message should not use markdown renderer'); }); - test('renders progress indicator correctly', () => { + test('renders tab bar for multi-question carousel', () => { const carousel = createMockCarousel([ { id: 'q1', type: 'text', title: 'Question 1', message: 'Question 1' }, { id: 'q2', type: 'text', title: 'Question 2', message: 'Question 2' }, @@ -133,11 +131,11 @@ suite('ChatQuestionCarouselPart', () => { ]); createWidget(carousel); - // Progress is shown in the step indicator in the footer as "1/3" - const stepIndicator = widget.domNode.querySelector('.chat-question-step-indicator'); - assert.ok(stepIndicator); - assert.ok(stepIndicator?.textContent?.includes('1')); - assert.ok(stepIndicator?.textContent?.includes('3')); + const tabBar = widget.domNode.querySelector('.chat-question-tab-bar'); + assert.ok(tabBar, 'Tab bar should exist for multi-question carousel'); + const tabs = widget.domNode.querySelectorAll('.chat-question-tab'); + // 3 question tabs + 1 review tab + assert.strictEqual(tabs.length, 4, 'Should have 3 question tabs + 1 review tab'); }); }); @@ -271,42 +269,16 @@ suite('ChatQuestionCarouselPart', () => { }); suite('Navigation', () => { - test('previous button is disabled on first question', () => { - const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Question 1' }, - { id: 'q2', type: 'text', title: 'Question 2' } - ]); - createWidget(carousel); - - // Use dedicated class selectors for stability - const prevButton = widget.domNode.querySelector('.chat-question-nav-prev') as HTMLButtonElement; - assert.ok(prevButton, 'Previous button should exist'); - assert.ok(prevButton.classList.contains('disabled') || prevButton.disabled, 'Previous button should be disabled on first question'); - }); - - test('next button stays as arrow and is disabled on last question', () => { + test('single question has no tab bar or submit button', () => { const carousel = createMockCarousel([ { id: 'q1', type: 'text', title: 'Only Question' } ]); createWidget(carousel); - // Use dedicated class selector for stability - const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLButtonElement; - assert.ok(nextButton, 'Next button should exist'); - assert.strictEqual(nextButton.getAttribute('aria-label'), 'Next', 'Next button should preserve Next aria-label on last question'); - assert.ok(nextButton.classList.contains('disabled') || nextButton.disabled, 'Next button should be disabled on last question'); - }); - - test('submit button is shown on last question', () => { - const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Only Question' } - ]); - createWidget(carousel); - - const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; - assert.ok(submitButton, 'Submit button should exist'); - assert.strictEqual(submitButton.getAttribute('aria-label'), 'Submit'); - assert.notStrictEqual(submitButton.style.display, 'none', 'Submit button should be visible on last question'); + const tabBar = widget.domNode.querySelector('.chat-question-tab-bar'); + assert.strictEqual(tabBar, null, 'Tab bar should not exist for single question'); + const submitButton = widget.domNode.querySelector('.chat-question-submit-button'); + assert.strictEqual(submitButton, null, 'Submit button is only in review panel for multi-question'); }); }); @@ -401,13 +373,14 @@ suite('ChatQuestionCarouselPart', () => { suite('Accessibility', () => { test('navigation area has proper role and aria-label', () => { const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Question 1' } + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } ]); createWidget(carousel); - const nav = widget.domNode.querySelector('.chat-question-carousel-nav'); - assert.strictEqual(nav?.getAttribute('role'), 'navigation'); - assert.ok(nav?.getAttribute('aria-label'), 'Navigation should have aria-label'); + const tabList = widget.domNode.querySelector('.chat-question-tabs'); + assert.strictEqual(tabList?.getAttribute('role'), 'tablist'); + assert.ok(tabList?.getAttribute('aria-label'), 'Tab list should have aria-label'); }); test('single select list has proper role and aria-label', () => { @@ -586,19 +559,20 @@ suite('ChatQuestionCarouselPart', () => { ], true); const firstWidget = createWidget(carousel); - const nextButton = firstWidget.domNode.querySelector('.chat-question-nav-next') as HTMLElement | null; - assert.ok(nextButton, 'next button should exist'); - nextButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); + // Click the second tab to navigate + const tabs = firstWidget.domNode.querySelectorAll('.chat-question-tab'); + assert.ok(tabs.length >= 2, 'should have at least 2 tabs'); + (tabs[1] as HTMLElement).click(); + + // Verify navigation happened + assert.strictEqual(tabs[1].getAttribute('aria-selected'), 'true', 'second tab should be selected after click'); firstWidget.dispose(); firstWidget.domNode.remove(); const recreatedWidget = createWidget(carousel); - const stepIndicator = recreatedWidget.domNode.querySelector('.chat-question-step-indicator'); - assert.strictEqual(stepIndicator?.textContent, '2/2', 'should restore the current question index after navigation'); - - const title = recreatedWidget.domNode.querySelector('.chat-question-title'); - assert.ok(title?.textContent?.includes('Question 2'), 'should restore to the second question view'); + const recreatedTabs = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); + assert.strictEqual(recreatedTabs[1]?.getAttribute('aria-selected'), 'true', 'should restore to second tab after recreation'); }); test('retains draft answers and current question after widget recreation', () => { @@ -613,9 +587,9 @@ suite('ChatQuestionCarouselPart', () => { firstInput.value = 'first draft answer'; firstInput.dispatchEvent(new Event('input', { bubbles: true })); - const nextButton = firstWidget.domNode.querySelector('.chat-question-nav-next') as HTMLElement | null; - assert.ok(nextButton, 'next button should exist'); - nextButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); + // Click the second tab to navigate + const tabs = firstWidget.domNode.querySelectorAll('.chat-question-tab'); + (tabs[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); const secondInput = firstWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(secondInput, 'second question input should exist'); @@ -626,16 +600,16 @@ suite('ChatQuestionCarouselPart', () => { firstWidget.domNode.remove(); const recreatedWidget = createWidget(carousel); - const stepIndicator = recreatedWidget.domNode.querySelector('.chat-question-step-indicator'); - assert.strictEqual(stepIndicator?.textContent, '2/2', 'should restore the current question index'); + const recreatedTabs = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); + assert.strictEqual(recreatedTabs[1]?.getAttribute('aria-selected'), 'true', 'should restore the current question index'); const recreatedSecondInput = recreatedWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(recreatedSecondInput, 'recreated second question input should exist'); assert.strictEqual(recreatedSecondInput.value, 'second draft answer', 'should restore draft input for current question'); - const prevButton = recreatedWidget.domNode.querySelector('.chat-question-nav-prev') as HTMLElement | null; - assert.ok(prevButton, 'previous button should exist'); - prevButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); + // Click the first tab to go back + const recreatedTabsAgain = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); + (recreatedTabsAgain[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); const recreatedFirstInput = recreatedWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(recreatedFirstInput, 'recreated first question input should exist'); @@ -655,7 +629,7 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(summary, 'Should show summary container after skip'); const summaryItem = summary?.querySelector('.chat-question-summary-item'); assert.ok(summaryItem, 'Should have summary item for the question'); - const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer-title'); + const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer'); assert.ok(summaryValue?.textContent?.includes('default answer'), 'Summary should show the default answer'); }); @@ -689,7 +663,7 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); assert.ok(summary, 'Should show summary container when isUsed is true'); - const summaryValue = summary?.querySelector('.chat-question-summary-answer-title'); + const summaryValue = summary?.querySelector('.chat-question-summary-answer'); assert.ok(summaryValue?.textContent?.includes('saved answer'), 'Summary should show saved answer from data'); }); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts index f82b6bbe55d..7db5b9b6031 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js'; import { NullLogService } from '../../../../../../../platform/log/common/log.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { AskQuestionsTool, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; +import { AskQuestionsTool, formatHeaderForDisplay, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; class TestableAskQuestionsTool extends AskQuestionsTool { @@ -149,4 +149,20 @@ suite('AskQuestionsTool - convertCarouselAnswers', () => { assert.deepStrictEqual(result.answers['Case'], { selected: [], freeText: 'yes', skipped: false }); }); + + test('formats headers for carousel tab title display', () => { + assert.deepStrictEqual([ + formatHeaderForDisplay('FocusArea'), + formatHeaderForDisplay('UserValue'), + formatHeaderForDisplay('RiskLevel'), + formatHeaderForDisplay('Already Spaced'), + formatHeaderForDisplay('snake_case_header'), + ], [ + 'Focus area', + 'User value', + 'Risk level', + 'Already spaced', + 'Snake case header', + ]); + }); }); From afc120d9d1fe22848c7155e7871a68a192d375c5 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:44:38 -0800 Subject: [PATCH 081/448] Add very basic documentation of built-in extensions --- extensions/CONTRIBUTING.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 extensions/CONTRIBUTING.md diff --git a/extensions/CONTRIBUTING.md b/extensions/CONTRIBUTING.md new file mode 100644 index 00000000000..a134a580808 --- /dev/null +++ b/extensions/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing to Built-In Extensions + +This directory contains built-in extensions that ship with VS Code. + +## Basic Structure + +A typical built-in extension has the following structure: + +- `package.json`: extension manifest. +- `src/`: Main directory for source code +- `tsconfig.json`: primary TypeScript config. This should inherit from `tsconfig.base.json`. +- `esbuild.mts`: Esbuild build script used for production builds. +- `.vscodeignore`: Ignore file list. You can copy this from an existing extension. + +Extensions have the following output structure: + +- `out`: Output directory for development builds +- `dist`: Output directory for production builds. + + +## Browser enabling an extension + +By default extensions will only target desktop. To enable an extension in browsers as well: + +- Add a `"browser"` entry in `package.json` pointing to the browser bundle (for example `"./dist/browser/extension"`). +- Add `tsconfig.browser.json` that typechecks only browser-safe sources. +- Add a `esbuild.browser.mts` file. This should set `platform: 'browser'`. + +Make sure the browser build of the extension only uses browser safe APIs. If an extension needs different behavior between desktop and web, you can create distinct entrypoints for each target: + +- `src/extension.ts`: Desktop entrypoint. +- `src/extension.browser.ts`: Browser entrypoint. Make sure `esbuild.browser.mts` builds this and that `tsconfig.browser.json` targets it. From 5d40b987bf66cc0ba56558e76b0040f336a143e1 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:54:59 -0800 Subject: [PATCH 082/448] Update extensions/CONTRIBUTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extensions/CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/CONTRIBUTING.md b/extensions/CONTRIBUTING.md index a134a580808..62b2a4ba690 100644 --- a/extensions/CONTRIBUTING.md +++ b/extensions/CONTRIBUTING.md @@ -4,15 +4,15 @@ This directory contains built-in extensions that ship with VS Code. ## Basic Structure -A typical built-in extension has the following structure: +A typical TypeScript-based built-in extension has the following structure: - `package.json`: extension manifest. -- `src/`: Main directory for source code +- `src/`: Main directory for TypeScript source code. - `tsconfig.json`: primary TypeScript config. This should inherit from `tsconfig.base.json`. - `esbuild.mts`: Esbuild build script used for production builds. - `.vscodeignore`: Ignore file list. You can copy this from an existing extension. -Extensions have the following output structure: +TypeScript-based extensions have the following output structure: - `out`: Output directory for development builds - `dist`: Output directory for production builds. From 92851df52f11c34cc3528e03425476527862fd0e Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:55:22 -0800 Subject: [PATCH 083/448] Update extensions/CONTRIBUTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extensions/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/CONTRIBUTING.md b/extensions/CONTRIBUTING.md index 62b2a4ba690..5a197670f15 100644 --- a/extensions/CONTRIBUTING.md +++ b/extensions/CONTRIBUTING.md @@ -18,7 +18,7 @@ TypeScript-based extensions have the following output structure: - `dist`: Output directory for production builds. -## Browser enabling an extension +## Enabling an Extension in the Browser By default extensions will only target desktop. To enable an extension in browsers as well: From ea0ea233c34f3760c12ded1a30a819d8f7069bb3 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:56:01 -0800 Subject: [PATCH 084/448] Update extensions/CONTRIBUTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extensions/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/CONTRIBUTING.md b/extensions/CONTRIBUTING.md index 5a197670f15..5a2f4c83012 100644 --- a/extensions/CONTRIBUTING.md +++ b/extensions/CONTRIBUTING.md @@ -9,7 +9,7 @@ A typical TypeScript-based built-in extension has the following structure: - `package.json`: extension manifest. - `src/`: Main directory for TypeScript source code. - `tsconfig.json`: primary TypeScript config. This should inherit from `tsconfig.base.json`. -- `esbuild.mts`: Esbuild build script used for production builds. +- `esbuild.mts`: esbuild build script used for production builds. - `.vscodeignore`: Ignore file list. You can copy this from an existing extension. TypeScript-based extensions have the following output structure: From a7ca6e78426a4f7a70680fd538935434ba8ec72e Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:56:09 -0800 Subject: [PATCH 085/448] Update extensions/CONTRIBUTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extensions/CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/CONTRIBUTING.md b/extensions/CONTRIBUTING.md index 5a2f4c83012..cfaf6b0ca8d 100644 --- a/extensions/CONTRIBUTING.md +++ b/extensions/CONTRIBUTING.md @@ -24,9 +24,9 @@ By default extensions will only target desktop. To enable an extension in browse - Add a `"browser"` entry in `package.json` pointing to the browser bundle (for example `"./dist/browser/extension"`). - Add `tsconfig.browser.json` that typechecks only browser-safe sources. -- Add a `esbuild.browser.mts` file. This should set `platform: 'browser'`. +- Add an `esbuild.browser.mts` file. This should set `platform: 'browser'`. -Make sure the browser build of the extension only uses browser safe APIs. If an extension needs different behavior between desktop and web, you can create distinct entrypoints for each target: +Make sure the browser build of the extension only uses browser-safe APIs. If an extension needs different behavior between desktop and web, you can create distinct entrypoints for each target: - `src/extension.ts`: Desktop entrypoint. - `src/extension.browser.ts`: Browser entrypoint. Make sure `esbuild.browser.mts` builds this and that `tsconfig.browser.json` targets it. From 37645e695f1d0e93cf1bb17a550a9e1fdf9d4867 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 13:18:49 -0800 Subject: [PATCH 086/448] chat: add approval management UI to tool picker (#299031) Adds support for managing tool confirmation preferences directly from the tool picker. This allows users to approve tools and external paths at the workspace level, reducing friction when using tools that require confirmation. - Adds 'Manage Approval' button to tools that support confirmation - Integrates ILanguageModelToolsConfirmationService with tool picker - Adds workspace-level allowlist persistence for external paths - Extends ActionableButton type to support keepOpen behavior - Implements workspace folder selection and allowlist management - Adds ObservableMemento for persistent storage of approved paths Fixes https://github.com/microsoft/vscode-internalbacklog/issues/6805 --- .../quickinput/browser/tree/quickTree.ts | 5 + .../platform/quickinput/common/quickInput.ts | 6 + .../chat/browser/actions/chatToolPicker.ts | 40 ++++- .../languageModelToolsConfirmationService.ts | 41 ++++- .../chatExternalPathConfirmation.ts | 159 ++++++++++++++++-- .../languageModelToolsConfirmationService.ts | 13 +- .../electron-browser/builtInTools/tools.ts | 17 ++ ...ckLanguageModelToolsConfirmationService.ts | 5 +- 8 files changed, 263 insertions(+), 23 deletions(-) diff --git a/src/vs/platform/quickinput/browser/tree/quickTree.ts b/src/vs/platform/quickinput/browser/tree/quickTree.ts index e506021a058..3c9a6146948 100644 --- a/src/vs/platform/quickinput/browser/tree/quickTree.ts +++ b/src/vs/platform/quickinput/browser/tree/quickTree.ts @@ -104,6 +104,11 @@ export class QuickTree extends QuickInput implements I this.ui.inputBox.setFocus(); } + reveal(element: T): void { + this.ui.tree.tree.reveal(element); + this.ui.tree.tree.setFocus([element]); + } + override show() { if (!this.visible) { const visibilities: Visibilities = { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 9426be48e2f..04d91e66aa4 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -1172,6 +1172,12 @@ export interface IQuickTree extends IQuickInput { */ focusOnInput(): void; + /** + * Reveals and focuses a specific item in the tree. + * @param element The item to reveal and focus. + */ + reveal(element: T): void; + /** * Focus a particular item in the list. Used internally for keyboard navigation. * @param focus The focus behavior. diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts index 91e60f599de..7d8e0512eaf 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts @@ -22,6 +22,7 @@ import { IMcpRegistry } from '../../../mcp/common/mcpRegistryTypes.js'; import { IMcpServer, IMcpService, IMcpWorkbenchService, McpConnectionState, McpServerCacheState, McpServerEditorTab } from '../../../mcp/common/mcpTypes.js'; import { startServerAndWaitForLiveTools } from '../../../mcp/common/mcpTypesUtils.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; +import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; import { ILanguageModelToolsService, IToolData, IToolSet, ToolDataSource, ToolSet } from '../../common/tools/languageModelToolsService.js'; import { ConfigureToolSets } from '../tools/toolSetsContribution.js'; @@ -31,7 +32,7 @@ const enum BucketOrdinal { User, BuiltIn, Mcp, Extension } type BucketPick = IQuickPickItem & { picked: boolean; ordinal: BucketOrdinal; status?: string; toolset?: ToolSet; children: (ToolPick | ToolSetPick)[] }; type ToolSetPick = IQuickPickItem & { picked: boolean; toolset: ToolSet; parent: BucketPick }; type ToolPick = IQuickPickItem & { picked: boolean; tool: IToolData; parent: BucketPick }; -type ActionableButton = IQuickInputButton & { action: () => void }; +type ActionableButton = IQuickInputButton & { action: () => void; keepOpen?: boolean }; // New QuickTree types for tree-based implementation @@ -77,6 +78,7 @@ interface IToolSetTreeItem extends IToolTreeItem { interface IToolTreeItemData extends IToolTreeItem { readonly itemType: 'tool'; readonly tool: IToolData; + buttons?: ActionableButton[]; checked: boolean; } @@ -205,6 +207,7 @@ export async function showToolsPicker( const editorService = accessor.get(IEditorService); const mcpWorkbenchService = accessor.get(IMcpWorkbenchService); const toolsService = accessor.get(ILanguageModelToolsService); + const confirmationService = accessor.get(ILanguageModelToolsConfirmationService); const telemetryService = accessor.get(ITelemetryService); const mcpServerByTool = new Map(); @@ -451,6 +454,38 @@ export async function showToolsPicker( } } } + // Add approval management buttons to tool items that support confirmation + for (const bucket of sortedBuckets) { + const isMcpBucket = bucket.ordinal === BucketOrdinal.Mcp; + const addConfirmationButton = (toolItem: IToolTreeItemData) => { + if (!confirmationService.toolCanManageConfirmation(toolItem.tool)) { + return; + } + const tool = toolItem.tool; + const manageTools = isMcpBucket ? bucket.children.flatMap(c => isToolTreeItem(c) ? [c.tool] : isToolSetTreeItem(c) && c.children ? c.children.filter(isToolTreeItem).map(gc => gc.tool) : []) : [tool]; + const buttons: ActionableButton[] = toolItem.buttons ? [...toolItem.buttons] : []; + buttons.push({ + iconClass: ThemeIcon.asClassName(Codicon.pass), + tooltip: localize('manageToolApproval', "Manage Approval"), + keepOpen: true, + action: () => confirmationService.manageConfirmationPreferences(manageTools, { focusToolId: tool.id }) + }); + toolItem.buttons = buttons; + }; + + for (const child of bucket.children) { + if (isToolTreeItem(child)) { + addConfirmationButton(child); + } else if (isToolSetTreeItem(child) && child.children) { + for (const grandchild of child.children) { + if (isToolTreeItem(grandchild)) { + addConfirmationButton(grandchild); + } + } + } + } + } + if (treeItems.length === 0) { treePicker.placeholder = localize('noTools', "Add tools to chat"); } else { @@ -474,7 +509,8 @@ export async function showToolsPicker( // Handle button triggers store.add(treePicker.onDidTriggerItemButton(e => { if (e.button && typeof (e.button as ActionableButton).action === 'function') { - (e.button as ActionableButton).action(); + const actionableButton = e.button as ActionableButton; + actionableButton.action(); store.dispose(); } })); diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts index 50a3495c4f8..3661f5a27c3 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts @@ -424,7 +424,15 @@ export class LanguageModelToolsConfirmationService extends Disposable implements }; } - manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void { + toolCanManageConfirmation(tool: IToolData): boolean { + return !!tool.canRequestPreApproval + || !!tool.canRequestPostApproval + || this._contributions.has(tool.id) + || !!this._preExecutionToolConfirmStore.checkAutoConfirmation(tool.id) + || !!this._postExecutionToolConfirmStore.checkAutoConfirmation(tool.id); + } + + manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session'; focusToolId?: string }): void { interface IToolTreeItem extends IQuickTreeItem { type: 'tool' | 'server' | 'tool-pre' | 'tool-post' | 'server-pre' | 'server-post' | 'manage'; toolId?: string; @@ -690,7 +698,7 @@ export class LanguageModelToolsConfirmationService extends Disposable implements description, checked, pickable, - collapsed: true, + collapsed: tools.length > 1, children: toolChildren.length > 0 ? toolChildren : undefined }); } @@ -773,12 +781,12 @@ export class LanguageModelToolsConfirmationService extends Disposable implements } })); - disposables.add(quickTree.onDidAccept(() => { - for (const item of quickTree.activeItems) { - if (item.type === 'manage') { - (item as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidOpen?.(); - quickTree.hide(); - } + disposables.add(quickTree.onDidAccept(async () => { + const manageItem = quickTree.activeItems.find(i => i.type === 'manage'); + if (manageItem) { + quickTree.hide(); + await (manageItem as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidOpen?.(); + this.manageConfirmationPreferences(tools, options); } })); @@ -787,6 +795,23 @@ export class LanguageModelToolsConfirmationService extends Disposable implements })); quickTree.show(); + + // If a focus tool was specified, expand its parent and set it as active. + // Must happen after show() since the tree data is applied via autorun on visibility. + if (options?.focusToolId) { + const focusToolId = options.focusToolId; + for (const serverItem of quickTree.itemTree) { + const serverItemTyped = serverItem as IToolTreeItem; + if (serverItemTyped.children) { + const toolItem = (serverItemTyped.children as IToolTreeItem[]).find(c => c.type === 'tool' && c.toolId === focusToolId); + if (toolItem) { + quickTree.expand(serverItem); + quickTree.reveal(toolItem); + break; + } + } + } + } } public resetToolAutoConfirmation(): void { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts index 9babb75b5bd..ee75a3bc056 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts @@ -3,17 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; import { dirname, extUriBiasedIgnorePathCase } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { ObservableMemento, observableMemento } from '../../../../../../platform/observable/common/observableMemento.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { ConfirmedReason, ToolConfirmKind } from '../../chatService/chatService.js'; import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, + ILanguageModelToolConfirmationContributionQuickTreeItem, ILanguageModelToolConfirmationRef } from '../languageModelToolsConfirmationService.js'; +const workspaceAllowlistMemento = observableMemento({ + key: 'chat.externalPath.workspaceAllowlist', + defaultValue: [], + toStorage: value => JSON.stringify(value), + fromStorage: value => { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + }, +}); + export interface IExternalPathInfo { path: string; isDirectory: boolean; @@ -24,26 +39,59 @@ export interface IExternalPathInfo { * accessing paths outside the workspace, with an option to allow all access * from a containing folder for the current chat session. */ -export class ChatExternalPathConfirmationContribution implements ILanguageModelToolConfirmationContribution { +export class ChatExternalPathConfirmationContribution implements ILanguageModelToolConfirmationContribution, IDisposable { readonly canUseDefaultApprovals = false; private readonly _sessionFolderAllowlist = new ResourceMap(); /** Cache of path URI -> resolved git root URI (or null if not in a repo) */ private readonly _gitRootCache = new ResourceMap(); + private readonly _workspaceAllowlist?: ObservableMemento; constructor( private readonly _getPathInfo: (ref: ILanguageModelToolConfirmationRef) => IExternalPathInfo | undefined, + private readonly _labelService: ILabelService, private readonly _findGitRoot?: (pathUri: URI) => Promise, - ) { } + storageService?: IStorageService, + private readonly _pickFolder?: () => Promise, + ) { + if (storageService) { + this._workspaceAllowlist = workspaceAllowlistMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE, storageService); + } + } + + dispose(): void { + this._workspaceAllowlist?.dispose(); + } + + private _getWorkspaceFolders(): ResourceSet { + if (!this._workspaceAllowlist) { + return new ResourceSet(); + } + const set = new ResourceSet(); + for (const s of this._workspaceAllowlist.get()) { + try { + set.add(URI.parse(s)); + } catch { + // ignore malformed URIs + } + } + return set; + } + + private _setWorkspaceFolders(folders: ResourceSet): void { + if (!this._workspaceAllowlist) { + return; + } + const uriStrings: string[] = []; + for (const uri of folders) { + uriStrings.push(uri.toString()); + } + this._workspaceAllowlist.set(uriStrings, undefined); + } getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { const pathInfo = this._getPathInfo(ref); - if (!pathInfo || !ref.chatSessionResource) { - return undefined; - } - - const allowedFolders = this._sessionFolderAllowlist.get(ref.chatSessionResource); - if (!allowedFolders || allowedFolders.size === 0) { + if (!pathInfo) { return undefined; } @@ -55,13 +103,26 @@ export class ChatExternalPathConfirmationContribution implements ILanguageModelT return undefined; } - // Check if path is under any allowed folder - for (const folderUri of allowedFolders) { + // Check workspace-level allowlist + const workspaceFolders = this._getWorkspaceFolders(); + for (const folderUri of workspaceFolders) { if (extUriBiasedIgnorePathCase.isEqualOrParent(pathUri, folderUri)) { return { type: ToolConfirmKind.UserAction }; } } + // Check session-level allowlist + if (ref.chatSessionResource) { + const sessionFolders = this._sessionFolderAllowlist.get(ref.chatSessionResource); + if (sessionFolders) { + for (const folderUri of sessionFolders) { + if (extUriBiasedIgnorePathCase.isEqualOrParent(pathUri, folderUri)) { + return { type: ToolConfirmKind.UserAction }; + } + } + } + } + return undefined; } @@ -149,4 +210,82 @@ export class ChatExternalPathConfirmationContribution implements ILanguageModelT return actions; } + + getManageActions(): ILanguageModelToolConfirmationContributionQuickTreeItem[] { + const items: ILanguageModelToolConfirmationContributionQuickTreeItem[] = []; + + // Workspace-level entries (persisted) + const workspaceFolders = this._getWorkspaceFolders(); + for (const folderUri of workspaceFolders) { + items.push({ + label: this._labelService.getUriLabel(folderUri), + description: localize('workspaceScope', "Workspace"), + checked: true, + onDidChangeChecked: (checked) => { + if (!checked) { + workspaceFolders.delete(folderUri); + this._setWorkspaceFolders(workspaceFolders); + } else { + workspaceFolders.add(folderUri); + this._setWorkspaceFolders(workspaceFolders); + } + }, + }); + } + + // Session-level entries (ephemeral) + const allSessionFolders = new ResourceSet(); + for (const [, folders] of this._sessionFolderAllowlist) { + for (const folder of folders) { + allSessionFolders.add(folder); + } + } + for (const folderUri of allSessionFolders) { + const wasInSessions = [...this._sessionFolderAllowlist].filter(([, folders]) => folders.has(folderUri)); + items.push({ + label: this._labelService.getUriLabel(folderUri), + description: localize('sessionScope', "Session"), + checked: true, + onDidChangeChecked: (checked) => { + if (!checked) { + for (const [, folders] of wasInSessions) { + folders.delete(folderUri); + } + } else { + for (const [, folders] of wasInSessions) { + folders.add(folderUri); + } + } + }, + }); + } + + // "Add Path..." option to add a new workspace-level folder + if (this._pickFolder) { + const pickFolder = this._pickFolder; + items.push({ + pickable: false, + label: localize('addPath', "Add Path..."), + description: localize('addPathDescription', "Allow a folder in this workspace"), + onDidOpen: async () => { + const uri = await pickFolder(); + if (uri) { + const folders = this._getWorkspaceFolders(); + folders.add(uri); + this._setWorkspaceFolders(folders); + } + } + }); + } + + return items; + } + + reset(): void { + this._sessionFolderAllowlist.clear(); + this._gitRootCache.clear(); + if (this._workspaceAllowlist) { + this._workspaceAllowlist.set([], undefined); + } + } } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts index d77bd07c0c0..2e3c66261b8 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts @@ -42,7 +42,7 @@ export interface ILanguageModelToolConfirmationActionProducer { export interface ILanguageModelToolConfirmationContributionQuickTreeItem extends IQuickTreeItem { onDidTriggerItemButton?(button: IQuickInputButton): void; onDidChangeChecked?(checked: boolean): void; - onDidOpen?(): void; + onDidOpen?(): void | Promise; } /** @@ -85,7 +85,7 @@ export interface ILanguageModelToolsConfirmationService extends ILanguageModelTo readonly _serviceBrand: undefined; /** Opens an IQuickTree to let the user manage their preferences. */ - manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void; + manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session'; focusToolId?: string }): void; /** * Registers a contribution that provides more specific confirmation logic @@ -93,6 +93,15 @@ export interface ILanguageModelToolsConfirmationService extends ILanguageModelTo */ registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable; + /** + * Returns true if the tool has confirmation that can be managed, either + * because it has {@link IToolData.canRequestPreApproval} or + * {@link IToolData.canRequestPostApproval} set, because a + * {@link ILanguageModelToolConfirmationContribution} is registered for it, + * or because it has stored auto-confirmation settings. + */ + toolCanManageConfirmation(tool: IToolData): boolean; + /** Resets all tool and server confirmation preferences */ resetToolAutoConfirmation(): void; } diff --git a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts index 3af7e138aeb..1d1d822cc30 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts @@ -6,8 +6,11 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { dirname, extUriBiasedIgnorePathCase } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ChatExternalPathConfirmationContribution } from '../../common/tools/builtinTools/chatExternalPathConfirmation.js'; import { ChatUrlFetchingConfirmationContribution } from '../../common/tools/builtinTools/chatUrlFetchingConfirmation.js'; @@ -25,6 +28,9 @@ export class NativeBuiltinToolsContribution extends Disposable implements IWorkb @IInstantiationService instantiationService: IInstantiationService, @ILanguageModelToolsConfirmationService confirmationService: ILanguageModelToolsConfirmationService, @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, + @IFileDialogService fileDialogService: IFileDialogService, + @ILabelService labelService: ILabelService, ) { super(); @@ -53,6 +59,7 @@ export class NativeBuiltinToolsContribution extends Disposable implements IWorkb } return undefined; }, + labelService, async (pathUri: URI) => { // Walk up from the path looking for a .git folder to find the repository root let dir = dirname(pathUri); @@ -71,8 +78,18 @@ export class NativeBuiltinToolsContribution extends Disposable implements IWorkb dir = parent; } return undefined; + }, + storageService, + async () => { + const result = await fileDialogService.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + }); + return result?.[0]; } ); + this._register(externalPathConfirmation); this._register(confirmationService.registerConfirmationContribution( 'copilot_readFile', diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts index e8d3da161cb..d3ce0affbf8 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts @@ -9,12 +9,15 @@ import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationCo import { IToolData } from '../../../common/tools/languageModelToolsService.js'; export class MockLanguageModelToolsConfirmationService implements ILanguageModelToolsConfirmationService { - manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void { + manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session'; focusToolId?: string }): void { throw new Error('Method not implemented.'); } registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable { throw new Error('Method not implemented.'); } + toolCanManageConfirmation(): boolean { + return false; + } resetToolAutoConfirmation(): void { } From d481546f4c48b6592932b8e5808a4eed42d8823b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 13:19:17 -0800 Subject: [PATCH 087/448] dialogs: prevent custom dialogs from overflowing screen vertically (#299048) Adds max-height: 90vh to the dialog box to prevent it from exceeding the viewport height, matching the existing max-width: 90vw constraint. The message content area now scrolls when it exceeds available space: - Added align-self: stretch to .dialog-message-container in horizontal layout so it fills the row height and triggers overflow-y scrolling - Added min-height: 0 to .dialog-message-container in vertical layout to allow flex shrinking and overflow-y scrolling - Changed .dialog-message-container overflow from 'hidden' to 'overflow-y: auto; overflow-x: hidden' to enable vertical scrolling Buttons, toolbar, and footer remain visible. Only message content scrolls. Fixes https://github.com/microsoft/vscode/issues/296528 (Commit message generated by Copilot) --- src/vs/base/browser/ui/dialog/dialog.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 0dd289c9f9e..d1d37dcff67 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -27,6 +27,7 @@ width: min-content; min-width: 500px; max-width: 90vw; + max-height: 90vh; min-height: 75px; padding: 10px; transform: translate3d(0px, 0px, 0px); @@ -55,12 +56,18 @@ flex-grow: 1; align-items: center; padding: 0 10px; + min-height: 0; /* allow flex item to shrink below content size */ + overflow: hidden; } .monaco-dialog-box.align-vertical .dialog-message-row { flex-direction: column; } +.monaco-dialog-box.align-vertical .dialog-message-row .dialog-message-container { + min-height: 0; /* allow flex item to shrink below content size in column layout */ +} + .monaco-dialog-box .dialog-message-row > .dialog-icon.codicon { flex: 0 0 48px; height: 48px; @@ -77,12 +84,17 @@ align-self: baseline; } +.monaco-dialog-box:not(.align-vertical) .dialog-message-row .dialog-message-container { + align-self: stretch; /* fill row height so overflow-y scrolling works */ +} + /** Dialog: Message/Footer Container */ .monaco-dialog-box .dialog-message-row .dialog-message-container, .monaco-dialog-box .dialog-footer-row { display: flex; flex-direction: column; - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; text-overflow: ellipsis; user-select: text; -webkit-user-select: text; From ea50b9a9c2300b37d80900423d71359df90151d8 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 13:40:02 -0800 Subject: [PATCH 088/448] chat: support relative paths in plugin locations (#299059) * chat: support relative paths in plugin locations - Changes chat.pluginLocations to support home-relative (~/) and workspace-relative (./) paths instead of absolute paths - Adds path normalization and validation in agentPluginServiceImpl - Updates constants and contribution for plugin path handling - Ensures settings are shareable across machines by avoiding absolute paths Fixes https://github.com/microsoft/vscode/issues/297365 (Commit message generated by Copilot) * comment --- .../contrib/chat/browser/chat.contribution.ts | 12 ++++-- .../contrib/chat/common/constants.ts | 2 +- .../common/plugins/agentPluginServiceImpl.ts | 42 ++++++++++++------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4fd8b274b75..954b313b84a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -653,12 +653,11 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['preview'], }, - [ChatConfiguration.PluginPaths]: { + [ChatConfiguration.PluginLocations]: { type: 'object', additionalProperties: { type: 'boolean' }, restricted: true, - markdownDescription: nls.localize('chat.plugins.paths', "Plugin directories to discover. Each key is a path that points directly to a plugin folder, and the value enables (`true`) or disables (`false`) it. Paths can be absolute or relative to the workspace root."), - default: {}, + markdownDescription: nls.localize('chat.pluginLocations', "Plugin directories to discover. Each key is a path that points directly to a plugin folder, and the value enables (`true`) or disables (`false`) it. Paths can be absolute, relative to the workspace root, or start with `~/` for the user's home directory."), scope: ConfigurationScope.MACHINE, tags: ['experimental'], }, @@ -1323,6 +1322,13 @@ Registry.as(Extensions.ConfigurationMigration). return []; } }, + { + key: 'chat.plugins.paths', + migrateFn: (value: unknown, _accessor) => ([ + ['chat.plugins.paths', { value: undefined }], + [ChatConfiguration.PluginLocations, { value }] + ]) + }, ]); class ChatResolverContribution extends Disposable { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 52fe2267506..9ed80f0451e 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -11,7 +11,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey export enum ChatConfiguration { AIDisabled = 'chat.disableAIFeatures', PluginsEnabled = 'chat.plugins.enabled', - PluginPaths = 'chat.plugins.paths', + PluginLocations = 'chat.pluginLocations', PluginMarketplaces = 'chat.plugins.marketplaces', AgentEnabled = 'chat.agent.enabled', PlanAgentDefaultModel = 'chat.planAgent.defaultModel', diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index fa2ed06dbc4..bfd8c768e5e 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -5,6 +5,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; +import { untildify } from '../../../../../base/common/labels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { cloneAndChange } from '../../../../../base/common/objects.js'; @@ -699,7 +700,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery { - private readonly _pluginPathsConfig: IObservable>; + private readonly _pluginLocationsConfig: IObservable>; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -711,13 +712,13 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery @IInstantiationService instantiationService: IInstantiationService, ) { super(fileService, pathService, logService, instantiationService); - this._pluginPathsConfig = observableConfigValue>(ChatConfiguration.PluginPaths, {}, _configurationService); + this._pluginLocationsConfig = observableConfigValue>(ChatConfiguration.PluginLocations, {}, _configurationService); } public override start(): void { const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); this._register(autorun(reader => { - this._pluginPathsConfig.read(reader); + this._pluginLocationsConfig.read(reader); scheduler.schedule(); })); scheduler.schedule(); @@ -725,14 +726,15 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery protected override async _discoverPluginSources(): Promise { const sources: IPluginSource[] = []; - const config = this._pluginPathsConfig.get(); + const config = this._pluginLocationsConfig.get(); + const userHome = await this._getUserHome(); for (const [path, enabled] of Object.entries(config)) { if (!path.trim()) { continue; } - const resources = this._resolvePluginPath(path.trim()); + const resources = this._resolvePluginPath(path.trim(), userHome); for (const resource of resources) { let stat; try { @@ -762,11 +764,23 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery return sources; } + private async _getUserHome(): Promise { + const userHome = await this._pathService.userHome(); + return userHome.scheme === 'file' ? userHome.fsPath : userHome.path; + } + /** - * Resolves a plugin path to one or more resource URIs. Absolute paths are - * used directly; relative paths are resolved against each workspace folder. + * Resolves a plugin path to one or more resource URIs. Supports: + * - Absolute paths (used directly) + * - Tilde paths (expanded to user home directory) + * - Relative paths (resolved against each workspace folder) */ - private _resolvePluginPath(path: string): URI[] { + private _resolvePluginPath(path: string, userHome: string): URI[] { + if (path.startsWith('~')) { + path = untildify(path, userHome); + } + + // Handle absolute paths if (win32.isAbsolute(path) || posix.isAbsolute(path)) { return [URI.file(path)]; } @@ -781,7 +795,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery * writing to the most specific config target where the key is defined. */ private _updatePluginPathEnabled(configKey: string, value: boolean): void { - const inspected = this._configurationService.inspect>(ChatConfiguration.PluginPaths); + const inspected = this._configurationService.inspect>(ChatConfiguration.PluginLocations); // Walk from most specific to least specific to find where this key is defined const targets = [ @@ -797,7 +811,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery const mapping = getConfigValueInTarget(inspected, target); if (mapping && Object.prototype.hasOwnProperty.call(mapping, configKey)) { this._configurationService.updateValue( - ChatConfiguration.PluginPaths, + ChatConfiguration.PluginLocations, { ...mapping, [configKey]: value }, target, ); @@ -808,18 +822,18 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery // Key not found in any target; write to USER_LOCAL as default const current = getConfigValueInTarget(inspected, ConfigurationTarget.USER_LOCAL) ?? {}; this._configurationService.updateValue( - ChatConfiguration.PluginPaths, + ChatConfiguration.PluginLocations, { ...current, [configKey]: value }, ConfigurationTarget.USER_LOCAL, ); } /** - * Removes a plugin path from `chat.plugins.paths` in the most specific + * Removes a plugin path from `chat.pluginLocations` in the most specific * config target where the key is defined. */ private _removePluginPath(configKey: string): void { - const inspected = this._configurationService.inspect>(ChatConfiguration.PluginPaths); + const inspected = this._configurationService.inspect>(ChatConfiguration.PluginLocations); const targets = [ ConfigurationTarget.WORKSPACE_FOLDER, @@ -836,7 +850,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery const updated = { ...mapping }; delete updated[configKey]; this._configurationService.updateValue( - ChatConfiguration.PluginPaths, + ChatConfiguration.PluginLocations, updated, target, ); From 66386a37bd9dac29b874d01e41b7eff6707e4cca Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:46:07 -0800 Subject: [PATCH 089/448] fix chat-input-container padding (#299054) --- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index ff44e93ef7d..9484c92e09a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -814,7 +814,7 @@ have to be updated for changes to the rules above, or to support more deeply nes background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); border-radius: var(--vscode-cornerRadius-large); - padding: 0 0px 6px 6px; + padding: 0 6px 6px 6px; /* top padding is inside the editor widget */ width: 100%; position: relative; From 560122b0998497485c9a08cd03d13a0d03f2a62a Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:47:55 -0800 Subject: [PATCH 090/448] =?UTF-8?q?sessions:=20add=20"github.copilot.chat.?= =?UTF-8?q?githubMcpServer.enabled"=20to=20config=E2=80=A6=20(#299030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sessions: add "github.copilot.chat.githubMcpServer.enabled" to config override --- .../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 26e300b0529..25c9f5cb914 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -15,6 +15,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'chat.implicitContext.suggestedContext': false, 'chat.implicitContext.enabled': { 'panel': 'never' }, 'chat.tools.terminal.enableAutoApprove': true, + 'github.copilot.chat.githubMcpServer.enabled': true, 'breadcrumbs.enabled': false, From 625bdaafde89c15ff99cb4d0fbdd08516a1a1528 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 13:54:16 -0800 Subject: [PATCH 091/448] Fix MCP tool validation warning for schemas without properties (#299035) Per the MCP spec, the 'properties' field in tool inputSchema is optional, with 'type' being the only required field. However, JSON Schema Draft 7 validation requires 'properties' for object types, causing spurious warnings like 'Tool foo failed validation: schema must have a properties object'. Fix by normalizing the inputSchema to include an empty properties object when not present. Fixes #251723 --- src/vs/workbench/contrib/mcp/common/mcpServer.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 143f856c0f4..3a1f490eb07 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -904,6 +904,13 @@ export class McpServer extends Disposable implements IMcpServer { tool.name = tool.name.replace(toolInvalidCharRe, '_'); } + // Per MCP spec, properties is optional. But JSON Schema Draft 7 requires + // it for object types. Normalize the schema to include an empty properties + // object if not present. https://github.com/microsoft/vscode/issues/251723 + if (tool.inputSchema && !tool.inputSchema.properties) { + tool.inputSchema = { ...tool.inputSchema, properties: {} }; + } + type JsonDiagnostic = { message: string; range: { line: number; character: number }[] }; let diagnostics: JsonDiagnostic[] = []; From dc2091f3968baecb34cbfdb101100d8d2053cc0d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 14:01:40 -0800 Subject: [PATCH 092/448] chat: add manage action gear icon to installed agent plugins (#299052) * chat: add manage action gear icon to installed agent plugins Adds a ManagePluginAction that displays a gear icon in the plugin list for installed plugins. Clicking the gear icon shows a context menu with management options (enable/disable, open folder, open README, uninstall), making it consistent with how extensions and MCP servers display their management UI. - Adds ManagePluginAction class that provides dropdown menu with management actions for installed plugins - Adds ManagePluginActionViewItem to render the action as a clickable gear icon with context menu - Updates AgentPluginRenderer to include the gear icon in the action bar for installed plugins - Context menu displays enable/disable, open folder, open README, and uninstall options Fixes https://github.com/microsoft/vscode/issues/298461 (Commit message generated by Copilot) * chat: clean up manage plugin action to follow MCP pattern - Extract shared getInstalledPluginContextMenuActionGroups() to deduplicate action construction between gear menu and context menu - Restructure ManagePluginAction with createActionViewItem() pattern matching MCP/extensions DropDownAction approach - Add onHide disposal for context menu actions - Remove redundant getDomNodePagePosition import --- .../contrib/chat/browser/agentPluginsView.ts | 101 +++++++++++++++--- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts index 21c59e68f76..129cbad928f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts @@ -5,6 +5,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { IPagedRenderer } from '../../../../base/browser/ui/list/listPaging.js'; import { Action, IAction, Separator } from '../../../../base/common/actions.js'; @@ -12,7 +13,8 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { autorun } from '../../../../base/common/observable.js'; import { IPagedModel, PagedModel } from '../../../../base/common/paging.js'; import { basename, dirname, joinPath } from '../../../../base/common/resources.js'; @@ -38,6 +40,7 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewDescriptorService, IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js'; import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js'; import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; +import { manageExtensionIcon } from '../../extensions/browser/extensionsIcons.js'; import { AbstractExtensionsListView } from '../../extensions/browser/extensionsViews.js'; import { DefaultViewsContext, extensionsFilterSubMenu, IExtensionsWorkbenchService, SearchAgentPluginsContext } from '../../extensions/common/extensions.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; @@ -180,6 +183,71 @@ class OpenPluginReadmeAction extends Action { } } +function getInstalledPluginContextMenuActionGroups(plugin: IAgentPlugin, instantiationService: IInstantiationService): IAction[][] { + const groups: IAction[][] = []; + if (plugin.enabled.get()) { + groups.push([instantiationService.createInstance(DisablePluginAction, plugin)]); + } else { + groups.push([instantiationService.createInstance(EnablePluginAction, plugin)]); + } + groups.push([ + instantiationService.createInstance(OpenPluginFolderAction, plugin), + instantiationService.createInstance(OpenPluginReadmeAction, joinPath(plugin.uri, 'README.md')), + ]); + groups.push([instantiationService.createInstance(UninstallPluginAction, plugin)]); + return groups; +} + +class ManagePluginAction extends Action { + static readonly ID = 'agentPlugin.manage'; + static readonly CLASS = `extension-action icon manage ${ThemeIcon.asClassName(manageExtensionIcon)}`; + + private _actionViewItem: DropDownActionViewItem | null = null; + + constructor( + private readonly getActionGroups: () => IAction[][], + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(ManagePluginAction.ID, '', ManagePluginAction.CLASS, true); + this.tooltip = localize('manage', "Manage"); + } + + createActionViewItem(options: IActionViewItemOptions): DropDownActionViewItem { + this._actionViewItem = this.instantiationService.createInstance(DropDownActionViewItem, this, options); + return this._actionViewItem; + } + + override async run(): Promise { + this._actionViewItem?.showMenu(this.getActionGroups()); + } +} + +class DropDownActionViewItem extends ActionViewItem { + constructor( + action: IAction, + options: IActionViewItemOptions, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + ) { + super(null, action, { ...options, icon: true, label: false }); + } + + showMenu(actionGroups: IAction[][]): void { + if (!this.element) { + return; + } + const actions = actionGroups.flatMap(group => [...group, new Separator()]); + if (actions.length > 0) { + actions.pop(); + } + const { left, top, height } = dom.getDomNodePagePosition(this.element); + this.contextMenuService.showContextMenu({ + getAnchor: () => ({ x: left, y: top + height + 10 }), + getActions: () => actions, + onHide: () => disposeIfDisposable(actions), + }); + } +} + //#endregion //#region Renderer @@ -213,7 +281,15 @@ class AgentPluginRenderer implements IPagedRenderer { + if (action instanceof ManagePluginAction) { + return action.createActionViewItem(options); + } + return undefined; + } + }); actionbar.setFocusable(false); return { root, name, description, detail, actionbar, disposables: [actionbar], elementDisposables: [] }; } @@ -244,6 +320,10 @@ class AgentPluginRenderer implements IPagedRenderer getInstalledPluginContextMenuActionGroups(element.plugin, this.instantiationService)); + data.elementDisposables.push(manageAction); + data.actionbar.push([manageAction], { icon: true, label: false }); } } @@ -383,20 +463,15 @@ export class AgentPluginsListView extends AbstractExtensionsListView [...group, new Separator()]); + if (actions.length > 0) { + actions.pop(); } - - actions.push(new Separator()); - actions.push(this.instantiationService.createInstance(OpenPluginFolderAction, item.plugin)); - actions.push(this.instantiationService.createInstance(OpenPluginReadmeAction, joinPath(item.plugin.uri, 'README.md'))); - actions.push(new Separator()); - actions.push(this.instantiationService.createInstance(UninstallPluginAction, item.plugin)); } else { + actions = []; if (item.readmeUri) { actions.push(this.instantiationService.createInstance(OpenPluginReadmeAction, item.readmeUri)); } From 96ab8cdf4958c9f0c4e8806a92052384ca14e341 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:04:54 -0800 Subject: [PATCH 093/448] Make sure we specify a tsconfig.browser.json for browser ext --- extensions/extension-editing/esbuild.browser.mts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/extension-editing/esbuild.browser.mts b/extensions/extension-editing/esbuild.browser.mts index 170f3cda313..58b5fb7d6d5 100644 --- a/extensions/extension-editing/esbuild.browser.mts +++ b/extensions/extension-editing/esbuild.browser.mts @@ -15,4 +15,7 @@ run({ }, srcDir, outdir: outDir, + additionalOptions: { + tsconfig: path.join(import.meta.dirname, 'tsconfig.browser.json'), + }, }, process.argv); From e8e300fb102f8fbc0788beb0ad500d2a7e6a371f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 14:05:00 -0800 Subject: [PATCH 094/448] fix compile error in mcpGatewayToolBrokerChannel test --- .../contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index c2e92dd5dd3..b61aac798c5 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -245,7 +245,7 @@ function createNeverStartingServer( start: async () => { startCalls++; // Never resolves — simulates a server that hangs on startup. - return new Promise<{ state: McpConnectionState.Kind }>(() => { }); + return new Promise(() => { }); }, stop: async () => { }, cacheState, From c5d8f77e74aac322285c1373bf0efa72679e6dd4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 14:17:23 -0800 Subject: [PATCH 095/448] chat: combine multiple pending steering messages into single request (#299061) * chat: combine multiple pending steering messages into single request When Copilot is running a tool, sending multiple steering messages only sends the first one. The chatbot waits indefinitely for the remaining messages. This fix combines all consecutive pending steering messages into a single request by: - Adding dequeueAllSteeringRequests() to ChatModel to dequeue all consecutive steering messages at the front of the queue - Refactoring processNextPendingRequest() to handle both steering and queued requests through a unified flow that: - Combines multiple steering message texts with \n\n separator - Merges attachments from all steering messages - Re-parses the combined text - Sends as a single request to the agent Fixes https://github.com/microsoft/vscode/issues/298324 (Commit message generated by Copilot) * comments --- .../common/chatService/chatServiceImpl.ts | 75 ++++++++++++++----- .../contrib/chat/common/model/chatModel.ts | 15 ++++ .../common/chatService/chatService.test.ts | 56 ++++++++++++++ 3 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 692aa3470f0..25bf8d21d2e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -40,7 +40,7 @@ import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, ICha import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../requestParser/chatRequestParser.js'; -import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; +import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatSendResultSent, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSessionsService } from '../chatSessionsService.js'; import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js'; @@ -1241,49 +1241,90 @@ export class ChatService extends Disposable implements IChatService { /** * Process the next pending request from the model's queue, if any. * Called after a request completes to continue processing queued requests. + * Multiple consecutive steering requests are combined into a single request. */ private processNextPendingRequest(model: ChatModel): void { - const pendingRequest = model.dequeuePendingRequest(); - if (!pendingRequest) { + // Dequeue all consecutive steering requests and combine them into one + const steeringRequests = model.dequeueAllSteeringRequests(); + + // Then dequeue a single non-steering request if no steering was found + const nextQueued = steeringRequests.length === 0 ? model.dequeuePendingRequest() : undefined; + + const allRequests = steeringRequests.length > 0 ? steeringRequests : (nextQueued ? [nextQueued] : []); + if (allRequests.length === 0) { return; } - this.trace('processNextPendingRequest', `Processing queued request for session ${model.sessionResource}`); + this.trace('processNextPendingRequest', `Processing ${allRequests.length} queued request(s) for session ${model.sessionResource}`); - const deferred = this._queuedRequestDeferreds.get(pendingRequest.request.id); - this._queuedRequestDeferreds.delete(pendingRequest.request.id); + // Collect and remove all deferreds + const deferreds: DeferredPromise[] = []; + for (const req of allRequests) { + const deferred = this._queuedRequestDeferreds.get(req.request.id); + this._queuedRequestDeferreds.delete(req.request.id); + if (deferred) { + deferreds.push(deferred); + } + } + // Build send options from the first request, combining attachments from all + const firstRequest = allRequests[0]; const sendOptions: IChatSendRequestOptions = { - ...pendingRequest.sendOptions, - // Ensure attachedContext is preserved after deserialization, where sendOptions - // loses attachedContext but the request model retains it in variableData. - attachedContext: pendingRequest.request.variableData.variables.slice(), + ...firstRequest.sendOptions, + attachedContext: allRequests.flatMap(req => req.request.variableData.variables.slice()), }; + const location = sendOptions.location ?? sendOptions.locationData?.type ?? model.initialLocation; const defaultAgent = this.chatAgentService.getDefaultAgent(location, sendOptions.modeInfo?.kind); if (!defaultAgent) { this.logService.warn('processNextPendingRequest', `No default agent for location ${location}`); - deferred?.complete({ kind: 'rejected', reason: 'No default agent available' }); + for (const deferred of deferreds) { + deferred.complete({ kind: 'rejected', reason: 'No default agent available' }); + } + return; + } + + // For multiple steering requests, combine texts and re-parse; otherwise use as-is + let parsedRequest: IParsedChatRequest; + try { + if (allRequests.length > 1) { + const combinedText = allRequests.map(req => req.request.message.text).join('\n\n'); + // message.text already includes agent/slash-command prefixes from the + // original parse, so clear them to avoid double-prefixing. + parsedRequest = this.parseChatRequest(model.sessionResource, combinedText, location, { + ...sendOptions, + agentId: undefined, + slashCommand: undefined, + }); + } else { + parsedRequest = firstRequest.request.message; + } + } catch (err) { + this.logService.error('processNextPendingRequest: failed to parse combined chat request', err); + const reason = toErrorMessage(err); + for (const deferred of deferreds) { + deferred.complete({ kind: 'rejected', reason }); + } return; } - const parsedRequest = pendingRequest.request.message; const silentAgent = sendOptions.agentIdSilent ? this.chatAgentService.getAgent(sendOptions.agentIdSilent) : undefined; const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - // Send the queued request - this will add it to _pendingRequests and handle it normally - const responseState = this._sendRequestAsync(model, model.sessionResource, parsedRequest, pendingRequest.request.attempt, !sendOptions.noCommandDetection, silentAgent ?? defaultAgent, location, sendOptions); + const responseState = this._sendRequestAsync(model, model.sessionResource, parsedRequest, firstRequest.request.attempt, !sendOptions.noCommandDetection, silentAgent ?? defaultAgent, location, sendOptions); - // Resolve the deferred with the sent result - deferred?.complete({ + const result: ChatSendResultSent = { kind: 'sent', data: { ...responseState, agent, slashCommand: agentSlashCommandPart?.command, }, - }); + }; + for (const deferred of deferreds) { + deferred.complete(result); + } } private generateInitialChatTitleIfNeeded(model: ChatModel, request: IChatAgentRequest, defaultAgent: IChatAgentData, token: CancellationToken): void { diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 6b322748eed..3fb3a88c82f 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -1994,6 +1994,21 @@ export class ChatModel extends Disposable implements IChatModel { return request; } + /** + * @internal Used by ChatService to dequeue all consecutive steering requests at the front of the queue. + * Returns an empty array if the first pending request is not a steering request. + */ + dequeueAllSteeringRequests(): IChatPendingRequest[] { + const steeringRequests: IChatPendingRequest[] = []; + while (this._pendingRequests.at(0)?.kind === ChatRequestQueueKind.Steering) { + steeringRequests.push(this._pendingRequests.shift()!); + } + if (steeringRequests.length > 0) { + this._onDidChangePendingRequests.fire(); + } + return steeringRequests; + } + /** * @internal Used by ChatService to clear all pending requests */ diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 9adc29af139..f0bd3984aae 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -479,6 +479,62 @@ suite('ChatService', () => { completeRequest.complete(); await response.data.responseCompletePromise; }); + + test('multiple steering messages are combined into a single request', async () => { + const requestStarted = new DeferredPromise(); + const completeRequest = new DeferredPromise(); + const invokedRequests: string[] = []; + + const slowAgent: IChatAgentImplementation = { + async invoke(request, progress, history, token) { + invokedRequests.push(request.message); + if (invokedRequests.length === 1) { + requestStarted.complete(); + await completeRequest.p; + } + return {}; + }, + }; + + testDisposables.add(chatAgentService.registerAgent('slowAgent', { ...getAgentData('slowAgent'), isDefault: true })); + testDisposables.add(chatAgentService.registerAgentImplementation('slowAgent', slowAgent)); + + const testService = createChatService(); + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + + // Start a request that will wait + const response = await testService.sendRequest(model.sessionResource, 'first request', { agentId: 'slowAgent' }); + ChatSendResult.assertSent(response); + + // Wait for the agent to start processing + await requestStarted.p; + + // Queue 3 steering messages while the first request is in progress + const steering1 = await testService.sendRequest(model.sessionResource, 'steering1', { agentId: 'slowAgent', queue: ChatRequestQueueKind.Steering }); + const steering2 = await testService.sendRequest(model.sessionResource, 'steering2', { agentId: 'slowAgent', queue: ChatRequestQueueKind.Steering }); + const steering3 = await testService.sendRequest(model.sessionResource, 'steering3', { agentId: 'slowAgent', queue: ChatRequestQueueKind.Steering }); + assert.ok(ChatSendResult.isQueued(steering1)); + assert.ok(ChatSendResult.isQueued(steering2)); + assert.ok(ChatSendResult.isQueued(steering3)); + + // Complete the first request - should trigger processing of combined steering requests + completeRequest.complete(); + await response.data.responseCompletePromise; + + // Wait for all deferred promises to resolve + await steering1.deferred; + await steering2.deferred; + await steering3.deferred; + + // Should have only invoked 2 requests: the initial and the combined steering + assert.strictEqual(invokedRequests.length, 2, 'Should have only 2 invocations (initial + combined steering)'); + // The combined message includes all steering texts joined with \n\n + assert.ok(invokedRequests[1].includes('steering1'), 'Combined message should include steering1'); + assert.ok(invokedRequests[1].includes('steering2'), 'Combined message should include steering2'); + assert.ok(invokedRequests[1].includes('steering3'), 'Combined message should include steering3'); + assert.ok(invokedRequests[1].includes('\n\n'), 'Combined message should use \\n\\n as separator'); + }); }); From 6acf4df97ef0f7e5d293b81c0bdbbe4e3cd67aba Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:21:47 -0800 Subject: [PATCH 096/448] McpGateway extensive logging (#299043) * McpGateway extensive logging * update * update --- src/vs/code/electron-main/app.ts | 2 +- src/vs/platform/mcp/node/mcpGatewayChannel.ts | 14 +++++- src/vs/platform/mcp/node/mcpGatewayService.ts | 25 +++++++++-- src/vs/platform/mcp/node/mcpGatewaySession.ts | 32 +++++++++++-- .../contrib/mcp/browser/mcpGatewayService.ts | 13 +++++- .../mcpGatewayToolBrokerContribution.ts | 4 +- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 45 ++++++++++++++++--- .../mcp/electron-browser/mcpGatewayService.ts | 17 +++++-- .../mcpGatewayToolBrokerContribution.ts | 4 +- .../mcpGatewayToolBrokerChannel.test.ts | 11 ++--- 10 files changed, 139 insertions(+), 28 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 78641318c5f..ab9fb26c1d2 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1288,7 +1288,7 @@ export class CodeApplication extends Disposable { // MCP const mcpDiscoveryChannel = ProxyChannel.fromService(accessor.get(INativeMcpDiscoveryHelperService), disposables); mainProcessElectronServer.registerChannel(NativeMcpDiscoveryHelperChannelName, mcpDiscoveryChannel); - const mcpGatewayChannel = this._register(new McpGatewayChannel(mainProcessElectronServer, accessor.get(IMcpGatewayService))); + const mcpGatewayChannel = this._register(new McpGatewayChannel(mainProcessElectronServer, accessor.get(IMcpGatewayService), accessor.get(ILoggerMainService))); mainProcessElectronServer.registerChannel(McpGatewayChannelName, mcpGatewayChannel); // Logger diff --git a/src/vs/platform/mcp/node/mcpGatewayChannel.ts b/src/vs/platform/mcp/node/mcpGatewayChannel.ts index 0b0ce1edb0a..78d0e93a3f0 100644 --- a/src/vs/platform/mcp/node/mcpGatewayChannel.ts +++ b/src/vs/platform/mcp/node/mcpGatewayChannel.ts @@ -6,6 +6,7 @@ import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { IPCServer, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { ILoggerService } from '../../log/common/log.js'; import { IGatewayCallToolResult, IGatewayServerResources, IGatewayServerResourceTemplates, IMcpGatewayService, McpGatewayToolBrokerChannelName } from '../common/mcpGateway.js'; import { MCP } from '../common/modelContextProtocol.js'; @@ -19,10 +20,14 @@ export class McpGatewayChannel extends Disposable implements IServerCh constructor( private readonly _ipcServer: IPCServer, - @IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService + @IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService, + @ILoggerService private readonly _loggerService: ILoggerService, ) { super(); - this._register(_ipcServer.onDidRemoveConnection(c => mcpGatewayService.disposeGatewaysForClient(c.ctx))); + this._register(_ipcServer.onDidRemoveConnection(c => { + this._loggerService.getLogger('mcpGateway')?.info(`[McpGateway][Channel] Client disconnected: ${c.ctx}, cleaning up gateways`); + mcpGatewayService.disposeGatewaysForClient(c.ctx); + })); } listen(_ctx: TContext, _event: string): Event { @@ -30,6 +35,9 @@ export class McpGatewayChannel extends Disposable implements IServerCh } async call(ctx: TContext, command: string, args?: unknown): Promise { + const logger = this._loggerService.getLogger('mcpGateway'); + logger?.debug(`[McpGateway][Channel] IPC call: ${command} from client ${ctx}`); + switch (command) { case 'createGateway': { const brokerChannel = ipcChannelForContext(this._ipcServer, ctx); @@ -42,9 +50,11 @@ export class McpGatewayChannel extends Disposable implements IServerCh readResource: (serverIndex, uri) => brokerChannel.call('readResource', { serverIndex, uri }), listResourceTemplates: () => brokerChannel.call('listResourceTemplates'), }); + logger?.info(`[McpGateway][Channel] Gateway created: ${result.gatewayId} for client ${ctx}`); return result as T; } case 'disposeGateway': { + logger?.info(`[McpGateway][Channel] Disposing gateway: ${args as string} for client ${ctx}`); await this.mcpGatewayService.disposeGateway(args as string); return undefined as T; } diff --git a/src/vs/platform/mcp/node/mcpGatewayService.ts b/src/vs/platform/mcp/node/mcpGatewayService.ts index c86b23f21db..6131c7677da 100644 --- a/src/vs/platform/mcp/node/mcpGatewayService.ts +++ b/src/vs/platform/mcp/node/mcpGatewayService.ts @@ -56,6 +56,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService const gateway = new McpGatewayRoute(gatewayId, this._logger, toolInvoker); this._gateways.set(gatewayId, gateway); + this._logger.info(`[McpGatewayService] Active gateways: ${this._gateways.size}`); // Track client ownership if clientId provided (for cleanup on disconnect) if (clientId) { @@ -83,7 +84,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService gateway.dispose(); this._gateways.delete(gatewayId); this._gatewayToClient.delete(gatewayId); - this._logger.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`); + this._logger.info(`[McpGatewayService] Disposed gateway: ${gatewayId} (remaining: ${this._gateways.size})`); // If no more gateways, shut down the server if (this._gateways.size === 0) { @@ -101,7 +102,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService } if (gatewaysToDispose.length > 0) { - this._logger.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`); + this._logger.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}: [${gatewaysToDispose.join(', ')}]`); for (const gatewayId of gatewaysToDispose) { this._gateways.get(gatewayId)?.dispose(); @@ -204,6 +205,8 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService const url = new URL(req.url!, `http://${req.headers.host}`); const pathParts = url.pathname.split('/').filter(Boolean); + this._logger.debug(`[McpGatewayService] ${req.method} ${url.pathname} (active gateways: ${this._gateways.size})`); + // Expected path: /gateway/{gatewayId} if (pathParts.length >= 2 && pathParts[0] === 'gateway') { const gatewayId = pathParts[1]; @@ -216,11 +219,13 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService } // Not found + this._logger.warn(`[McpGatewayService] ${req.method} ${url.pathname}: gateway not found`); res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Gateway not found' })); } override dispose(): void { + this._logger.info(`[McpGatewayService] Disposing service (gateways: ${this._gateways.size})`); this._stopServer(); for (const gateway of this._gateways.values()) { gateway.dispose(); @@ -247,6 +252,8 @@ class McpGatewayRoute extends Disposable { } handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { + this._logger.debug(`[McpGateway][route ${this.gatewayId}] ${req.method} request (sessions: ${this._sessions.size})`); + if (req.method === 'POST') { void this._handlePost(req, res); return; @@ -266,6 +273,7 @@ class McpGatewayRoute extends Disposable { } public override dispose(): void { + this._logger.info(`[McpGateway][route ${this.gatewayId}] Disposing route (sessions: ${this._sessions.size})`); for (const session of this._sessions.values()) { session.dispose(); } @@ -286,6 +294,7 @@ class McpGatewayRoute extends Disposable { return; } + this._logger.info(`[McpGateway][route ${this.gatewayId}] Deleting session ${sessionId}`); session.dispose(); this._sessions.delete(sessionId); res.writeHead(204); @@ -305,6 +314,7 @@ class McpGatewayRoute extends Disposable { return; } + this._logger.info(`[McpGateway][route ${this.gatewayId}] SSE connection requested for session ${sessionId}`); session.attachSseClient(req, res); } @@ -315,10 +325,13 @@ class McpGatewayRoute extends Disposable { return; } + this._logger.debug(`[McpGateway][route ${this.gatewayId}] Handling POST`); + let message: JsonRpcMessage | JsonRpcMessage[]; try { message = JSON.parse(body) as JsonRpcMessage | JsonRpcMessage[]; } catch (error) { + this._logger.warn(`[McpGateway][route ${this.gatewayId}] JSON parse error: ${error instanceof Error ? error.message : String(error)}`); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(JsonRpcProtocol.createParseError('Parse error', error instanceof Error ? error.message : String(error)))); return; @@ -339,13 +352,16 @@ class McpGatewayRoute extends Disposable { }; if (responses.length === 0) { + this._logger.debug(`[McpGateway][route ${this.gatewayId}] POST response: 202 (no content)`); res.writeHead(202, headers); res.end(); return; } + const responseBody = JSON.stringify(Array.isArray(message) ? responses : responses[0]); + this._logger.debug(`[McpGateway][route ${this.gatewayId}] POST response: 200, body: ${responseBody}`); res.writeHead(200, headers); - res.end(JSON.stringify(Array.isArray(message) ? responses : responses[0])); + res.end(responseBody); } catch (error) { this._logger.error('[McpGatewayService] Failed handling gateway request', error); this._respondHttpError(res, 500, 'Internal server error'); @@ -356,6 +372,7 @@ class McpGatewayRoute extends Disposable { if (headerSessionId) { const existing = this._sessions.get(headerSessionId); if (!existing) { + this._logger.warn(`[McpGateway][route ${this.gatewayId}] Session not found: ${headerSessionId}`); this._respondHttpError(res, 404, 'Session not found'); return undefined; } @@ -369,6 +386,7 @@ class McpGatewayRoute extends Disposable { } const sessionId = generateUuid(); + this._logger.info(`[McpGateway][route ${this.gatewayId}] Creating new session ${sessionId}`); const session = new McpGatewaySession(sessionId, this._logger, () => { this._sessions.delete(sessionId); }, this._toolInvoker); @@ -377,6 +395,7 @@ class McpGatewayRoute extends Disposable { } private _respondHttpError(res: http.ServerResponse, statusCode: number, error: string): void { + this._logger.debug(`[McpGateway][route ${this.gatewayId}] HTTP error response: ${statusCode} ${error}`); res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: statusCode, message: error } } satisfies JsonRpcMessage)); } diff --git a/src/vs/platform/mcp/node/mcpGatewaySession.ts b/src/vs/platform/mcp/node/mcpGatewaySession.ts index f35223a15a3..579b0184495 100644 --- a/src/vs/platform/mcp/node/mcpGatewaySession.ts +++ b/src/vs/platform/mcp/node/mcpGatewaySession.ts @@ -105,6 +105,7 @@ export class McpGatewaySession extends Disposable { return; } + this._logService.info(`[McpGateway][session ${this.id}] Tools changed, notifying client`); this._rpc.sendNotification({ method: 'notifications/tools/list_changed' }); })); @@ -113,6 +114,7 @@ export class McpGatewaySession extends Disposable { return; } + this._logService.info(`[McpGateway][session ${this.id}] Resources changed, notifying client`); this._rpc.sendNotification({ method: 'notifications/resources/list_changed' }); })); } @@ -126,9 +128,11 @@ export class McpGatewaySession extends Disposable { res.write(': connected\n\n'); this._sseClients.add(res); + this._logService.info(`[McpGateway][session ${this.id}] SSE client attached (total: ${this._sseClients.size})`); res.on('close', () => { this._sseClients.delete(res); + this._logService.info(`[McpGateway][session ${this.id}] SSE client detached (total: ${this._sseClients.size})`); }); } @@ -145,6 +149,7 @@ export class McpGatewaySession extends Disposable { } public override dispose(): void { + this._logService.info(`[McpGateway][session ${this.id}] Disposing session (SSE clients: ${this._sseClients.size})`); for (const client of this._sseClients) { if (!client.destroyed) { client.end(); @@ -160,10 +165,12 @@ export class McpGatewaySession extends Disposable { if (this._isCollectingPostResponses) { this._pendingResponses.push(message); } + this._logService.debug(`[McpGateway][session ${this.id}] --> response: ${JSON.stringify(message)}`); return; } if (isJsonRpcNotification(message)) { + this._logService.debug(`[McpGateway][session ${this.id}] --> notification: ${(message as IJsonRpcNotification).method}`); this._broadcastSse(message); return; } @@ -173,11 +180,13 @@ export class McpGatewaySession extends Disposable { private _broadcastSse(message: JsonRpcMessage): void { if (this._sseClients.size === 0) { + this._logService.debug(`[McpGateway][session ${this.id}] No SSE clients to broadcast to, dropping message`); return; } const payload = JSON.stringify(message); const eventId = String(++this._lastEventId); + this._logService.debug(`[McpGateway][session ${this.id}] Broadcasting SSE event id=${eventId} to ${this._sseClients.size}`); const lines = payload.split(/\r?\n/g); const data = [ `id: ${eventId}`, @@ -198,11 +207,14 @@ export class McpGatewaySession extends Disposable { } private async _handleRequest(request: IJsonRpcRequest): Promise { + this._logService.debug(`[McpGateway][session ${this.id}] <-- request: ${request.method} (id=${String(request.id)})`); + if (request.method === 'initialize') { return this._handleInitialize(request); } if (!this._isInitialized) { + this._logService.warn(`[McpGateway][session ${this.id}] Rejected request '${request.method}': session not initialized`); throw new JsonRpcError(MCP_INVALID_REQUEST, 'Session is not initialized'); } @@ -220,13 +232,17 @@ export class McpGatewaySession extends Disposable { case 'resources/templates/list': return this._handleListResourceTemplates(); default: + this._logService.warn(`[McpGateway][session ${this.id}] Unknown method: ${request.method}`); throw new JsonRpcError(MCP_METHOD_NOT_FOUND, `Method not found: ${request.method}`); } } private _handleNotification(notification: IJsonRpcNotification): void { + this._logService.debug(`[McpGateway][session ${this.id}] <-- notification: ${notification.method}`); + if (notification.method === 'notifications/initialized') { this._isInitialized = true; + this._logService.info(`[McpGateway][session ${this.id}] Session initialized`); this._rpc.sendNotification({ method: 'notifications/tools/list_changed' }); this._rpc.sendNotification({ method: 'notifications/resources/list_changed' }); } @@ -276,21 +292,27 @@ export class McpGatewaySession extends Disposable { ? params.arguments as Record : {}; + this._logService.debug(`[McpGateway][session ${this.id}] Calling tool '${params.name}' with args: ${JSON.stringify(argumentsValue)}`); + try { const { result, serverIndex } = await this._toolInvoker.callTool(params.name, argumentsValue); + this._logService.debug(`[McpGateway][session ${this.id}] Tool '${params.name}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`); return { ...result, content: encodeResourceUrisInContent(result.content, serverIndex), }; } catch (error) { - this._logService.error('[McpGatewayService] Tool call invocation failed', error); + this._logService.error(`[McpGateway][session ${this.id}] Tool '${params.name}' invocation failed`, error); throw new JsonRpcError(MCP_INVALID_PARAMS, String(error)); } } private _handleListTools(): unknown { return this._toolInvoker.listTools() - .then(tools => ({ tools })); + .then(tools => { + this._logService.debug(`[McpGateway][session ${this.id}] Listed ${tools.length} tool(s): [${tools.map(t => t.name).join(', ')}]`); + return { tools }; + }); } private async _handleListResources(): Promise { @@ -304,6 +326,7 @@ export class McpGatewaySession extends Disposable { }); } } + this._logService.debug(`[McpGateway][session ${this.id}] Listed ${allResources.length} resource(s) from ${serverResults.length} server(s)`); return { resources: allResources }; } @@ -314,8 +337,10 @@ export class McpGatewaySession extends Disposable { } const { serverIndex, originalUri } = decodeGatewayResourceUri(params.uri); + this._logService.debug(`[McpGateway][session ${this.id}] Reading resource '${originalUri}' from server ${serverIndex}`); try { const result = await this._toolInvoker.readResource(serverIndex, originalUri); + this._logService.debug(`[McpGateway][session ${this.id}] Resource read returned ${result.contents.length} content(s)`); return { contents: result.contents.map(content => ({ ...content, @@ -323,7 +348,7 @@ export class McpGatewaySession extends Disposable { })), }; } catch (error) { - this._logService.error('[McpGatewayService] Resource read failed', error); + this._logService.error(`[McpGateway][session ${this.id}] Resource read failed for '${originalUri}'`, error); throw new JsonRpcError(MCP_INVALID_PARAMS, String(error)); } } @@ -339,6 +364,7 @@ export class McpGatewaySession extends Disposable { }); } } + this._logService.debug(`[McpGateway][session ${this.id}] Listed ${allTemplates.length} resource template(s) from ${serverResults.length} server(s)`); return { resourceTemplates: allTemplates }; } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts index 49bb014e5d9..4d2d9df2c49 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts @@ -5,6 +5,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; @@ -23,28 +24,36 @@ export class BrowserMcpGatewayService implements IWorkbenchMcpGatewayService { constructor( @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @ILogService private readonly _logService: ILogService, ) { } async createGateway(inRemote: boolean): Promise { + this._logService.debug(`[McpGateway][BrowserWorkbench] createGateway requested (inRemote=${inRemote})`); + // Browser can only create gateways in remote environment if (!inRemote) { + this._logService.info('[McpGateway][BrowserWorkbench] Cannot create local gateway in browser environment'); return undefined; } const connection = this._remoteAgentService.getConnection(); if (!connection) { - // Serverless web environment - no gateway available + this._logService.info('[McpGateway][BrowserWorkbench] No remote connection available (serverless web)'); return undefined; } + this._logService.info('[McpGateway][BrowserWorkbench] Creating remote gateway via remote server'); // Use the remote server's gateway service return connection.withChannel(McpGatewayChannelName, async channel => { const service = ProxyChannel.toService(channel); const info = await service.createGateway(undefined); + const address = URI.revive(info.address); + this._logService.info(`[McpGateway][BrowserWorkbench] Remote gateway created: ${address}`); return { - address: URI.revive(info.address), + address, dispose: () => { + this._logService.info(`[McpGateway][BrowserWorkbench] Disposing remote gateway: ${info.gatewayId}`); service.disposeGateway(info.gatewayId); } }; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts b/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts index 175bb839214..4c041d05f47 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { McpGatewayToolBrokerChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpService } from '../common/mcpTypes.js'; @@ -13,7 +14,8 @@ export class McpGatewayToolBrokerContribution implements IWorkbenchContribution constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IMcpService mcpService: IMcpService, + @ILogService logService: ILogService, ) { - remoteAgentService.getConnection()?.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService)); + remoteAgentService.getConnection()?.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService, logService)); } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 88a1da8b4b6..d1b0a61d23a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -8,6 +8,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IGatewayCallToolResult, IGatewayServerResources, IGatewayServerResourceTemplates } from '../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../../../platform/mcp/common/modelContextProtocol.js'; import { McpServer } from './mcpServer.js'; @@ -32,8 +33,10 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh constructor( private readonly _mcpService: IMcpService, + private readonly _logService: ILogService, ) { super(); + this._logService.debug('[McpGateway][ToolBroker] Initialized'); let toolsInitialized = false; this._register(autorun(reader => { @@ -42,6 +45,7 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } if (toolsInitialized) { + this._logService.debug('[McpGateway][ToolBroker] Tools changed, firing onDidChangeTools'); this._onDidChangeTools.fire(); } else { toolsInitialized = true; @@ -55,6 +59,7 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } if (resourcesInitialized) { + this._logService.debug('[McpGateway][ToolBroker] Resources changed, firing onDidChangeResources'); this._onDidChangeResources.fire(); } else { resourcesInitialized = true; @@ -93,6 +98,8 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } async call(_ctx: unknown, command: string, arg?: unknown, cancellationToken?: CancellationToken): Promise { + this._logService.debug(`[McpGateway][ToolBroker] IPC call: ${command}`); + switch (command) { case 'listTools': { const tools = await this._listTools(); @@ -124,11 +131,13 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private async _listTools(): Promise { const mcpTools: MCP.Tool[] = []; const servers = this._mcpService.servers.get(); + this._logService.debug(`[McpGateway][ToolBroker] listTools: ${servers.length} server(s) known`); await Promise.all(servers.map(server => this._ensureServerReady(server))); for (const server of servers) { const cacheState = server.cacheState.get(); if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { + this._logService.debug(`[McpGateway][ToolBroker] Skipping server '${server.definition.id}' (cacheState=${cacheState})`); continue; } @@ -141,58 +150,74 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } } + this._logService.debug(`[McpGateway][ToolBroker] listTools result: ${mcpTools.length} tool(s): [${mcpTools.map(t => t.name).join(', ')}]`); return mcpTools; } private async _callTool(name: string, args: Record, token: CancellationToken = CancellationToken.None): Promise { + this._logService.debug(`[McpGateway][ToolBroker] callTool '${name}' with args: ${JSON.stringify(args)}`); + for (const server of this._mcpService.servers.get()) { const tool = server.tools.get().find(t => t.definition.name === name && (t.visibility & McpToolVisibility.Model) ); if (tool) { + this._logService.debug(`[McpGateway][ToolBroker] Found tool '${name}' on server '${server.definition.id}' (index=${this._getServerIndex(server)})`); const result = await tool.call(args, undefined, token); + this._logService.debug(`[McpGateway][ToolBroker] Tool '${name}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`); return { result, serverIndex: this._getServerIndex(server) }; } } + this._logService.warn(`[McpGateway][ToolBroker] Tool '${name}' not found on any server`); throw new Error(`Unknown tool: ${name}`); } private async _listResources(): Promise { const results: IGatewayServerResources[] = []; const servers = this._mcpService.servers.get(); + this._logService.debug(`[McpGateway][ToolBroker] listResources: ${servers.length} server(s) known`); + await Promise.all(servers.map(async server => { await this._ensureServerReady(server); const capabilities = server.capabilities.get(); if (!capabilities || !(capabilities & McpCapability.Resources)) { + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' has no resource capability, skipping`); return; } try { const resources = await McpServer.callOn(server, h => h.listResources()); + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' (index=${this._getServerIndex(server)}) listed ${resources.length} resource(s)`); results.push({ serverIndex: this._getServerIndex(server), resources }); - } catch { - // Server failed; skip + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to list resources`, error); } })); + this._logService.debug(`[McpGateway][ToolBroker] listResources result: ${results.length} server(s) with resources`); return results; } private async _readResource(serverIndex: number, uri: string, token: CancellationToken = CancellationToken.None): Promise { const server = this._getServerByIndex(serverIndex); if (!server) { + this._logService.warn(`[McpGateway][ToolBroker] readResource: unknown server index ${serverIndex}`); throw new Error(`Unknown server index: ${serverIndex}`); } - return McpServer.callOn(server, h => h.readResource({ uri }, token), token); + this._logService.debug(`[McpGateway][ToolBroker] readResource '${uri}' from server '${server.definition.id}' (index=${serverIndex})`); + const result = await McpServer.callOn(server, h => h.readResource({ uri }, token), token); + this._logService.debug(`[McpGateway][ToolBroker] readResource returned ${result.contents.length} content(s)`); + return result; } private async _listResourceTemplates(): Promise { const results: IGatewayServerResourceTemplates[] = []; const servers = this._mcpService.servers.get(); + this._logService.debug(`[McpGateway][ToolBroker] listResourceTemplates: ${servers.length} server(s) known`); await Promise.all(servers.map(async server => { await this._ensureServerReady(server); @@ -204,12 +229,14 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh try { const resourceTemplates = await McpServer.callOn(server, h => h.listResourceTemplates()); + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' (index=${this._getServerIndex(server)}) listed ${resourceTemplates.length} resource template(s)`); results.push({ serverIndex: this._getServerIndex(server), resourceTemplates }); - } catch { - // Server failed; skip + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to list resource templates`, error); } })); + this._logService.debug(`[McpGateway][ToolBroker] listResourceTemplates result: ${results.length} server(s) with templates`); return results; } @@ -219,12 +246,16 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh return true; } + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' not ready (cacheState=${cacheState}), starting...`); try { - return await startServerAndWaitForLiveTools(server, { + const ready = await startServerAndWaitForLiveTools(server, { promptType: 'all-untrusted', errorOnUserInteraction: true, }); - } catch { + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' ready=${ready}`); + return ready; + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to start`, error); return false; } } diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts index 2af2c0b4adf..71d0dcdc64b 100644 --- a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts @@ -6,6 +6,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; @@ -24,6 +25,7 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { constructor( @IMainProcessService mainProcessService: IMainProcessService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @ILogService private readonly _logService: ILogService, ) { this._localPlatformService = ProxyChannel.toService( mainProcessService.getChannel(McpGatewayChannelName) @@ -31,6 +33,7 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { } async createGateway(inRemote: boolean): Promise { + this._logService.debug(`[McpGateway][Workbench] createGateway requested (inRemote=${inRemote})`); if (inRemote) { return this._createRemoteGateway(); } else { @@ -39,11 +42,15 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { } private async _createLocalGateway(): Promise { + this._logService.info('[McpGateway][Workbench] Creating local gateway via main process'); const info = await this._localPlatformService.createGateway(undefined); + const address = URI.revive(info.address); + this._logService.info(`[McpGateway][Workbench] Local gateway created: ${address}`); return { - address: URI.revive(info.address), + address, dispose: () => { + this._logService.info(`[McpGateway][Workbench] Disposing local gateway: ${info.gatewayId}`); this._localPlatformService.disposeGateway(info.gatewayId); } }; @@ -52,17 +59,21 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { private async _createRemoteGateway(): Promise { const connection = this._remoteAgentService.getConnection(); if (!connection) { - // No remote connection - cannot create remote gateway + this._logService.info('[McpGateway][Workbench] No remote connection available for remote gateway'); return undefined; } + this._logService.info('[McpGateway][Workbench] Creating remote gateway via remote server'); return connection.withChannel(McpGatewayChannelName, async channel => { const service = ProxyChannel.toService(channel); const info = await service.createGateway(undefined); + const address = URI.revive(info.address); + this._logService.info(`[McpGateway][Workbench] Remote gateway created: ${address}`); return { - address: URI.revive(info.address), + address, dispose: () => { + this._logService.info(`[McpGateway][Workbench] Disposing remote gateway: ${info.gatewayId}`); service.disposeGateway(info.gatewayId); } }; diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts index af994c82fe3..778d53de5f6 100644 --- a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts @@ -5,6 +5,7 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { McpGatewayToolBrokerChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IMcpService } from '../common/mcpTypes.js'; import { McpGatewayToolBrokerChannel } from '../common/mcpGatewayToolBrokerChannel.js'; @@ -13,7 +14,8 @@ export class McpGatewayToolBrokerContribution implements IWorkbenchContribution constructor( @IMainProcessService mainProcessService: IMainProcessService, @IMcpService mcpService: IMcpService, + @ILogService logService: ILogService, ) { - mainProcessService.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService)); + mainProcessService.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService, logService)); } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index 53124c8acc3..f102628f86a 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -7,6 +7,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IGatewayCallToolResult } from '../../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../common/modelContextProtocol.js'; import { McpGatewayToolBrokerChannel } from '../../common/mcpGatewayToolBrokerChannel.js'; @@ -18,7 +19,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('lists model-visible tools with namespaced identities', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const serverA = createServer('collectionA', 'serverA', [ createTool('mcp_serverA_echo', async () => ({ content: [{ type: 'text', text: 'A' }] })), @@ -43,7 +44,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('routes tool calls by namespaced identity', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const invoked: string[] = []; const serverA = createServer('collectionA', 'serverA', [ @@ -79,7 +80,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('emits onDidChangeTools when tool lists change', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer('collectionA', 'serverA', [ createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] })), ]); @@ -104,7 +105,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('does not start server when cache state is live', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer( 'collectionA', @@ -122,7 +123,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('starts server when cache state is unknown', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer( 'collectionA', From 6c608bf99d01df074a174f9b35e54cc2778f63aa Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 14:26:33 -0800 Subject: [PATCH 097/448] fix: invalidate _startupGrace when cacheState regresses --- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 264d67d0a8c..d170e7a8255 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -68,6 +68,18 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh resourcesInitialized = true; } })); + + // Invalidate _startupGrace entries when a server's cacheState transitions + // back to Unknown or Outdated (e.g. after a cache reset), so the next list + // call will re-wait for the server instead of using a stale resolved promise. + this._register(autorun(reader => { + for (const server of this._mcpService.servers.read(reader)) { + const cacheState = server.cacheState.read(reader); + if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { + this._startupGrace.delete(server.definition.id); + } + } + })); } private _getServerIndex(server: IMcpServer): number { From fa59b650b956977b693b30cee62a76cb07631271 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 14:43:16 -0800 Subject: [PATCH 098/448] fix: use per-entry resolved tracking instead of autorun for _startupGrace invalidation --- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index d170e7a8255..ea4548fdae8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -34,8 +34,13 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh * Per-server promise that races server startup against the grace period timeout. * Once set for a server, subsequent list calls await the already-resolved promise * and return immediately instead of waiting again. + * + * The `resolved` flag tracks whether the promise has settled. If a server's + * cacheState regresses to Unknown/Outdated after the promise resolved (e.g. + * after a cache reset), `_waitForStartup` discards the stale entry and creates + * a fresh race so the server gets another chance to start. */ - private readonly _startupGrace = new Map>(); + private readonly _startupGrace = new Map; resolved: boolean }>(); constructor( private readonly _mcpService: IMcpService, @@ -68,18 +73,6 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh resourcesInitialized = true; } })); - - // Invalidate _startupGrace entries when a server's cacheState transitions - // back to Unknown or Outdated (e.g. after a cache reset), so the next list - // call will re-wait for the server instead of using a stale resolved promise. - this._register(autorun(reader => { - for (const server of this._mcpService.servers.read(reader)) { - const cacheState = server.cacheState.read(reader); - if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { - this._startupGrace.delete(server.definition.id); - } - } - })); } private _getServerIndex(server: IMcpServer): number { @@ -103,13 +96,28 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private _waitForStartup(server: IMcpServer): Promise { const id = server.definition.id; - if (!this._startupGrace.has(id)) { - this._startupGrace.set(id, Promise.race([ - this._ensureServerReady(server), - new Promise(resolve => setTimeout(() => resolve(false), this._startupGracePeriodMs)), - ])); + const existing = this._startupGrace.get(id); + // If the previous grace promise already resolved but the server is still + // Unknown/Outdated, the entry is stale (e.g. caches were reset). Discard + // it so we create a fresh race below. + if (existing?.resolved) { + const state = server.cacheState.get(); + if (state === McpServerCacheState.Unknown || state === McpServerCacheState.Outdated) { + this._startupGrace.delete(id); + } } - return this._startupGrace.get(id)!; + if (!this._startupGrace.has(id)) { + const entry: { promise: Promise; resolved: boolean } = { + promise: Promise.race([ + this._ensureServerReady(server), + new Promise(resolve => setTimeout(() => resolve(false), this._startupGracePeriodMs)), + ]), + resolved: false, + }; + entry.promise.then(() => { entry.resolved = true; }); + this._startupGrace.set(id, entry); + } + return this._startupGrace.get(id)!.promise; } private async _shouldUseCachedData(server: IMcpServer): Promise { From 2910902834bb161091514ab7c48487ce4bc0870f Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:50:38 -0800 Subject: [PATCH 099/448] Use settings icon instead of tools icon in chat input (#299068) --- .../workbench/contrib/chat/browser/actions/chatToolActions.ts | 2 +- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index 7afcbdd62aa..a144b617853 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -121,7 +121,7 @@ export class ConfigureToolsAction extends Action2 { super({ id: ConfigureToolsAction.ID, title: localize('label', "Configure Tools..."), - icon: Codicon.tools, + icon: Codicon.settings, f1: false, category: CHAT_CATEGORY, precondition: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 9484c92e09a..86c54b7bd96 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1447,7 +1447,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } /* Hide the tools button when the toolbar is in collapsed state */ -.interactive-session .chat-input-toolbar:has(.hide-chevrons) .action-item:has(.codicon-tools) { +.interactive-session .chat-input-toolbar:has(.hide-chevrons) .action-item:has(.codicon-settings) { display: none; } From f0113db49c4b7abec7d9a2d21475325c0462be6b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 14:59:38 -0800 Subject: [PATCH 100/448] tests --- .../mcpGatewayToolBrokerChannel.test.ts | 112 +++++++++++++++--- 1 file changed, 93 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index b61aac798c5..1cd172ad729 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -6,6 +6,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { observableValue } from '../../../../../base/common/observable.js'; +import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { IGatewayCallToolResult } from '../../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../common/modelContextProtocol.js'; @@ -160,28 +161,92 @@ suite('McpGatewayToolBrokerChannel', () => { channel.dispose(); }); - test('returns empty tools and does not re-wait if server does not start within grace period', async () => { - const mcpService = new TestMcpService(); - // Use a very short grace period so the test does not take 5 s. - const channel = new McpGatewayToolBrokerChannel(mcpService, 10); + test('returns empty tools and does not re-wait if server does not start within grace period', () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const mcpService = new TestMcpService(); + const channel = new McpGatewayToolBrokerChannel(mcpService, 100); - const server = createNeverStartingServer( - 'collectionA', - 'serverA', - [createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] }))], - ); + const server = createNeverStartingServer( + 'collectionA', + 'serverA', + [createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] }))], + ); - mcpService.servers.set([server], undefined); + mcpService.servers.set([server], undefined); - // First call: waits up to 10 ms, server never starts → empty result. - const tools = await channel.call(undefined, 'listTools'); - assert.deepStrictEqual(tools, []); + // First call: waits up to the grace period, server never starts → empty result. + const tools = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools, []); - // Second call: grace-period promise already resolved; returns immediately without re-waiting. - const tools2 = await channel.call(undefined, 'listTools'); - assert.deepStrictEqual(tools2, []); + // Second call: grace-period promise already resolved; returns immediately without re-waiting. + const tools2 = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools2, []); - channel.dispose(); + channel.dispose(); + }); + }); + + test('invalidates stale grace entry when cacheState regresses to Unknown after timeout', () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const mcpService = new TestMcpService(); + const channel = new McpGatewayToolBrokerChannel(mcpService, 100); + + const server = createNeverStartingServer( + 'collectionA', + 'serverA', + [createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] }))], + ); + + mcpService.servers.set([server], undefined); + + // First call: grace period elapses, server never starts → empty. + const tools1 = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools1, []); + assert.strictEqual(server.startCalls, 1); + + // Simulate a cache reset: server goes back to Unknown. + server.cacheStateValue.set(McpServerCacheState.Unknown, undefined); + + // Make the server succeed this time. + server.startBehavior = 'succeed'; + + // Second call: stale grace entry should be discarded, a new grace race starts, + // and the server successfully starts → tools returned. + const tools2 = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools2.map(t => t.name), ['echo']); + assert.strictEqual(server.startCalls, 2); + + channel.dispose(); + }); + }); + + test('does not invalidate grace entry when cacheState is not Unknown/Outdated', () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const mcpService = new TestMcpService(); + const channel = new McpGatewayToolBrokerChannel(mcpService, 100); + + const server = createServer( + 'collectionA', + 'serverA', + [createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] }))], + McpServerCacheState.Unknown, + ); + + mcpService.servers.set([server], undefined); + + // First call: server starts successfully during grace period. + const tools1 = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools1.map(t => t.name), ['echo']); + assert.strictEqual(server.startCalls, 1); + + // Second call: cacheState is now Live (server started), grace entry should NOT + // be invalidated, so no additional start call is made. + const tools2 = await channel.call(undefined, 'listTools'); + assert.deepStrictEqual(tools2.map(t => t.name), ['echo']); + assert.strictEqual(server.startCalls, 1); + + channel.dispose(); + }); }); }); @@ -227,14 +292,15 @@ function createNeverStartingServer( collectionId: string, definitionId: string, initialTools: readonly IMcpTool[], -): IMcpServer & { startCalls: number } { +): IMcpServer & { startCalls: number; startBehavior: 'hang' | 'succeed'; cacheStateValue: ReturnType> } { const owner = {}; const tools = observableValue(owner, initialTools); const connectionState = observableValue(owner, { state: McpConnectionState.Kind.Running }); const cacheState = observableValue(owner, McpServerCacheState.Unknown); let startCalls = 0; + let startBehavior: 'hang' | 'succeed' = 'hang'; - return { + const result: IMcpServer & { startCalls: number; startBehavior: 'hang' | 'succeed'; cacheStateValue: ReturnType> } = { collection: { id: collectionId, label: collectionId }, definition: { id: definitionId, label: definitionId }, connection: observableValue(owner, undefined), @@ -244,6 +310,10 @@ function createNeverStartingServer( showOutput: async () => { }, start: async () => { startCalls++; + if (result.startBehavior === 'succeed') { + cacheState.set(McpServerCacheState.Live, undefined); + return { state: McpConnectionState.Kind.Running }; + } // Never resolves — simulates a server that hangs on startup. return new Promise(() => { }); }, @@ -256,7 +326,11 @@ function createNeverStartingServer( resourceTemplates: async () => [], dispose: () => { }, get startCalls() { return startCalls; }, + get startBehavior() { return startBehavior; }, + set startBehavior(v) { startBehavior = v; }, + cacheStateValue: cacheState, }; + return result; } function createTool(name: string, call: (params: Record) => Promise, visibility: McpToolVisibility = McpToolVisibility.Model): IMcpTool { From c3d549c2408eb09adad6e8f5ba3ccd114825f77b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 3 Mar 2026 18:01:30 -0500 Subject: [PATCH 101/448] fix chat tip issue (#299070) fix #299036 --- src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index f0bc32db08f..4bd1c8fd3c6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -123,15 +123,11 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ id: 'tip.switchToAuto', tier: ChatTipTier.Foundational, priority: 0, - buildMessage(ctx) { - const label = getCommandLabel('workbench.action.chat.openModelPicker'); - const kb = formatKeybinding(ctx, 'workbench.action.chat.openModelPicker'); + buildMessage(_ctx) { return new MarkdownString( localize( 'tip.switchToAuto', - "Using gpt-4.1? Try switching to [{0}](command:workbench.action.chat.openModelPicker){1} for better coding performance.", - label, - kb + "Using gpt-4.1? Try switching to [Auto](command:workbench.action.chat.openModelPicker) in the model picker for better coding performance." ) ); }, From e848b209318194b4d3b866ad01fc95a9d454de4b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 3 Mar 2026 18:10:42 -0500 Subject: [PATCH 102/448] constrain and make chat questions scrollable (#298786) --- .../chatQuestionCarouselPart.ts | 66 ++++++++++++++++++- .../media/chatQuestionCarousel.css | 47 +++++++++++-- .../contrib/chat/browser/widget/chatWidget.ts | 8 +++ 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 996b73a63f3..c22a7d3391b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -18,6 +18,7 @@ import { IMarkdownRendererService } from '../../../../../../platform/markdown/br import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { InputBox } from '../../../../../../base/browser/ui/inputbox/inputBox.js'; +import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; import { Checkbox } from '../../../../../../base/browser/ui/toggle/toggle.js'; import { IChatQuestion, IChatQuestionCarousel } from '../../../common/chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; @@ -29,6 +30,7 @@ import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidg import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; import './media/chatQuestionCarousel.css'; export interface IChatQuestionCarouselOptions { @@ -63,6 +65,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private readonly _freeformTextareas: Map = new Map(); private readonly _inputBoxes: DisposableStore = this._register(new DisposableStore()); private readonly _questionRenderStore = this._register(new MutableDisposable()); + private _inputScrollable: DomScrollableElement | undefined; /** * Disposable store for interactive UI components (header, nav buttons, etc.) @@ -418,9 +421,45 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._questionTabIndicators.clear(); this._reviewIndex = -1; this._footerRow = undefined; + this._inputScrollable = undefined; this._explicitlyAnsweredQuestionIds.clear(); } + private layoutInputScrollable(inputScrollable: DomScrollableElement): void { + if (!this._questionContainer) { + return; + } + + const scrollableNode = inputScrollable.getDomNode(); + const scrollableContent = scrollableNode.firstElementChild; + if (!dom.isHTMLElement(scrollableContent)) { + return; + } + + // Use the flex-resolved container height (constrained by CSS max-height) + // instead of window.innerHeight, so the scroll viewport tracks actual chat space. + const maxContainerHeight = this._questionContainer.clientHeight; + + const computedStyle = dom.getWindow(this._questionContainer).getComputedStyle(this._questionContainer); + const contentVerticalPadding = + Number.parseFloat(computedStyle.paddingTop || '0') + + Number.parseFloat(computedStyle.paddingBottom || '0'); + + const nonScrollableContentHeight = Array.from(this._questionContainer.children) + .filter(child => child !== scrollableNode) + .reduce((sum, child) => sum + (child as HTMLElement).offsetHeight, 0); + + const availableScrollableHeight = Math.floor(maxContainerHeight - contentVerticalPadding - nonScrollableContentHeight); + const constrainedScrollableHeight = Math.max(0, availableScrollableHeight); + + // Constrain the content element (DomScrollableElement._element) so that + // scanDomNode sees clientHeight < scrollHeight and enables scrolling. + // The wrapper inherits the same constraint via CSS flex. + scrollableContent.style.height = `${constrainedScrollableHeight}px`; + scrollableContent.style.maxHeight = `${constrainedScrollableHeight}px`; + inputScrollable.scanDomNode(); + } + /** * Skips the carousel with default values - called when user wants to proceed quickly. * Returns defaults for all questions. @@ -588,6 +627,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const questionRenderStore = new DisposableStore(); this._questionRenderStore.value = questionRenderStore; + this._inputScrollable = undefined; // Clear previous input boxes and stale references this._inputBoxes.clear(); @@ -706,7 +746,28 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // Render input based on question type const inputContainer = dom.$('.chat-question-input-container'); this.renderInput(inputContainer, question); - this._questionContainer.appendChild(inputContainer); + + const inputScrollable = questionRenderStore.add(new DomScrollableElement(inputContainer, { + vertical: ScrollbarVisibility.Visible, + horizontal: ScrollbarVisibility.Hidden, + consumeMouseWheelIfScrollbarIsNeeded: true, + })); + this._inputScrollable = inputScrollable; + const inputScrollableNode = inputScrollable.getDomNode(); + inputScrollableNode.classList.add('chat-question-input-scrollable'); + this._questionContainer.appendChild(inputScrollableNode); + + const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => this.layoutInputScrollable(inputScrollable))); + questionRenderStore.add(inputResizeObserver.observe(inputScrollableNode)); + questionRenderStore.add(inputResizeObserver.observe(inputContainer)); + questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => this.layoutInputScrollable(inputScrollable))); + this.layoutInputScrollable(inputScrollable); + questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => { + inputContainer.scrollTop = 0; + inputContainer.scrollLeft = 0; + inputScrollable.setScrollPosition({ scrollTop: 0, scrollLeft: 0 }); + inputScrollable.scanDomNode(); + })); } /** @@ -800,6 +861,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const autoResize = () => { textarea.style.height = 'auto'; textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; + if (this._inputScrollable) { + this.layoutInputScrollable(this._inputScrollable); + } this._onDidChangeHeight.fire(); }; this._inputBoxes.add(dom.addDisposableListener(textarea, dom.EventType.INPUT, autoResize)); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 06842836e88..ae28fb9038e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ /* question carousel - this is above edits and todos */ -.interactive-session .interactive-input-part > .chat-question-carousel-widget-container:empty { +.interactive-session .interactive-input-part > .chat-question-carousel-widget-container:empty, +.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container:empty { display: none; } /* input specific styling */ -.interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-container { +.interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-container, +.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-container { margin: 0; border: 1px solid var(--vscode-input-border, transparent); background-color: var(--vscode-chat-list-background); @@ -29,10 +31,12 @@ flex-direction: column; overflow: hidden; container-type: inline-size; + max-height: min(420px, 45vh); } /* input part wrapper */ -.interactive-session .interactive-input-part > .chat-question-carousel-widget-container { +.interactive-session .interactive-input-part > .chat-question-carousel-widget-container, +.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container { width: 100%; position: relative; display: flex; @@ -40,16 +44,30 @@ gap: 8px; } +.interactive-session .interactive-input-part > .chat-question-carousel-widget-container:not(:empty), +.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container:not(:empty) { + margin-top: 8px; +} + +.interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-content, +.interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-content { + flex: 1; + min-height: 0; +} + /* container and header */ .interactive-session .chat-question-carousel-container .chat-question-carousel-content { display: flex; flex-direction: column; + flex: 1; + min-height: 0; background: var(--vscode-chat-list-background); overflow: hidden; .chat-question-header-row { display: flex; flex-direction: column; + flex-shrink: 0; background: var(--vscode-chat-list-background); overflow: hidden; @@ -116,8 +134,10 @@ } } } - .chat-question-message { + flex-shrink: 0; + font-size: var(--vscode-chat-font-size-body-s); + line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; padding: 16px; @@ -143,9 +163,18 @@ .interactive-session .chat-question-carousel-container .chat-question-input-container { display: flex; flex-direction: column; + margin-top: 4px; + padding-right: 14px; padding-bottom: 12px; min-width: 0; + &::after { + content: ''; + display: block; + height: 8px; + flex-shrink: 0; + } + /* some hackiness to get the focus looking right */ .chat-question-list-item:focus, .chat-question-list-item:focus-visible, @@ -304,6 +333,16 @@ } } +.interactive-session .chat-question-carousel-container .chat-question-input-container > * { + flex-shrink: 0; +} + +.interactive-session .chat-question-carousel-container .chat-question-input-scrollable { + flex: 1; + min-height: 0; + overscroll-behavior: contain; +} + /* tab bar for multi-question carousels */ .interactive-session .chat-question-carousel-container .chat-question-tab-bar { display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 39bc719fba5..7c172e77ee9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -690,6 +690,14 @@ export class ChatWidget extends Disposable implements IChatWidget { // Forward scroll events from the parent container margins (outside the max-width area) to the chat list this._register(dom.addDisposableListener(parent, dom.EventType.MOUSE_WHEEL, (e: IMouseWheelEvent) => { + if (e.defaultPrevented) { + return; + } + + if (dom.isAncestor(e.target as Node | null, this.container)) { + return; + } + this.listWidget.delegateScrollFromMouseWheelEvent(e); })); From 1f3ddd891e0ba30b22d35e9ca4d5e624daac5842 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 4 Mar 2026 00:16:37 +0100 Subject: [PATCH 103/448] add play button in prompt config dialog (#297715) --- .../promptSyntax/pickers/promptFilePickers.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index aaf3a9b1340..349bb815934 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -54,6 +54,7 @@ export interface ISelectOptions { readonly optionRename?: boolean; readonly optionCopy?: boolean; readonly optionVisibility?: boolean; + readonly optionRun?: boolean; } export interface ISelectPromptResult { @@ -323,6 +324,11 @@ const MAKE_INVISIBLE_BUTTON: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.eye), }; +const RUN_IN_CHAT_BUTTON: IQuickInputButton = { + tooltip: localize('runInChat', "Run in Chat View"), + iconClass: ThemeIcon.asClassName(Codicon.play), +}; + export class PromptFilePickers { constructor( @IQuickInputService private readonly _quickInputService: IQuickInputService, @@ -425,6 +431,9 @@ export class PromptFilePickers { private async _createPromptPickItems(options: ISelectOptions, token: CancellationToken): Promise<(IPromptPickerQuickPickItem | IQuickPickSeparator)[]> { const buttons: IQuickInputButton[] = []; + if (options.type === PromptsType.prompt && options.optionRun !== false) { + buttons.push(RUN_IN_CHAT_BUTTON); + } if (options.optionEdit !== false) { buttons.push(EDIT_BUTTON); } @@ -481,6 +490,9 @@ export class PromptFilePickers { const exts = (await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.extension, token)).filter(isExtensionPromptPath); if (exts.length) { const extButtons: IQuickInputButton[] = []; + if (options.type === PromptsType.prompt && options.optionRun !== false) { + extButtons.push(RUN_IN_CHAT_BUTTON); + } if (options.optionEdit !== false) { extButtons.push(EDIT_BUTTON); } @@ -613,6 +625,15 @@ export class PromptFilePickers { } const value = item.promptFileUri; + if (button === RUN_IN_CHAT_BUTTON) { + const commandId = quickPick.keyMods.ctrlCmd === true + ? 'workbench.action.chat.run-in-new-chat.prompt.current' + : 'workbench.action.chat.run.prompt.current'; + await this._commandService.executeCommand(commandId, value); + quickPick.hide(); + return false; + } + // `edit` button was pressed, open the prompt file in editor if (button === EDIT_BUTTON) { await this._openerService.open(value); @@ -710,7 +731,8 @@ export class PromptFilePickers { optionDelete: true, optionRename: true, optionCopy: true, - optionVisibility: false + optionVisibility: false, + optionRun: false }; try { From c4672f21b51946b540f93668addbd585ddac55d1 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 15:36:06 -0800 Subject: [PATCH 104/448] fix merge error --- .../mcp/test/common/mcpGatewayToolBrokerChannel.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index 5343a9f83e3..7035bc3aa9b 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -144,7 +144,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('starts server and waits within grace period when cache state is outdated', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer( 'collectionA', @@ -165,7 +165,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('returns empty tools and does not re-wait if server does not start within grace period', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService, 100); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService(), 100); const server = createNeverStartingServer( 'collectionA', @@ -190,7 +190,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('invalidates stale grace entry when cacheState regresses to Unknown after timeout', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService, 100); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService(), 100); const server = createNeverStartingServer( 'collectionA', @@ -224,7 +224,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('does not invalidate grace entry when cacheState is not Unknown/Outdated', () => { return runWithFakedTimers({ useFakeTimers: true }, async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService, 100); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService(), 100); const server = createServer( 'collectionA', From 26b6024286ee4f7a3ce0da548ebdc53c505d5aa1 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 3 Mar 2026 15:43:40 -0800 Subject: [PATCH 105/448] Using merge strategy --- extensions/git/src/commands.ts | 23 +- extensions/github/package.json | 2 +- src/vs/platform/actions/common/actions.ts | 1 + .../browser/applyChangesToParentRepo.ts | 201 ++++++++++++++++ .../browser/applyToParentRepo.contribution.ts | 216 ------------------ .../changesView/browser/changesView.ts | 5 +- .../changesView/browser/media/changesView.css | 24 ++ src/vs/sessions/sessions.desktop.main.ts | 2 +- .../actions/common/menusExtensionPoint.ts | 6 + 9 files changed, 254 insertions(+), 226 deletions(-) create mode 100644 src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts delete mode 100644 src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 010d34e4b01..cb54a17fbae 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1075,6 +1075,22 @@ export class CommandCenter { } } + @command('_git.revParseAbbrevRef') + async revParseAbbrevRef(repositoryPath: string): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + const result = await repo.exec(['rev-parse', '--abbrev-ref', 'HEAD']); + return result.stdout.trim(); + } + + @command('_git.mergeBranch') + async mergeBranch(repositoryPath: string, branch: string): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + const result = await repo.exec(['merge', branch, '--no-edit']); + return result.stdout.trim(); + } + @command('git.init') async init(skipFolderPrompt = false): Promise { let repositoryPath: string | undefined = undefined; @@ -5653,15 +5669,14 @@ export class CommandCenter { options.modal = false; break; default: { - const hint = (err.stderr || err.message || String(err)) + const hint = (err.stderr || err.stdout || err.message || String(err)) .replace(/^error: /mi, '') .replace(/^> husky.*$/mi, '') .split(/[\r\n]/) - .filter((line: string) => !!line) - [0]; + .filter((line: string) => !!line); message = hint - ? l10n.t('Git: {0}', hint) + ? l10n.t('Git: {0}', err.stdout ? hint[hint.length - 1] : hint[0]) : l10n.t('Git error'); break; diff --git a/extensions/github/package.json b/extensions/github/package.json index bce90fe1812..815c6452706 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -183,7 +183,7 @@ "when": "github.hasGitHubRepo && timelineItem =~ /git:file:commit\\b/" } ], - "chat/input/editing/sessionToolbar": [ + "chat/input/editing/sessionApplyActions": [ { "command": "github.createPullRequest", "group": "navigation", diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1b3e9d595c8..f7168ac83af 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -259,6 +259,7 @@ export class MenuId { static readonly ChatModePicker = new MenuId('ChatModePicker'); static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar'); static readonly ChatEditingSessionChangesToolbar = new MenuId('ChatEditingSessionChangesToolbar'); + static readonly ChatEditingSessionApplySubmenu = new MenuId('ChatEditingSessionApplySubmenu'); static readonly ChatEditingEditorContent = new MenuId('ChatEditingEditorContent'); static readonly ChatEditingEditorHunk = new MenuId('ChatEditingEditorHunk'); static readonly ChatEditingDeletedNotebookCell = new MenuId('ChatEditingDeletedNotebookCell'); diff --git a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts new file mode 100644 index 00000000000..5cd8f78d9b7 --- /dev/null +++ b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { toAction } from '../../../../base/common/actions.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; +import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { URI } from '../../../../base/common/uri.js'; + +const hasWorktreeAndRepositoryContextKey = new RawContextKey('agentSessionHasWorktreeAndRepository', false, { + type: 'boolean', + description: localize('agentSessionHasWorktreeAndRepository', "True when the active agent session has both a worktree and a parent repository.") +}); + +const hasAheadCommitsContextKey = new RawContextKey('agentSessionHasAheadCommits', false, { + type: 'boolean', + description: localize('agentSessionHasAheadCommits', "True when the active agent session worktree has commits ahead of its upstream.") +}); + +class ApplyChangesToParentRepoContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'sessions.contrib.applyChangesToParentRepo'; + + private readonly _gitRepoDisposables = this._register(new MutableDisposable()); + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ISessionsManagementService sessionManagementService: ISessionsManagementService, + @IGitService private readonly gitService: IGitService, + ) { + super(); + + const worktreeAndRepoKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService); + const aheadCommitsKey = hasAheadCommitsContextKey.bindTo(contextKeyService); + + this._register(autorun(reader => { + const activeSession = sessionManagementService.activeSession.read(reader); + const hasWorktreeAndRepo = !!activeSession?.worktree && !!activeSession?.repository; + worktreeAndRepoKey.set(hasWorktreeAndRepo); + + this._gitRepoDisposables.clear(); + + if (!hasWorktreeAndRepo || !activeSession?.worktree) { + aheadCommitsKey.set(false); + return; + } + + const repoDisposables = this._gitRepoDisposables.value = new DisposableStore(); + this.gitService.openRepository(activeSession.worktree).then(repository => { + if (repoDisposables.isDisposed || !repository) { + aheadCommitsKey.set(false); + return; + } + repoDisposables.add(autorun(innerReader => { + const state = repository.state.read(innerReader); + const ahead = state.HEAD?.ahead ?? 0; + aheadCommitsKey.set(ahead > 0); + })); + }); + })); + } +} + +class ApplyChangesToParentRepoAction extends Action2 { + static readonly ID = 'chatEditing.applyChangesToParentRepo'; + + constructor() { + super({ + id: ApplyChangesToParentRepoAction.ID, + title: localize2('applyChangesToParentRepo', 'Apply Changes to Parent Repo'), + icon: Codicon.desktopDownload, + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and( + IsSessionsWindowContext, + hasWorktreeAndRepositoryContextKey, + ), + menu: [ + { + id: MenuId.ChatEditingSessionApplySubmenu, + group: 'navigation', + order: 2, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + hasWorktreeAndRepositoryContextKey, + ), + }, + ], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const sessionManagementService = accessor.get(ISessionsManagementService); + const commandService = accessor.get(ICommandService); + const notificationService = accessor.get(INotificationService); + const logService = accessor.get(ILogService); + const openerService = accessor.get(IOpenerService); + const productService = accessor.get(IProductService); + + const activeSession = sessionManagementService.getActiveSession(); + if (!activeSession?.worktree || !activeSession?.repository) { + return; + } + + const worktreeRoot = activeSession.worktree; + const repoRoot = activeSession.repository; + + const openFolderAction = toAction({ + id: 'applyChangesToParentRepo.openFolder', + label: localize('openInVSCode', "Open in VS Code"), + run: () => { + const scheme = productService.quality === 'stable' + ? 'vscode' + : productService.quality === 'exploration' + ? 'vscode-exploration' + : 'vscode-insiders'; + + const params = new URLSearchParams(); + params.set('windowId', '_blank'); + params.set('session', activeSession.resource.toString()); + + openerService.open(URI.from({ + scheme, + authority: Schemas.file, + path: repoRoot.path, + query: params.toString(), + }), { openExternal: true }); + } + }); + + try { + // Get the worktree branch name. Since the worktree and parent repo + // share the same git object store, the parent can directly reference + // this branch for a merge. + const worktreeBranch = await commandService.executeCommand( + '_git.revParseAbbrevRef', + worktreeRoot.fsPath + ); + + if (!worktreeBranch) { + notificationService.notify({ + severity: Severity.Warning, + message: localize('applyChangesNoBranch', "Could not determine worktree branch name."), + }); + return; + } + + // Merge the worktree branch into the parent repo. + // This is idempotent: if already merged, git says "Already up to date." + // If new commits exist, they're brought in. Handles partial applies naturally. + const result = await commandService.executeCommand('_git.mergeBranch', repoRoot.fsPath, worktreeBranch); + if (!result) { + logService.warn('[ApplyChangesToParentRepo] No result from merge command'); + } else { + notificationService.notify({ + severity: Severity.Info, + message: typeof result === 'string' && result.startsWith('Already up to date') + ? localize('alreadyUpToDate', 'Parent repository is up to date with worktree.') + : localize('applyChangesSuccess', 'Applied changes to parent repository.'), + actions: { primary: [openFolderAction] } + }); + } + } catch (err) { + logService.error('[ApplyChangesToParentRepo] Failed to apply changes', err); + notificationService.notify({ + severity: Severity.Warning, + message: localize('applyChangesConflict', "Failed to apply changes to parent repo. The parent repo may have diverged — resolve conflicts manually."), + actions: { primary: [openFolderAction] } + }); + } + } +} + +registerAction2(ApplyChangesToParentRepoAction); +registerWorkbenchContribution2(ApplyChangesToParentRepoContribution.ID, ApplyChangesToParentRepoContribution, WorkbenchPhase.AfterRestored); + +// Register the apply submenu in the session changes toolbar +MenuRegistry.appendMenuItem(MenuId.ChatEditingSessionChangesToolbar, { + submenu: MenuId.ChatEditingSessionApplySubmenu, + title: localize2('applyActions', 'Apply Actions'), + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(IsSessionsWindowContext, ChatContextKeys.hasAgentSessionChanges), +}); diff --git a/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts b/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts deleted file mode 100644 index 47a5b66183a..00000000000 --- a/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts +++ /dev/null @@ -1,216 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { toAction } from '../../../../base/common/actions.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; -import { generateUnifiedDiff } from '../../../../workbench/contrib/chat/browser/chatRepoInfo.js'; -import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; -import { isEqualOrParent, relativePath } from '../../../../base/common/resources.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { URI } from '../../../../base/common/uri.js'; - -/** - * Normalizes a URI to the `file` scheme so that path comparisons work - * even when the source URI uses a different scheme (e.g. `github-remote-file`). - */ -function toFileUri(uri: URI): URI { - return uri.scheme === 'file' ? uri : URI.file(uri.path); -} - -const hasWorktreeAndRepositoryContextKey = new RawContextKey('agentSessionHasWorktreeAndRepository', false, { - type: 'boolean', - description: localize('agentSessionHasWorktreeAndRepository', "True when the active agent session has both a worktree and a parent repository.") -}); - -class ApplyToParentRepoContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'sessions.contrib.applyToParentRepo'; - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ISessionsManagementService sessionManagementService: ISessionsManagementService, - ) { - super(); - - const contextKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService); - - this._register(autorun(reader => { - const activeSession = sessionManagementService.activeSession.read(reader); - const hasWorktreeAndRepo = !!activeSession?.worktree && !!activeSession?.repository; - contextKey.set(hasWorktreeAndRepo); - })); - } -} - -class ApplyToParentRepoAction extends Action2 { - static readonly ID = 'chatEditing.applyToParentRepo'; - - constructor() { - super({ - id: ApplyToParentRepoAction.ID, - title: localize2('applyToParentRepo', 'Apply to Parent Repo'), - icon: Codicon.desktopDownload, - category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(IsSessionsWindowContext, hasWorktreeAndRepositoryContextKey, ChatContextKeys.hasAgentSessionChanges), - menu: [ - { - id: MenuId.ChatEditingSessionChangesToolbar, - group: 'navigation', - order: 4, - when: ContextKeyExpr.and(IsSessionsWindowContext, hasWorktreeAndRepositoryContextKey, ChatContextKeys.hasAgentSessionChanges), - }, - ], - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const sessionManagementService = accessor.get(ISessionsManagementService); - const agentSessionsService = accessor.get(IAgentSessionsService); - const fileService = accessor.get(IFileService); - const notificationService = accessor.get(INotificationService); - const logService = accessor.get(ILogService); - const openerService = accessor.get(IOpenerService); - const productService = accessor.get(IProductService); - const commandService = accessor.get(ICommandService); - - const activeSession = sessionManagementService.getActiveSession(); - if (!activeSession?.worktree || !activeSession?.repository) { - return; - } - - const worktreeRoot = activeSession.worktree; - const repoRoot = activeSession.repository; - - const agentSession = agentSessionsService.getSession(activeSession.resource); - const changes = agentSession?.changes; - if (!changes || !(changes instanceof Array)) { - return; - } - - // Generate a combined unified diff patch from all changes - const patchParts: string[] = []; - let fileCount = 0; - - for (const change of changes) { - try { - const modifiedUri = isIChatSessionFileChange2(change) - ? change.modifiedUri ?? change.uri - : change.modifiedUri; - const isDeletion = isIChatSessionFileChange2(change) - ? change.modifiedUri === undefined - : false; - - const originalUri = change.originalUri; - let relPath: string | undefined; - - if (isDeletion) { - if (originalUri && isEqualOrParent(toFileUri(originalUri), worktreeRoot)) { - relPath = relativePath(worktreeRoot, toFileUri(originalUri)); - } - } else { - if (isEqualOrParent(toFileUri(modifiedUri), worktreeRoot)) { - relPath = relativePath(worktreeRoot, toFileUri(modifiedUri)); - } - } - - if (!relPath) { - continue; - } - - const changeType: 'added' | 'modified' | 'deleted' = isDeletion - ? 'deleted' - : originalUri ? 'modified' : 'added'; - - const diff = await generateUnifiedDiff( - fileService, - relPath, - originalUri, - modifiedUri, - changeType - ); - - if (diff) { - patchParts.push(diff); - fileCount++; - } - } catch (err) { - logService.error('[ApplyToParentRepo] Failed to generate diff for change', err); - } - } - - if (patchParts.length === 0) { - notificationService.notify({ - severity: Severity.Info, - message: localize('applyToParentRepoNoDiffs', "No applicable changes to apply to parent repo."), - }); - return; - } - - const combinedPatch = patchParts.join('\n') + '\n'; - - const openFolderAction = toAction({ - id: 'applyToParentRepo.openFolder', - label: localize('openInVSCode', "Open in VS Code"), - run: () => { - const scheme = productService.quality === 'stable' - ? 'vscode' - : productService.quality === 'exploration' - ? 'vscode-exploration' - : 'vscode-insiders'; - - const params = new URLSearchParams(); - params.set('windowId', '_blank'); - params.set('session', activeSession.resource.toString()); - - openerService.open(URI.from({ - scheme, - authority: Schemas.file, - path: repoRoot.path, - query: params.toString(), - }), { openExternal: true }); - } - }); - - try { - await commandService.executeCommand('_git.applyPatch', repoRoot.fsPath, combinedPatch); - - notificationService.notify({ - severity: Severity.Info, - message: fileCount === 1 - ? localize('applyToParentRepoSuccess1', "Applied 1 file to parent repo.") - : localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", fileCount), - actions: { primary: [openFolderAction] } - }); - } catch (err) { - logService.error('[ApplyToParentRepo] git apply failed', err); - notificationService.notify({ - severity: Severity.Warning, - message: localize('applyToParentRepoConflict', "Failed to apply patch to parent repo. The parent repo may have diverged — resolve conflicts manually."), - actions: { primary: [openFolderAction] } - }); - } - } -} - -registerAction2(ApplyToParentRepoAction); -registerWorkbenchContribution2(ApplyToParentRepoContribution.ID, ApplyToParentRepoContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 47eca5b0933..94cef1215c4 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -576,10 +576,7 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' }; - } - if (action.id === 'chatEditing.applyToParentRepo') { - return { showIcon: true, showLabel: false, isSecondary: true }; + return { showIcon: true, showLabel: true, isSecondary: true }; } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesView.css b/src/vs/sessions/contrib/changesView/browser/media/changesView.css index 1300b886cbc..05c1f461efc 100644 --- a/src/vs/sessions/contrib/changesView/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changesView/browser/media/changesView.css @@ -114,6 +114,29 @@ flex: 1; } +/* ButtonWithDropdown container grows to fill available space */ +.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown { + flex: 1; + display: flex; +} + +.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button { + flex: 1; + box-sizing: border-box; +} + +.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button-dropdown-separator { + flex: 0; +} + +.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button.monaco-dropdown-button { + flex: 0 0 auto; + padding: 4px; + width: auto; + min-width: 0; + border-radius: 0px 4px 4px 0px; +} + .changes-view-body .chat-editing-session-actions.outside-card .monaco-button.secondary.monaco-text-button.codicon { padding: 4px 8px; font-size: 16px !important; @@ -134,6 +157,7 @@ .changes-view-body .chat-editing-session-actions .monaco-button.secondary.monaco-text-button { background-color: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); + border-radius: 4px 0px 0px 4px; } .changes-view-body .chat-editing-session-actions .monaco-button.secondary:hover { diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 406ea03e508..99ac51a5695 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -208,7 +208,7 @@ import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; import './contrib/files/browser/files.contribution.js'; import './contrib/gitSync/browser/gitSync.contribution.js'; -import './contrib/applyToParentRepo/browser/applyToParentRepo.contribution.js'; +import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 7f5c8e64d14..3fb7f02494e 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -479,6 +479,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.chatEditingSessionChangesToolbar', "The Chat Editing widget toolbar menu for session changes."), proposed: 'chatSessionsProvider' }, + { + key: 'chat/input/editing/sessionApplyActions', + id: MenuId.ChatEditingSessionApplySubmenu, + description: localize('menus.chatEditingSessionApplySubmenu', "Submenu for apply actions in the Chat Editing session changes toolbar."), + proposed: 'chatSessionsProvider' + }, { // TODO: rename this to something like: `chatSessions/item/inline` key: 'chat/chatSessions', From bfd958ec2b5863bde0ff5efac50a4fc0be76ba66 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 3 Mar 2026 15:50:29 -0800 Subject: [PATCH 106/448] Rename --- .../browser/applyChangesToParentRepo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts index 5cd8f78d9b7..799d84ea10b 100644 --- a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts +++ b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts @@ -85,7 +85,7 @@ class ApplyChangesToParentRepoAction extends Action2 { constructor() { super({ id: ApplyChangesToParentRepoAction.ID, - title: localize2('applyChangesToParentRepo', 'Apply Changes to Parent Repo'), + title: localize2('applyChangesToParentRepo', 'Apply Changes to Parent Repository'), icon: Codicon.desktopDownload, category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( From 3dfa3a438f9637126754826fd47847b460d93709 Mon Sep 17 00:00:00 2001 From: Robo Date: Wed, 4 Mar 2026 08:57:22 +0900 Subject: [PATCH 107/448] fix: passing js-flags to utility process (#299069) --- src/main.ts | 10 ++++++++-- .../utilityProcess/electron-main/utilityProcess.ts | 6 +++++- .../workbench/electron-browser/desktop.contribution.ts | 4 ++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index ec2e45c31d2..42f599c9b37 100644 --- a/src/main.ts +++ b/src/main.ts @@ -342,7 +342,7 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { app.commandLine.appendSwitch('disable-blink-features', blinkFeaturesToDisable); // Support JS Flags - const jsFlags = getJSFlags(cliArgs); + const jsFlags = getJSFlags(cliArgs, argvConfig); if (jsFlags) { app.commandLine.appendSwitch('js-flags', jsFlags); } @@ -374,6 +374,7 @@ interface IArgvConfig { readonly 'use-inmemory-secretstorage'?: boolean; readonly 'enable-rdp-display-tracking'?: boolean; readonly 'remote-debugging-port'?: string; + readonly 'js-flags'?: string; } function readArgvConfigSync(): IArgvConfig { @@ -537,7 +538,7 @@ function configureCrashReporter(): void { }); } -function getJSFlags(cliArgs: NativeParsedArgs): string | null { +function getJSFlags(cliArgs: NativeParsedArgs, argvConfig: IArgvConfig): string | null { const jsFlags: string[] = []; // Add any existing JS flags we already got from the command line @@ -545,6 +546,11 @@ function getJSFlags(cliArgs: NativeParsedArgs): string | null { jsFlags.push(cliArgs['js-flags']); } + // Add JS flags from runtime arguments (argv.json) + if (typeof argvConfig['js-flags'] === 'string' && argvConfig['js-flags']) { + jsFlags.push(argvConfig['js-flags']); + } + if (process.platform === 'linux') { // Fix cppgc crash on Linux with 16KB page size. // Refs https://issues.chromium.org/issues/378017037 diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index fccddba0156..c1dd46b4c17 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -245,8 +245,12 @@ export class UtilityProcess extends Disposable { const serviceName = `${this.configuration.type}-${this.id}`; const modulePath = FileAccess.asFileUri('bootstrap-fork.js').fsPath; const args = this.configuration.args ?? []; - const execArgv = this.configuration.execArgv ?? []; + const execArgv = [...(this.configuration.execArgv ?? [])]; const allowLoadingUnsignedLibraries = this.configuration.allowLoadingUnsignedLibraries; + const jsFlags = app.commandLine.getSwitchValue('js-flags'); + if (jsFlags) { + execArgv.push(`--js-flags=${jsFlags}`); + } const respondToAuthRequestsFromMainProcess = this.configuration.respondToAuthRequestsFromMainProcess; const stdio = 'pipe'; const env = this.createEnv(configuration); diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index fd09050e533..4c3893ac7ed 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -455,6 +455,10 @@ import product from '../../platform/product/common/product.js'; 'remote-debugging-port': { type: 'string', description: localize('argv.remoteDebuggingPort', "Specifies the port to use for remote debugging.") + }, + 'js-flags': { + type: 'string', + description: localize('argv.jsFlags', "Specifies V8 JavaScript engine flags to pass (e.g. \"--max-old-space-size=4096\"). These flags are applied to the main process, renderer and utility processes.") } } }; From c7d548ad58c653c1a469ddb63296ec251ae09c61 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 3 Mar 2026 16:13:51 -0800 Subject: [PATCH 108/448] Fix /hooks slash command from blocking chat (#299084) --- .../chat/browser/promptSyntax/hookActions.ts | 790 +++++++++--------- 1 file changed, 388 insertions(+), 402 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index db3d6f6acc2..805c977bf06 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -361,12 +361,6 @@ export async function showConfigureHooksQuickPick( const store = new DisposableStore(); const picker = store.add(quickInputService.createQuickPick({ useSeparators: true })); const backButton = quickInputService.backButton; - let suppressHideDispose = false; - store.add(picker.onDidHide(() => { - if (!suppressHideDispose) { - store.dispose(); - } - })); picker.show(); let step = Step.SelectHookType; @@ -380,157 +374,404 @@ export async function showConfigureHooksQuickPick( const stepHistory: Step[] = []; const goBack = (): Step | undefined => stepHistory.pop(); - while (true) { - switch (step) { - case Step.SelectHookType: { - // Step 1: Show lifecycle events with hook counts, filtered by target - const makeItem = ([hookType, meta]: [HookType, IHookTypeMeta]): IHookTypeQuickPickItem => { - const count = hookCountByType.get(hookType) ?? 0; - const countLabel = count > 0 ? ` (${count})` : ''; - return { - label: `${meta.label}${countLabel}`, - description: meta.description, - hookType, - hookTypeMeta: meta + try { + while (true) { + switch (step) { + case Step.SelectHookType: { + // Step 1: Show lifecycle events with hook counts, filtered by target + const makeItem = ([hookType, meta]: [HookType, IHookTypeMeta]): IHookTypeQuickPickItem => { + const count = hookCountByType.get(hookType) ?? 0; + const countLabel = count > 0 ? ` (${count})` : ''; + return { + label: `${meta.label}${countLabel}`, + description: meta.description, + hookType, + hookTypeMeta: meta + }; }; - }; - let pickerItems: (IHookTypeQuickPickItem | IQuickPickSeparator)[]; + let pickerItems: (IHookTypeQuickPickItem | IQuickPickSeparator)[]; - if (options?.target) { - // Filtered to a specific target - const targetHookTypes = new Set(Object.values(HOOKS_BY_TARGET[options.target])); - pickerItems = (Object.entries(HOOK_METADATA) as [HookType, IHookTypeMeta][]) - .filter(([hookType]) => targetHookTypes.has(hookType)) - .map(makeItem); - } else { - // No target: group into Default (shared), VS Code Only, Copilot CLI Only - const vscodeTypes = new Set(Object.values(HOOKS_BY_TARGET[Target.VSCode])); - const copilotTypes = new Set(Object.values(HOOKS_BY_TARGET[Target.GitHubCopilot])); - const allEntries = Object.entries(HOOK_METADATA) as [HookType, IHookTypeMeta][]; + if (options?.target) { + // Filtered to a specific target + const targetHookTypes = new Set(Object.values(HOOKS_BY_TARGET[options.target])); + pickerItems = (Object.entries(HOOK_METADATA) as [HookType, IHookTypeMeta][]) + .filter(([hookType]) => targetHookTypes.has(hookType)) + .map(makeItem); + } else { + // No target: group into Default (shared), VS Code Only, Copilot CLI Only + const vscodeTypes = new Set(Object.values(HOOKS_BY_TARGET[Target.VSCode])); + const copilotTypes = new Set(Object.values(HOOKS_BY_TARGET[Target.GitHubCopilot])); + const allEntries = Object.entries(HOOK_METADATA) as [HookType, IHookTypeMeta][]; - const shared = allEntries.filter(([h]) => vscodeTypes.has(h) && copilotTypes.has(h)); - const vscodeOnly = allEntries.filter(([h]) => vscodeTypes.has(h) && !copilotTypes.has(h)); - const copilotOnly = allEntries.filter(([h]) => !vscodeTypes.has(h) && copilotTypes.has(h)); + const shared = allEntries.filter(([h]) => vscodeTypes.has(h) && copilotTypes.has(h)); + const vscodeOnly = allEntries.filter(([h]) => vscodeTypes.has(h) && !copilotTypes.has(h)); + const copilotOnly = allEntries.filter(([h]) => !vscodeTypes.has(h) && copilotTypes.has(h)); - pickerItems = []; - if (shared.length > 0) { - pickerItems.push({ type: 'separator', label: localize('hookSection.default', "Local/Copilot CLI Agents") }); - pickerItems.push(...shared.map(makeItem)); - } - if (vscodeOnly.length > 0) { - pickerItems.push({ type: 'separator', label: localize('hookSection.vscodeOnly', "Local Agents") }); - pickerItems.push(...vscodeOnly.map(makeItem)); - } - if (copilotOnly.length > 0) { - pickerItems.push({ type: 'separator', label: localize('hookSection.copilotCliOnly', "Copilot CLI Agents") }); - pickerItems.push(...copilotOnly.map(makeItem)); - } - } - - picker.items = pickerItems; - picker.value = ''; - picker.placeholder = localize('commands.hooks.selectEvent.placeholder', 'Select a lifecycle event'); - picker.title = localize('commands.hooks.title', 'Hooks'); - picker.buttons = []; - - const result = await awaitPick(picker, backButton); - - if (!result || result === 'back') { - picker.hide(); - return; - } - - selectedHookType = result; - stepHistory.push(Step.SelectHookType); - step = Step.SelectHook; - break; - } - - case Step.SelectHook: { - // Filter hooks by the selected type - const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType!.hookType); - - // Step 2: Show "Add new hook" + existing hooks of this type - const hookItems: (IHookQuickPickItem | IQuickPickSeparator)[] = []; - - // Add "Add new hook" option at the top - hookItems.push({ - label: `$(plus) ${localize('commands.addNewHook.label', 'Add new hook...')}`, - isAddNewHook: true, - alwaysShow: true - }); - - // Add existing hooks - if (hooksOfType.length > 0) { - hookItems.push({ - type: 'separator', - label: localize('existingHooks', "Existing Hooks") - }); - - for (const entry of hooksOfType) { - const description = labelService.getUriLabel(entry.fileUri, { relative: true }); - hookItems.push({ - label: entry.commandLabel, - description, - hookEntry: entry - }); - } - } - - // Auto-execute if only "Add new hook" is available (no existing hooks) - if (hooksOfType.length === 0) { - selectedHook = hookItems[0] as IHookQuickPickItem; - } else { - picker.items = hookItems; - picker.value = ''; - picker.placeholder = localize('commands.hooks.selectHook.placeholder', 'Select a hook to open or add a new one'); - picker.title = selectedHookType!.hookTypeMeta.label; - picker.buttons = [backButton]; - - const result = await awaitPick(picker, backButton); - - if (result === 'back') { - step = goBack() ?? Step.SelectHookType; - break; - } - if (!result) { - picker.hide(); - return; - } - selectedHook = result; - stepHistory.push(Step.SelectHook); - } - - // Handle clicking on existing hook (focus into command) - if (selectedHook.hookEntry) { - const entry = selectedHook.hookEntry; - let selection: ITextEditorSelection | undefined; - - // Determine the command field name to highlight based on target platform - const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS); - - // Try to find the command field to highlight - if (commandFieldName) { - try { - const content = await fileService.readFile(entry.fileUri); - selection = findHookCommandSelection( - content.value.toString(), - entry.originalHookTypeId, - entry.index, - commandFieldName - ); - } catch { - // Ignore errors and just open without selection + pickerItems = []; + if (shared.length > 0) { + pickerItems.push({ type: 'separator', label: localize('hookSection.default', "Local/Copilot CLI Agents") }); + pickerItems.push(...shared.map(makeItem)); + } + if (vscodeOnly.length > 0) { + pickerItems.push({ type: 'separator', label: localize('hookSection.vscodeOnly', "Local Agents") }); + pickerItems.push(...vscodeOnly.map(makeItem)); + } + if (copilotOnly.length > 0) { + pickerItems.push({ type: 'separator', label: localize('hookSection.copilotCliOnly', "Copilot CLI Agents") }); + pickerItems.push(...copilotOnly.map(makeItem)); } } + picker.items = pickerItems; + picker.value = ''; + picker.placeholder = localize('commands.hooks.selectEvent.placeholder', 'Select a lifecycle event'); + picker.title = localize('commands.hooks.title', 'Hooks'); + picker.buttons = []; + + const result = await awaitPick(picker, backButton); + + if (!result || result === 'back') { + return; + } + + selectedHookType = result; + stepHistory.push(Step.SelectHookType); + step = Step.SelectHook; + break; + } + + case Step.SelectHook: { + // Filter hooks by the selected type + const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType!.hookType); + + // Step 2: Show "Add new hook" + existing hooks of this type + const hookItems: (IHookQuickPickItem | IQuickPickSeparator)[] = []; + + // Add "Add new hook" option at the top + hookItems.push({ + label: `$(plus) ${localize('commands.addNewHook.label', 'Add new hook...')}`, + isAddNewHook: true, + alwaysShow: true + }); + + // Add existing hooks + if (hooksOfType.length > 0) { + hookItems.push({ + type: 'separator', + label: localize('existingHooks', "Existing Hooks") + }); + + for (const entry of hooksOfType) { + const description = labelService.getUriLabel(entry.fileUri, { relative: true }); + hookItems.push({ + label: entry.commandLabel, + description, + hookEntry: entry + }); + } + } + + // Auto-execute if only "Add new hook" is available (no existing hooks) + if (hooksOfType.length === 0) { + selectedHook = hookItems[0] as IHookQuickPickItem; + } else { + picker.items = hookItems; + picker.value = ''; + picker.placeholder = localize('commands.hooks.selectHook.placeholder', 'Select a hook to open or add a new one'); + picker.title = selectedHookType!.hookTypeMeta.label; + picker.buttons = [backButton]; + + const result = await awaitPick(picker, backButton); + + if (result === 'back') { + step = goBack() ?? Step.SelectHookType; + break; + } + if (!result) { + return; + } + selectedHook = result; + stepHistory.push(Step.SelectHook); + } + + // Handle clicking on existing hook (focus into command) + if (selectedHook.hookEntry) { + const entry = selectedHook.hookEntry; + let selection: ITextEditorSelection | undefined; + + // Determine the command field name to highlight based on target platform + const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS); + + // Try to find the command field to highlight + if (commandFieldName) { + try { + const content = await fileService.readFile(entry.fileUri); + selection = findHookCommandSelection( + content.value.toString(), + entry.originalHookTypeId, + entry.index, + commandFieldName + ); + } catch { + // Ignore errors and just open without selection + } + } + + if (options?.openEditor) { + await options.openEditor(entry.fileUri, { selection }); + } else { + await editorService.openEditor({ + resource: entry.fileUri, + options: { + selection, + pinned: false + } + }); + } + return; + } + + // "Add new hook" was selected + step = Step.SelectFile; + break; + } + + case Step.SelectFile: { + // Step 3: Handle "Add new hook" - show create new file + existing hook files + // Get existing hook files (local storage only, not User Data) + const hookFiles = await promptsService.listPromptFilesForStorage(PromptsType.hook, PromptsStorage.local, CancellationToken.None); + + const fileItems: (IHookFileQuickPickItem | IQuickPickSeparator)[] = []; + + // Add "Create new hook config file" option at the top + fileItems.push({ + label: `$(new-file) ${localize('commands.createNewHookFile.label', 'Create new hook config file...')}`, + isCreateNewFile: true, + alwaysShow: true + }); + + // Add existing hook files + if (hookFiles.length > 0) { + fileItems.push({ + type: 'separator', + label: localize('existingHookFiles', "Existing Hook Files") + }); + + for (const hookFile of hookFiles) { + const relativePath = labelService.getUriLabel(hookFile.uri, { relative: true }); + fileItems.push({ + label: relativePath, + fileUri: hookFile.uri + }); + } + } + + // Auto-execute if no existing hook files + if (hookFiles.length === 0) { + selectedFile = fileItems[0] as IHookFileQuickPickItem; + } else { + picker.items = fileItems; + picker.value = ''; + picker.placeholder = localize('commands.hooks.selectFile.placeholder', 'Select a hook file or create a new one'); + picker.title = localize('commands.hooks.addHook.title', 'Add Hook'); + picker.buttons = [backButton]; + + const result = await awaitPick(picker, backButton); + + if (result === 'back') { + step = goBack() ?? Step.SelectHook; + break; + } + if (!result) { + return; + } + selectedFile = result; + stepHistory.push(Step.SelectFile); + } + + // Handle adding hook to existing file + if (selectedFile.fileUri) { + await addHookToFile( + selectedFile.fileUri, + selectedHookType!.hookType, + fileService, + editorService, + notificationService, + bulkEditService, + options?.openEditor, + ); + return; + } + + // "Create new hook config file" was selected + step = Step.SelectFolder; + break; + } + + case Step.SelectFolder: { + // Get source folders for hooks + const allFolders = await promptsService.getSourceFolders(PromptsType.hook); + const localFolders = allFolders.filter(f => f.storage === PromptsStorage.local); + + if (localFolders.length === 0) { + notificationService.error(localize('commands.hook.noLocalFolders', "Please open a workspace folder to configure hooks.")); + return; + } + + // Auto-select if only one folder, otherwise show picker + selectedFolder = localFolders[0]; + if (localFolders.length > 1) { + const folderItems = localFolders.map(folder => ({ + label: labelService.getUriLabel(folder.uri, { relative: true }), + folder + })); + + picker.items = folderItems; + picker.value = ''; + picker.placeholder = localize('commands.hook.selectFolder.placeholder', 'Select a location for the hook file'); + picker.title = localize('commands.hook.selectFolder.title', 'Hook File Location'); + picker.buttons = [backButton]; + + const result = await awaitPick(picker, backButton); + + if (result === 'back') { + step = goBack() ?? Step.SelectFile; + break; + } + if (!result) { + return; + } + selectedFolder = result.folder; + stepHistory.push(Step.SelectFolder); + } + + step = Step.EnterFilename; + break; + } + + case Step.EnterFilename: { + // Hide the picker and show an input box for the filename picker.hide(); + + const fileNameResult = await new Promise(resolve => { + let resolved = false; + const done = (value: string | 'back' | undefined) => { + if (!resolved) { + resolved = true; + inputDisposables.dispose(); + resolve(value); + } + }; + const inputDisposables = new DisposableStore(); + const inputBox = inputDisposables.add(quickInputService.createInputBox()); + inputBox.prompt = localize('commands.hook.filename.prompt', "Enter hook file name"); + inputBox.placeholder = localize('commands.hook.filename.placeholder', "e.g., hooks, diagnostics, security"); + inputBox.title = localize('commands.hook.filename.title', "Hook File Name"); + inputBox.buttons = [backButton]; + inputBox.ignoreFocusOut = true; + + inputDisposables.add(inputBox.onDidAccept(async () => { + const value = inputBox.value; + if (!value || !value.trim()) { + inputBox.validationMessage = localize('commands.hook.filename.required', "File name is required"); + return; + } + const name = value.trim(); + if (/[/\\:*?"<>|]/.test(name)) { + inputBox.validationMessage = localize('commands.hook.filename.invalidChars', "File name contains invalid characters"); + return; + } + done(name); + })); + inputDisposables.add(inputBox.onDidChangeValue(() => { + inputBox.validationMessage = undefined; + })); + inputDisposables.add(inputBox.onDidTriggerButton(button => { + if (button === backButton) { + done('back'); + } + })); + inputDisposables.add(inputBox.onDidHide(() => { + done(undefined); + })); + inputBox.show(); + }); + + if (fileNameResult === 'back') { + // Re-show the picker for the previous step + picker.show(); + step = goBack() ?? Step.SelectFolder; + break; + } + if (!fileNameResult) { + return; + } + + // Create the hooks folder if it doesn't exist + await fileService.createFolder(selectedFolder!.uri); + + // Use user-provided filename with .json extension + const hookFileName = fileNameResult.endsWith('.json') ? fileNameResult : `${fileNameResult}.json`; + const hookFileUri = URI.joinPath(selectedFolder!.uri, hookFileName); + + // Check if file already exists + if (await fileService.exists(hookFileUri)) { + // File exists - add hook to it instead of creating new + await addHookToFile( + hookFileUri, + selectedHookType!.hookType, + fileService, + editorService, + notificationService, + bulkEditService, + options?.openEditor, + ); + return; + } + + // Detect if new file is a Claude hooks file based on its path + const newFileFormat = getHookSourceFormat(hookFileUri); + const isClaudeNewFile = newFileFormat === HookSourceFormat.Claude; + const isCopilotCliOnly = !isClaudeNewFile + && !new Set(Object.values(HOOKS_BY_TARGET[Target.VSCode])).has(selectedHookType!.hookType) + && new Set(Object.values(HOOKS_BY_TARGET[Target.GitHubCopilot])).has(selectedHookType!.hookType); + const hookTypeKey = isClaudeNewFile + ? (getClaudeHookTypeName(selectedHookType!.hookType) ?? selectedHookType!.hookType) + : isCopilotCliOnly + ? (getCopilotCliHookTypeName(selectedHookType!.hookType) ?? selectedHookType!.hookType) + : selectedHookType!.hookType; + const newFileHookEntry = isCopilotCliOnly + ? { type: 'command', [targetOS === OperatingSystem.Windows ? 'powershell' : 'bash']: '' } + : buildNewHookEntry(newFileFormat); + const commandFieldKey = isCopilotCliOnly + ? (targetOS === OperatingSystem.Windows ? 'powershell' : 'bash') + : 'command'; + + // Create new hook file with the selected hook type + const hooksContent: Record = { + ...(isCopilotCliOnly ? { version: 1 } : {}), + hooks: { + [hookTypeKey]: [ + newFileHookEntry + ] + } + }; + + const jsonContent = JSON.stringify(hooksContent, null, '\t'); + await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); + + options?.onHookFileCreated?.(hookFileUri); + + // Find the selection for the new hook's command field + const selection = findHookCommandSelection(jsonContent, hookTypeKey, 0, commandFieldKey); + + // Open editor with selection if (options?.openEditor) { - await options.openEditor(entry.fileUri, { selection }); + await options.openEditor(hookFileUri, { selection }); } else { await editorService.openEditor({ - resource: entry.fileUri, + resource: hookFileUri, options: { selection, pinned: false @@ -539,265 +780,10 @@ export async function showConfigureHooksQuickPick( } return; } - - // "Add new hook" was selected - step = Step.SelectFile; - break; - } - - case Step.SelectFile: { - // Step 3: Handle "Add new hook" - show create new file + existing hook files - // Get existing hook files (local storage only, not User Data) - const hookFiles = await promptsService.listPromptFilesForStorage(PromptsType.hook, PromptsStorage.local, CancellationToken.None); - - const fileItems: (IHookFileQuickPickItem | IQuickPickSeparator)[] = []; - - // Add "Create new hook config file" option at the top - fileItems.push({ - label: `$(new-file) ${localize('commands.createNewHookFile.label', 'Create new hook config file...')}`, - isCreateNewFile: true, - alwaysShow: true - }); - - // Add existing hook files - if (hookFiles.length > 0) { - fileItems.push({ - type: 'separator', - label: localize('existingHookFiles', "Existing Hook Files") - }); - - for (const hookFile of hookFiles) { - const relativePath = labelService.getUriLabel(hookFile.uri, { relative: true }); - fileItems.push({ - label: relativePath, - fileUri: hookFile.uri - }); - } - } - - // Auto-execute if no existing hook files - if (hookFiles.length === 0) { - selectedFile = fileItems[0] as IHookFileQuickPickItem; - } else { - picker.items = fileItems; - picker.value = ''; - picker.placeholder = localize('commands.hooks.selectFile.placeholder', 'Select a hook file or create a new one'); - picker.title = localize('commands.hooks.addHook.title', 'Add Hook'); - picker.buttons = [backButton]; - - const result = await awaitPick(picker, backButton); - - if (result === 'back') { - step = goBack() ?? Step.SelectHook; - break; - } - if (!result) { - picker.hide(); - return; - } - selectedFile = result; - stepHistory.push(Step.SelectFile); - } - - // Handle adding hook to existing file - if (selectedFile.fileUri) { - picker.hide(); - await addHookToFile( - selectedFile.fileUri, - selectedHookType!.hookType, - fileService, - editorService, - notificationService, - bulkEditService, - options?.openEditor, - ); - return; - } - - // "Create new hook config file" was selected - step = Step.SelectFolder; - break; - } - - case Step.SelectFolder: { - // Get source folders for hooks - const allFolders = await promptsService.getSourceFolders(PromptsType.hook); - const localFolders = allFolders.filter(f => f.storage === PromptsStorage.local); - - if (localFolders.length === 0) { - picker.hide(); - notificationService.error(localize('commands.hook.noLocalFolders', "Please open a workspace folder to configure hooks.")); - return; - } - - // Auto-select if only one folder, otherwise show picker - selectedFolder = localFolders[0]; - if (localFolders.length > 1) { - const folderItems = localFolders.map(folder => ({ - label: labelService.getUriLabel(folder.uri, { relative: true }), - folder - })); - - picker.items = folderItems; - picker.value = ''; - picker.placeholder = localize('commands.hook.selectFolder.placeholder', 'Select a location for the hook file'); - picker.title = localize('commands.hook.selectFolder.title', 'Hook File Location'); - picker.buttons = [backButton]; - - const result = await awaitPick(picker, backButton); - - if (result === 'back') { - step = goBack() ?? Step.SelectFile; - break; - } - if (!result) { - picker.hide(); - return; - } - selectedFolder = result.folder; - stepHistory.push(Step.SelectFolder); - } - - step = Step.EnterFilename; - break; - } - - case Step.EnterFilename: { - // Hide the picker and show an input box for the filename - suppressHideDispose = true; - picker.hide(); - suppressHideDispose = false; - - const fileNameResult = await new Promise(resolve => { - let resolved = false; - const done = (value: string | 'back' | undefined) => { - if (!resolved) { - resolved = true; - inputDisposables.dispose(); - resolve(value); - } - }; - const inputDisposables = new DisposableStore(); - const inputBox = inputDisposables.add(quickInputService.createInputBox()); - inputBox.prompt = localize('commands.hook.filename.prompt', "Enter hook file name"); - inputBox.placeholder = localize('commands.hook.filename.placeholder', "e.g., hooks, diagnostics, security"); - inputBox.title = localize('commands.hook.filename.title', "Hook File Name"); - inputBox.buttons = [backButton]; - inputBox.ignoreFocusOut = true; - - inputDisposables.add(inputBox.onDidAccept(async () => { - const value = inputBox.value; - if (!value || !value.trim()) { - inputBox.validationMessage = localize('commands.hook.filename.required', "File name is required"); - return; - } - const name = value.trim(); - if (/[/\\:*?"<>|]/.test(name)) { - inputBox.validationMessage = localize('commands.hook.filename.invalidChars', "File name contains invalid characters"); - return; - } - done(name); - })); - inputDisposables.add(inputBox.onDidChangeValue(() => { - inputBox.validationMessage = undefined; - })); - inputDisposables.add(inputBox.onDidTriggerButton(button => { - if (button === backButton) { - done('back'); - } - })); - inputDisposables.add(inputBox.onDidHide(() => { - done(undefined); - })); - inputBox.show(); - }); - - if (fileNameResult === 'back') { - // Re-show the picker for the previous step - picker.show(); - step = goBack() ?? Step.SelectFolder; - break; - } - if (!fileNameResult) { - store.dispose(); - return; - } - - // Create the hooks folder if it doesn't exist - await fileService.createFolder(selectedFolder!.uri); - - // Use user-provided filename with .json extension - const hookFileName = fileNameResult.endsWith('.json') ? fileNameResult : `${fileNameResult}.json`; - const hookFileUri = URI.joinPath(selectedFolder!.uri, hookFileName); - - // Check if file already exists - if (await fileService.exists(hookFileUri)) { - // File exists - add hook to it instead of creating new - store.dispose(); - await addHookToFile( - hookFileUri, - selectedHookType!.hookType, - fileService, - editorService, - notificationService, - bulkEditService, - options?.openEditor, - ); - return; - } - - // Detect if new file is a Claude hooks file based on its path - const newFileFormat = getHookSourceFormat(hookFileUri); - const isClaudeNewFile = newFileFormat === HookSourceFormat.Claude; - const isCopilotCliOnly = !isClaudeNewFile - && !new Set(Object.values(HOOKS_BY_TARGET[Target.VSCode])).has(selectedHookType!.hookType) - && new Set(Object.values(HOOKS_BY_TARGET[Target.GitHubCopilot])).has(selectedHookType!.hookType); - const hookTypeKey = isClaudeNewFile - ? (getClaudeHookTypeName(selectedHookType!.hookType) ?? selectedHookType!.hookType) - : isCopilotCliOnly - ? (getCopilotCliHookTypeName(selectedHookType!.hookType) ?? selectedHookType!.hookType) - : selectedHookType!.hookType; - const newFileHookEntry = isCopilotCliOnly - ? { type: 'command', [targetOS === OperatingSystem.Windows ? 'powershell' : 'bash']: '' } - : buildNewHookEntry(newFileFormat); - const commandFieldKey = isCopilotCliOnly - ? (targetOS === OperatingSystem.Windows ? 'powershell' : 'bash') - : 'command'; - - // Create new hook file with the selected hook type - const hooksContent: Record = { - ...(isCopilotCliOnly ? { version: 1 } : {}), - hooks: { - [hookTypeKey]: [ - newFileHookEntry - ] - } - }; - - const jsonContent = JSON.stringify(hooksContent, null, '\t'); - await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); - - options?.onHookFileCreated?.(hookFileUri); - - // Find the selection for the new hook's command field - const selection = findHookCommandSelection(jsonContent, hookTypeKey, 0, commandFieldKey); - - // Open editor with selection - store.dispose(); - if (options?.openEditor) { - await options.openEditor(hookFileUri, { selection }); - } else { - await editorService.openEditor({ - resource: hookFileUri, - options: { - selection, - pinned: false - } - }); - } - return; } } + } finally { + store.dispose(); } } From fd97f699a20cab44505a76b7d0437dbe552863ea Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Mar 2026 11:53:42 +1100 Subject: [PATCH 109/448] Disable slash commands for background (#299085) --- .../contrib/chat/browser/chatSlashCommands.ts | 21 +++++++++++------ .../input/editor/chatInputCompletions.ts | 23 +++++++++++++++---- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index d1cc0bba474..3570a436c48 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -69,7 +69,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_hooks', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await instantiationService.invokeFunction(showConfigureHooksQuickPick); })); @@ -79,7 +80,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_models', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(OpenModelPickerAction.ID); })); @@ -100,7 +102,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_plugins', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(ManagePluginsAction.ID); })); @@ -122,7 +125,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_agents', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(OpenModePickerAction.ID); })); @@ -132,7 +136,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_skills', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(CONFIGURE_SKILLS_ACTION_ID); })); @@ -142,7 +147,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_instructions', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(CONFIGURE_INSTRUCTIONS_ACTION_ID); })); @@ -152,7 +158,8 @@ export class ChatSlashCommandsContribution extends Disposable { sortText: 'z3_prompts', executeImmediately: true, silent: true, - locations: [ChatAgentLocation.Chat] + locations: [ChatAgentLocation.Chat], + target: Target.VSCode }, async () => { await commandService.executeCommand(CONFIGURE_PROMPTS_ACTION_ID); })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index 0452f785a63..e82e12830d5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -55,12 +55,16 @@ import { ChatAgentLocation, ChatModeKind, isSupportedChatFileScheme } from '../. import { isToolSet } from '../../../../common/tools/languageModelToolsService.js'; import { IChatSessionsService } from '../../../../common/chatSessionsService.js'; import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; -import { Target } from '../../../../common/promptSyntax/promptTypes.js'; +import { + PromptsType, + Target +} from '../../../../common/promptSyntax/promptTypes.js'; import { ChatSubmitAction, IChatExecuteActionContext } from '../../../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../../../chat.js'; import { resizeImage } from '../../../chatImageUtils.js'; import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; +import { getPromptFileType } from '../../../../common/promptSyntax/config/promptFileLocations.js'; /** * Regex matching a slash command word (e.g. `/foo`). Uses `\p{L}` for Unicode @@ -239,9 +243,20 @@ class SlashCommandCompletions extends Disposable { // Filter out commands that are not user-invocable (hidden from / menu) const userInvocableCommands = promptCommands .filter(c => { - // Exclude extension-provided prompt files for locked agents. - if (widget.lockedAgentId && c.promptPath.extension) { - return false; + if (widget.lockedAgentId) { + // Exclude extension-provided prompt files for locked agents. + if (c.promptPath.extension) { + return false; + } + // Exclude hooks as those don't work in locked agent scenarios. + try { + const promptType = getPromptFileType(c.promptPath.uri); + if (promptType && promptType === PromptsType.hook) { + return false; + } + } catch { + + } } return true; }) From f7d450f8a30a49b27f7672f132aa3393e7cd108a Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:55:09 -0800 Subject: [PATCH 110/448] refactor: update getCliUserSubfolder to clarify prompts exclusion (#299089) --- src/vs/sessions/contrib/chat/browser/promptsService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/promptsService.ts b/src/vs/sessions/contrib/chat/browser/promptsService.ts index 3a7af9380b2..f226fd20410 100644 --- a/src/vs/sessions/contrib/chat/browser/promptsService.ts +++ b/src/vs/sessions/contrib/chat/browser/promptsService.ts @@ -124,13 +124,15 @@ class AgenticPromptFilesLocator extends PromptFilesLocator { /** * Returns the subfolder name under ~/.copilot/ for a given customization type. * Used to determine the CLI-accessible user creation target. + * + * Prompts are a VS Code concept and use the standard profile promptsHome, + * so they are intentionally excluded here. */ function getCliUserSubfolder(type: PromptsType): string | undefined { switch (type) { case PromptsType.instructions: return 'instructions'; case PromptsType.skill: return 'skills'; case PromptsType.agent: return 'agents'; - case PromptsType.prompt: return 'prompts'; default: return undefined; } } From 0b270f19fa461fbb2af4cabf47a6803e63cbfd14 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 3 Mar 2026 17:05:30 -0800 Subject: [PATCH 111/448] Don't show Used n references when opening an old session (#299092) * Don't show Used n references when opening an old session Alternate fix for #297152 * Don't show Used n references when opening an old session Alternate fix for #297152 --- .../workbench/contrib/chat/browser/widget/chatListRenderer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 8a52ad08d8b..b559869616f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1777,7 +1777,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer other.kind === content.kind); } return this.renderContentReferencesListData(content, undefined, context, templateData); From 56ca891d2fcacfc849345bc44dc364219f99ada2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 3 Mar 2026 17:31:49 -0800 Subject: [PATCH 112/448] api: fix memory leaks in MainThreadManagedSockets (#299093) * api: fix memory leaks in MainThreadManagedSockets Refactors MainThreadManagedSockets to properly manage disposables and prevent memory leaks. Uses DisposableMap for registrations and DisposableStore to collect socket disposal listeners. - Changes _registrations from Map to DisposableMap to leverage automatic disposal when clearing entries. - Collects Event.once listeners for socket disposal in a DisposableStore to ensure they are properly disposed of when the factory is unregistered. - Minor whitespace fix on closeRemote method signature. Refs https://github.com/microsoft/vscode/issues/293200 (Commit message generated by Copilot) * Better --- .../api/browser/mainThreadManagedSockets.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadManagedSockets.ts b/src/vs/workbench/api/browser/mainThreadManagedSockets.ts index 8bd8f02136a..7b22545502e 100644 --- a/src/vs/workbench/api/browser/mainThreadManagedSockets.ts +++ b/src/vs/workbench/api/browser/mainThreadManagedSockets.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from '../../../base/common/buffer.js'; -import { Emitter } from '../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js'; import { ISocket, SocketCloseEventType } from '../../../base/parts/ipc/common/ipc.net.js'; import { ManagedSocket, RemoteSocketHalf, connectManagedSocket } from '../../../platform/remote/common/managedSocket.js'; import { ManagedRemoteConnection, RemoteConnectionType } from '../../../platform/remote/common/remoteAuthorityResolver.js'; import { IRemoteSocketFactoryService, ISocketFactory } from '../../../platform/remote/common/remoteSocketFactoryService.js'; -import { ExtHostContext, ExtHostManagedSocketsShape, MainContext, MainThreadManagedSocketsShape } from '../common/extHost.protocol.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostContext, ExtHostManagedSocketsShape, MainContext, MainThreadManagedSocketsShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadManagedSockets) export class MainThreadManagedSockets extends Disposable implements MainThreadManagedSocketsShape { private readonly _proxy: ExtHostManagedSocketsShape; - private readonly _registrations = new Map(); + private readonly _registrations = this._register(new DisposableMap()); private readonly _remoteSockets = new Map(); constructor( @@ -30,6 +30,7 @@ export class MainThreadManagedSockets extends Disposable implements MainThreadMa async $registerSocketFactory(socketFactoryId: number): Promise { const that = this; + const store = new DisposableStore(); const socketFactory = new class implements ISocketFactory { supports(connectTo: ManagedRemoteConnection): boolean { @@ -54,7 +55,7 @@ export class MainThreadManagedSockets extends Disposable implements MainThreadMa MainThreadManagedSocket.connect(socketId, that._proxy, path, query, debugLabel, half) .then( socket => { - socket.onDidDispose(() => that._remoteSockets.delete(socketId)); + store.add(Event.once(socket.onDidDispose)(() => that._remoteSockets.delete(socketId))); resolve(socket); }, err => { @@ -65,12 +66,13 @@ export class MainThreadManagedSockets extends Disposable implements MainThreadMa }); } }; - this._registrations.set(socketFactoryId, this._remoteSocketFactoryService.register(RemoteConnectionType.Managed, socketFactory)); + store.add(this._remoteSocketFactoryService.register(RemoteConnectionType.Managed, socketFactory)); + this._registrations.set(socketFactoryId, store); } async $unregisterSocketFactory(socketFactoryId: number): Promise { - this._registrations.get(socketFactoryId)?.dispose(); + this._registrations.deleteAndDispose(socketFactoryId); } $onDidManagedSocketHaveData(socketId: number, data: VSBuffer): void { @@ -115,7 +117,7 @@ export class MainThreadManagedSocket extends ManagedSocket { this.proxy.$remoteSocketWrite(this.socketId, buffer); } - protected override closeRemote(): void { + protected override closeRemote(): void { this.proxy.$remoteSocketEnd(this.socketId); } From 55dc53f5d33ca2287ef0866384588293b082acb7 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 3 Mar 2026 17:45:30 -0800 Subject: [PATCH 113/448] Review comments --- extensions/git/src/commands.ts | 6 ++-- .../browser/applyChangesToParentRepo.ts | 32 +------------------ .../changesView/browser/media/changesView.css | 3 ++ 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index cb54a17fbae..f6af6942efd 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5669,14 +5669,14 @@ export class CommandCenter { options.modal = false; break; default: { - const hint = (err.stderr || err.stdout || err.message || String(err)) + const hintLines = (err.stderr || err.stdout || err.message || String(err)) .replace(/^error: /mi, '') .replace(/^> husky.*$/mi, '') .split(/[\r\n]/) .filter((line: string) => !!line); - message = hint - ? l10n.t('Git: {0}', err.stdout ? hint[hint.length - 1] : hint[0]) + message = hintLines.length > 0 + ? l10n.t('Git: {0}', err.stdout ? hintLines[hintLines.length - 1] : hintLines[0]) : l10n.t('Git error'); break; diff --git a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts index 799d84ea10b..60bbbed6769 100644 --- a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts +++ b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { toAction } from '../../../../base/common/actions.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { autorun } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -20,7 +20,6 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { URI } from '../../../../base/common/uri.js'; @@ -30,51 +29,22 @@ const hasWorktreeAndRepositoryContextKey = new RawContextKey('agentSess description: localize('agentSessionHasWorktreeAndRepository', "True when the active agent session has both a worktree and a parent repository.") }); -const hasAheadCommitsContextKey = new RawContextKey('agentSessionHasAheadCommits', false, { - type: 'boolean', - description: localize('agentSessionHasAheadCommits', "True when the active agent session worktree has commits ahead of its upstream.") -}); - class ApplyChangesToParentRepoContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'sessions.contrib.applyChangesToParentRepo'; - private readonly _gitRepoDisposables = this._register(new MutableDisposable()); - constructor( @IContextKeyService contextKeyService: IContextKeyService, @ISessionsManagementService sessionManagementService: ISessionsManagementService, - @IGitService private readonly gitService: IGitService, ) { super(); const worktreeAndRepoKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService); - const aheadCommitsKey = hasAheadCommitsContextKey.bindTo(contextKeyService); this._register(autorun(reader => { const activeSession = sessionManagementService.activeSession.read(reader); const hasWorktreeAndRepo = !!activeSession?.worktree && !!activeSession?.repository; worktreeAndRepoKey.set(hasWorktreeAndRepo); - - this._gitRepoDisposables.clear(); - - if (!hasWorktreeAndRepo || !activeSession?.worktree) { - aheadCommitsKey.set(false); - return; - } - - const repoDisposables = this._gitRepoDisposables.value = new DisposableStore(); - this.gitService.openRepository(activeSession.worktree).then(repository => { - if (repoDisposables.isDisposed || !repository) { - aheadCommitsKey.set(false); - return; - } - repoDisposables.add(autorun(innerReader => { - const state = repository.state.read(innerReader); - const ahead = state.HEAD?.ahead ?? 0; - aheadCommitsKey.set(ahead > 0); - })); - }); })); } } diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesView.css b/src/vs/sessions/contrib/changesView/browser/media/changesView.css index 05c1f461efc..4f6d74f525c 100644 --- a/src/vs/sessions/contrib/changesView/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changesView/browser/media/changesView.css @@ -157,6 +157,9 @@ .changes-view-body .chat-editing-session-actions .monaco-button.secondary.monaco-text-button { background-color: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); +} + +.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button.secondary.monaco-text-button { border-radius: 4px 0px 0px 4px; } From 68da1933d02e01101ac5307149c6cf5d8111880d Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 3 Mar 2026 17:55:47 -0800 Subject: [PATCH 114/448] Clean up --- extensions/git/src/commands.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f6af6942efd..7ca7c241cdf 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1061,20 +1061,6 @@ export class CommandCenter { await repo.pull(); } - @command('_git.applyPatch') - async applyPatch(repositoryPath: string, patchContent: string): Promise { - const dotGit = await this.git.getRepositoryDotGit(repositoryPath); - const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); - const patchPath = path.join(os.tmpdir(), `vscode-patch-${Date.now()}.patch`); - const { promises: fsp } = await import('fs'); - try { - await fsp.writeFile(patchPath, patchContent, 'utf8'); - await repo.apply(patchPath, { threeWay: true }); - } finally { - await fsp.unlink(patchPath).catch(() => { }); - } - } - @command('_git.revParseAbbrevRef') async revParseAbbrevRef(repositoryPath: string): Promise { const dotGit = await this.git.getRepositoryDotGit(repositoryPath); From d4ab06fee0bc7915086e4e77cb4efb2e97757f2f Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:38:45 -0800 Subject: [PATCH 115/448] Commit customization files to main repo for worktree persistence (#299094) * Commit customization files to main repo for worktree persistence Customization files (agents, skills, instructions, prompts, hooks) are now always committed to the main repository so they persist across worktrees. When a worktree session is active, the file is also copied and committed there so the running session picks it up immediately. - Rewrite SessionsAICustomizationWorkspaceService.commitFiles() with dual-commit logic (main repo + worktree) - Add deleteFiles() to IAICustomizationWorkspaceService interface - Wire delete action to commit removals to git - Show friendly warning when main repo commit fails from a worktree * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../aiCustomizationWorkspaceService.ts | 162 +++++++++++++++++- .../aiCustomizationManagement.contribution.ts | 10 ++ .../aiCustomizationWorkspaceService.ts | 4 + .../common/aiCustomizationWorkspaceService.ts | 9 + 4 files changed, 178 insertions(+), 7 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 194d9ceb84b..0f874a7d7f8 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js'; -import { joinPath } from '../../../../base/common/resources.js'; +import { joinPath, relativePath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; @@ -13,11 +13,21 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { localize } from '../../../../nls.js'; /** * Agent Sessions override of IAICustomizationWorkspaceService. * Delegates to ISessionsManagementService to provide the active session's * worktree/repository as the project root, and supports worktree commit. + * + * Customization files are always committed to the main repository so they + * persist across worktrees. When a worktree is active the file is also + * copied into the worktree and committed there so the running session + * picks it up immediately. */ export class SessionsAICustomizationWorkspaceService implements IAICustomizationWorkspaceService { declare readonly _serviceBrand: undefined; @@ -45,6 +55,10 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization @ISessionsManagementService private readonly sessionsService: ISessionsManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPathService pathService: IPathService, + @ICommandService private readonly commandService: ICommandService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + @INotificationService private readonly notificationService: INotificationService, ) { const userHome = pathService.userHome({ preferLocal: true }); this._cliUserRoots = [ @@ -119,15 +133,149 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization return this._cliUserFilter; } - /** - * Returns the CLI-accessible user directories (~/.copilot, ~/.claude, ~/.agents). - */ readonly isSessionsWindow = true; - async commitFiles(projectRoot: URI, fileUris: URI[]): Promise { + /** + * Commits customization files. Always commits to the main repository + * so the change persists across worktrees. When a worktree is active + * the file is also committed there so the session sees it immediately. + */ + async commitFiles(_projectRoot: URI, fileUris: URI[]): Promise { const session = this.sessionsService.getActiveSession(); - if (session) { - await this.sessionsService.commitWorktreeFiles(session, fileUris); + if (!session?.repository) { + return; + } + + for (const fileUri of fileUris) { + await this.commitFileToRepos(fileUri, session.repository, session.worktree); + } + } + + /** + * Commits the deletion of files that have already been removed from disk. + * Always stages + commits the removal in the main repository, and also + * in the worktree if one is active. + */ + async deleteFiles(_projectRoot: URI, fileUris: URI[]): Promise { + const session = this.sessionsService.getActiveSession(); + if (!session?.repository) { + return; + } + + for (const fileUri of fileUris) { + await this.commitDeletionToRepos(fileUri, session.repository, session.worktree); + } + } + + /** + * Computes the repository-relative path for a file. The file may be + * located under the worktree or the repository root. + */ + private getRelativePath(fileUri: URI, repositoryUri: URI, worktreeUri: URI | undefined): string | undefined { + // Try worktree first (when active, files are written under it) + if (worktreeUri) { + const rel = relativePath(worktreeUri, fileUri); + if (rel) { + return rel; + } + } + return relativePath(repositoryUri, fileUri); + } + + /** + * Commits a single file to the main repository and optionally the worktree. + * Copies the file content between trees when needed. + */ + private async commitFileToRepos(fileUri: URI, repositoryUri: URI, worktreeUri: URI | undefined): Promise { + const relPath = this.getRelativePath(fileUri, repositoryUri, worktreeUri); + if (!relPath) { + return; + } + + const repoFileUri = URI.joinPath(repositoryUri, relPath); + + // 1. Always commit to main repository + try { + if (repoFileUri.toString() !== fileUri.toString()) { + const content = await this.fileService.readFile(fileUri); + await this.fileService.writeFile(repoFileUri, content.value); + } + await this.commandService.executeCommand( + 'github.copilot.cli.sessions.commitToRepository', + { repositoryUri, fileUri: repoFileUri } + ); + } catch (error) { + this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit to repository:', error); + if (worktreeUri) { + this.notificationService.notify({ + severity: Severity.Warning, + message: localize('commitToRepoFailed', "Your customization was saved to this session's worktree, but we couldn't apply it to the default branch. You may need to apply it manually."), + }); + } + } + + // 2. Also commit to the worktree if active + if (worktreeUri) { + const worktreeFileUri = URI.joinPath(worktreeUri, relPath); + try { + if (worktreeFileUri.toString() !== fileUri.toString()) { + const content = await this.fileService.readFile(fileUri); + await this.fileService.writeFile(worktreeFileUri, content.value); + } + await this.commandService.executeCommand( + 'github.copilot.cli.sessions.commitToWorktree', + { worktreeUri, fileUri: worktreeFileUri } + ); + } catch (error) { + this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit to worktree:', error); + } + } + } + + /** + * Commits the deletion of a file to the main repository and optionally + * the worktree. The file is already deleted from disk before this is called; + * `git add` on a deleted path stages the removal. + */ + private async commitDeletionToRepos(fileUri: URI, repositoryUri: URI, worktreeUri: URI | undefined): Promise { + const relPath = this.getRelativePath(fileUri, repositoryUri, worktreeUri); + if (!relPath) { + return; + } + + const repoFileUri = URI.joinPath(repositoryUri, relPath); + + // 1. Delete from main repository if it exists there, then commit + try { + if (await this.fileService.exists(repoFileUri)) { + await this.fileService.del(repoFileUri, { useTrash: true, recursive: true }); + } + await this.commandService.executeCommand( + 'github.copilot.cli.sessions.commitToRepository', + { repositoryUri, fileUri: repoFileUri } + ); + } catch (error) { + this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit deletion to repository:', error); + if (worktreeUri) { + this.notificationService.notify({ + severity: Severity.Warning, + message: localize('deleteFromRepoFailed', "Your customization was removed from this session's worktree, but we couldn't apply the change to the default branch. You may need to remove it manually."), + }); + } + } + + // 2. Also commit the deletion in the worktree if active + if (worktreeUri) { + const worktreeFileUri = URI.joinPath(worktreeUri, relPath); + try { + // The file may already be deleted from the worktree by the caller + await this.commandService.executeCommand( + 'github.copilot.cli.sessions.commitToWorktree', + { worktreeUri, fileUri: worktreeFileUri } + ); + } catch (error) { + this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit deletion to worktree:', error); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts index 6e0924c3c46..7011d2094f6 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts @@ -30,6 +30,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ChatConfiguration } from '../../common/constants.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -233,6 +234,15 @@ registerAction2(class extends Action2 { // since each skill is a folder containing SKILL.md. const deleteTarget = isSkill ? dirname(uri) : uri; await fileService.del(deleteTarget, { useTrash: true, recursive: isSkill }); + + // Commit the deletion to git (sessions: main repo + worktree) + if (storage === PromptsStorage.local) { + const workspaceService = accessor.get(IAICustomizationWorkspaceService); + const projectRoot = workspaceService.getActiveProjectRoot(); + if (projectRoot) { + await workspaceService.deleteFiles(projectRoot, [deleteTarget]); + } + } } } }); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 6d867728d37..bd55fdf5449 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -71,6 +71,10 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic // No-op in core VS Code. } + async deleteFiles(_projectRoot: URI, _fileUris: URI[]): Promise { + // No-op in core VS Code. + } + async generateCustomization(type: PromptsType): Promise { const commandIds: Partial> = { [PromptsType.agent]: GENERATE_AGENT_COMMAND_ID, diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index 26af14f6561..df9060677f2 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -99,6 +99,15 @@ export interface IAICustomizationWorkspaceService { */ commitFiles(projectRoot: URI, fileUris: URI[]): Promise; + /** + * Commits the deletion of resources that have already been removed from disk. + * The URIs may point to individual files or to directories (for example, when + * deleting a skill, the entire customization folder is removed). Implementations + * should ensure that directory deletions are handled recursively as needed. + * In sessions this stages and commits the removal in the relevant repositories. + */ + deleteFiles(projectRoot: URI, fileUris: URI[]): Promise; + /** * Launches the AI-guided creation flow for the given customization type. */ From ea82cfa22b0476bfa91f06af280e10166f102558 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:46:13 -0800 Subject: [PATCH 116/448] sessions: suggest slash commands on new session page (#299098) * feat: add dynamic customization slash commands to sessions new-chat page Add individual prompt/skill files as slash commands in the sessions window's new-chat input, matching what the customizations view shows. - Add getFilteredPromptSlashCommands() to IAICustomizationWorkspaceService - Core: passthrough to IPromptsService - Sessions: filters via applyStorageSourceFilter() per prompt type - Add second completion provider in SlashCommandHandler for dynamic prompt/skill slash commands alongside existing static ones - Update decorations to recognize and highlight dynamic prompt commands - Subscribe to onDidChangeSlashCommands for cache refresh - Fix regex in tryExecuteSlashCommand to support Unicode prompt names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: expand prompt slash commands into CLI-friendly references When a user types /my-prompt in the sessions new-chat input, expand it before sending to: 'Use the prompt file located at [name](uri).' so the CLI agent can locate and process the prompt file. - Add tryExpandPromptSlashCommand() to SlashCommandHandler - Call it in _send() before session.setQuery() to rewrite the query Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aiCustomizationWorkspaceService.ts | 14 ++- .../contrib/chat/browser/newChatViewPane.ts | 8 +- .../contrib/chat/browser/slashCommands.ts | 87 +++++++++++++++++-- .../aiCustomizationWorkspaceService.ts | 8 +- .../common/aiCustomizationWorkspaceService.ts | 10 ++- 5 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 0f874a7d7f8..47292441bdb 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -6,8 +6,9 @@ import { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js'; import { joinPath, relativePath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; -import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; -import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter, applyStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IChatPromptSlashCommand, IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js'; @@ -54,6 +55,7 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization constructor( @ISessionsManagementService private readonly sessionsService: ISessionsManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IPromptsService private readonly promptsService: IPromptsService, @IPathService pathService: IPathService, @ICommandService private readonly commandService: ICommandService, @ILogService private readonly logService: ILogService, @@ -283,4 +285,12 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization const creator = this.instantiationService.createInstance(CustomizationCreatorService); await creator.createWithAI(type); } + + async getFilteredPromptSlashCommands(token: CancellationToken): Promise { + const allCommands = await this.promptsService.getPromptSlashCommands(token); + return allCommands.filter(cmd => { + const filter = this.getStorageSourceFilter(cmd.promptPath.type); + return applyStorageSourceFilter([cmd.promptPath], filter).length > 0; + }); + } } diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 9f285ef4c48..fd5104e8dc5 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -948,7 +948,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } private async _send(options?: { openNewAfterSend?: boolean }): Promise { - const query = this._editor.getModel()?.getValue().trim(); + let query = this._editor.getModel()?.getValue().trim(); const session = this._newSession.value; if (!query || !session || this._sending) { return; @@ -968,6 +968,12 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { return; } + // Expand prompt/skill slash commands into a CLI-friendly reference + const expanded = this._slashCommandHandler?.tryExpandPromptSlashCommand(query); + if (expanded) { + query = expanded; + } + session.setQuery(query); session.setAttachedContext( this._contextAttachments.attachments.length > 0 ? [...this._contextAttachments.attachments] : undefined diff --git a/src/vs/sessions/contrib/chat/browser/slashCommands.ts b/src/vs/sessions/contrib/chat/browser/slashCommands.ts index a2b6d2dc343..bda010579a6 100644 --- a/src/vs/sessions/contrib/chat/browser/slashCommands.ts +++ b/src/vs/sessions/contrib/chat/browser/slashCommands.ts @@ -21,6 +21,8 @@ import { inputPlaceholderForeground } from '../../../../platform/theme/common/co import { localize } from '../../../../nls.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../workbench/contrib/chat/common/widget/chatColors.js'; import { AICustomizationManagementCommands, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; /** * Static command ID used by completion items to trigger immediate slash command execution, @@ -57,6 +59,7 @@ export class SlashCommandHandler extends Disposable { private static _slashDecosRegistered = false; private readonly _slashCommands: ISessionsSlashCommandData[] = []; + private _cachedPromptCommands: readonly IChatPromptSlashCommand[] = []; constructor( private readonly _editor: CodeEditorWidget, @@ -64,23 +67,34 @@ export class SlashCommandHandler extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IThemeService private readonly themeService: IThemeService, + @IAICustomizationWorkspaceService private readonly aiCustomizationWorkspaceService: IAICustomizationWorkspaceService, + @IPromptsService private readonly promptsService: IPromptsService, ) { super(); this._registerSlashCommands(); this._registerCompletions(); this._registerDecorations(); + this._refreshPromptCommands(); + this._register(this.promptsService.onDidChangeSlashCommands(() => this._refreshPromptCommands())); } clearInput(): void { this._editor.getModel()?.setValue(''); } + private _refreshPromptCommands(): void { + this.aiCustomizationWorkspaceService.getFilteredPromptSlashCommands(CancellationToken.None).then(commands => { + this._cachedPromptCommands = commands; + this._updateDecorations(); + }, () => { /* swallow errors from stale refresh */ }); + } + /** * Attempts to parse and execute a slash command from the input. * Returns `true` if a command was handled. */ tryExecuteSlashCommand(query: string): boolean { - const match = query.match(/^\/(\w+)\s*(.*)/s); + const match = query.match(/^\/([\w\p{L}\d_\-\.:]+)\s*(.*)/su); if (!match) { return false; } @@ -95,6 +109,29 @@ export class SlashCommandHandler extends Disposable { return true; } + /** + * If the query starts with a prompt/skill slash command (e.g. `/my-prompt args`), + * expands it into a CLI-friendly markdown reference so the agent can locate the + * file. Returns `undefined` when the query is not a prompt slash command. + */ + tryExpandPromptSlashCommand(query: string): string | undefined { + const match = query.match(/^\/([\w\p{L}\d_\-\.:]+)\s*(.*)/su); + if (!match) { + return undefined; + } + + const commandName = match[1]; + const promptCommand = this._cachedPromptCommands.find(c => c.name === commandName); + if (!promptCommand) { + return undefined; + } + + const args = match[2]?.trim() ?? ''; + const uri = promptCommand.promptPath.uri; + const expanded = `Use the prompt file located at [${promptCommand.name}](${uri.toString()}).`; + return args ? `${expanded} ${args}` : expanded; + } + private _registerSlashCommands(): void { const openSection = (section: AICustomizationManagementSection) => () => this.commandService.executeCommand(AICustomizationManagementCommands.OpenEditor, section); @@ -154,7 +191,7 @@ export class SlashCommandHandler extends Disposable { private _updateDecorations(): void { const model = this._editor.getModel(); const value = model?.getValue() ?? ''; - const match = value.match(/^\/(\w+)\s?/); + const match = value.match(/^\/([\w\p{L}\d_\-\.:]+)\s?/u); if (!match) { this._editor.setDecorationsByType('sessions-chat', SlashCommandHandler._slashDecoType, []); @@ -164,7 +201,8 @@ export class SlashCommandHandler extends Disposable { const commandName = match[1]; const slashCommand = this._slashCommands.find(c => c.command === commandName); - if (!slashCommand) { + const promptCommand = this._cachedPromptCommands.find(c => c.name === commandName); + if (!slashCommand && !promptCommand) { this._editor.setDecorationsByType('sessions-chat', SlashCommandHandler._slashDecoType, []); this._editor.setDecorationsByType('sessions-chat', SlashCommandHandler._slashPlaceholderDecoType, []); return; @@ -179,13 +217,14 @@ export class SlashCommandHandler extends Disposable { // Show the command description as a placeholder after the command const restOfInput = value.slice(match[0].length).trim(); - if (!restOfInput && slashCommand.detail) { + const detail = slashCommand?.detail ?? promptCommand?.description; + if (!restOfInput && detail) { const placeholderCol = match[0].length + 1; const placeholderDeco: IDecorationOptions[] = [{ range: { startLineNumber: 1, startColumn: placeholderCol, endLineNumber: 1, endColumn: model!.getLineMaxColumn(1) }, renderOptions: { after: { - contentText: slashCommand.detail, + contentText: detail, color: this._getPlaceholderColor(), } } @@ -238,6 +277,44 @@ export class SlashCommandHandler extends Disposable { }; } })); + + // Dynamic completions for individual prompt/skill files (filtered to match + // what the sessions customizations view shows). + this._register(this.languageFeaturesService.completionProvider.register({ scheme: uri.scheme, hasAccessToAllModels: true }, { + _debugDisplayName: 'sessionsPromptSlashCommands', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + const range = this._computeCompletionRanges(model, position, /\/[\p{L}0-9_.:-]*/gu); + if (!range) { + return null; + } + + const textBefore = model.getValueInRange(new Range(1, 1, range.replace.startLineNumber, range.replace.startColumn)); + if (textBefore.trim() !== '') { + return null; + } + + const promptCommands = await this.aiCustomizationWorkspaceService.getFilteredPromptSlashCommands(token); + const userInvocable = promptCommands.filter(c => c.parsedPromptFile?.header?.userInvocable !== false); + if (userInvocable.length === 0) { + return null; + } + + return { + suggestions: userInvocable.map((c, i): CompletionItem => { + const label = `/${c.name}`; + return { + label: { label, description: c.description }, + insertText: `${label} `, + documentation: c.description, + range, + sortText: 'b'.repeat(i + 1), + kind: CompletionItemKind.Text, + }; + }) + }; + } + })); } private _computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range } | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index bd55fdf5449..3ebfcea382f 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -5,10 +5,11 @@ import { constObservable, derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js'; import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; -import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { IChatPromptSlashCommand, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { @@ -27,6 +28,7 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ICommandService private readonly commandService: ICommandService, + @IPromptsService private readonly promptsService: IPromptsService, ) { const workspaceFolders = observableFromEventOpts( { owner: this }, @@ -88,6 +90,10 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic await this.commandService.executeCommand(commandId); } } + + async getFilteredPromptSlashCommands(token: CancellationToken): Promise { + return this.promptsService.getPromptSlashCommands(token); + } } registerSingleton(IAICustomizationWorkspaceService, AICustomizationWorkspaceService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index df9060677f2..de4108ff5fb 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { isEqualOrParent } from '../../../../base/common/resources.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { PromptsType } from './promptSyntax/promptTypes.js'; -import { PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { IChatPromptSlashCommand, PromptsStorage } from './promptSyntax/service/promptsService.js'; export const IAICustomizationWorkspaceService = createDecorator('aiCustomizationWorkspaceService'); @@ -130,4 +131,11 @@ export interface IAICustomizationWorkspaceService { * session-derived (or workspace-derived) root. */ clearOverrideProjectRoot(): void; + + /** + * Returns prompt/skill slash commands filtered through the workspace + * service's storage source policy, ensuring the results match the + * customizations visible in the AI Customization views. + */ + getFilteredPromptSlashCommands(token: CancellationToken): Promise; } From f6da4b4e1a07823b5183b4e25416cceced47d99b Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 4 Mar 2026 13:08:01 +0900 Subject: [PATCH 117/448] fix: add graceful shutdown path when heapprofile is enabled --- .../src/tsServer/serverProcess.electron.ts | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index 992cae925df..a356fd7817b 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -24,6 +24,12 @@ const contentLengthSize: number = Buffer.byteLength(contentLength, 'utf8'); const blank: number = Buffer.from(' ', 'utf8')[0]; const backslashR: number = Buffer.from('\r', 'utf8')[0]; const backslashN: number = Buffer.from('\n', 'utf8')[0]; +const gracefulExitTimeout = 5000; +const tsServerExitRequest: Proto.Request = { + seq: 0, + type: 'request', + command: 'exit', +}; class ProtocolBuffer { @@ -207,10 +213,15 @@ function getTssDebugBrk(): string | undefined { } class IpcChildServerProcess extends Disposable implements TsServerProcess { + private _killTimeout: NodeJS.Timeout | undefined; + private _isShuttingDown = false; + constructor( private readonly _process: child_process.ChildProcess, + private readonly _useGracefulShutdown: boolean, ) { super(); + this._process.once('exit', () => this.clearKillTimeout()); } write(serverRequest: Proto.Request): void { @@ -230,18 +241,47 @@ class IpcChildServerProcess extends Disposable implements TsServerProcess { } kill(): void { - this._process.kill(); + if (!this._useGracefulShutdown) { + this._process.kill(); + return; + } + + if (this._isShuttingDown) { + return; + } + this._isShuttingDown = true; + + try { + this._process.send(tsServerExitRequest); + } catch { + this._process.kill(); + return; + } + + this._killTimeout = setTimeout(() => this._process.kill(), gracefulExitTimeout); + this._killTimeout.unref?.(); + } + + private clearKillTimeout(): void { + if (this._killTimeout) { + clearTimeout(this._killTimeout); + this._killTimeout = undefined; + } } } class StdioChildServerProcess extends Disposable implements TsServerProcess { private readonly _reader: Reader; + private _killTimeout: NodeJS.Timeout | undefined; + private _isShuttingDown = false; constructor( private readonly _process: child_process.ChildProcess, + private readonly _useGracefulShutdown: boolean, ) { super(); this._reader = this._register(new Reader(this._process.stdout!)); + this._process.once('exit', () => this.clearKillTimeout()); } write(serverRequest: Proto.Request): void { @@ -262,7 +302,39 @@ class StdioChildServerProcess extends Disposable implements TsServerProcess { } kill(): void { - this._process.kill(); + if (!this._useGracefulShutdown) { + this._process.kill(); + this._reader.dispose(); + return; + } + + if (this._isShuttingDown) { + return; + } + this._isShuttingDown = true; + + try { + this._process.stdin?.write(JSON.stringify(tsServerExitRequest) + '\r\n', 'utf8'); + this._process.stdin?.end(); + } catch { + this._process.kill(); + this._reader.dispose(); + return; + } + + this._killTimeout = setTimeout(() => { + this._process.kill(); + this._reader.dispose(); + }, gracefulExitTimeout); + this._killTimeout.unref?.(); + } + + private clearKillTimeout(): void { + if (this._killTimeout) { + clearTimeout(this._killTimeout); + this._killTimeout = undefined; + } + this._reader.dispose(); } } @@ -290,6 +362,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { const env = generatePatchedEnv(process.env, tsServerPath, !!execPath); const runtimeArgs = [...args]; const execArgv = getExecArgv(kind, configuration); + const useGracefulShutdown = configuration.heapProfile.enabled; const useIpc = !execPath && version.apiVersion?.gte(API.v460); if (useIpc) { runtimeArgs.push('--useNodeIpc'); @@ -309,6 +382,6 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { stdio: useIpc ? ['pipe', 'pipe', 'pipe', 'ipc'] : undefined, }); - return useIpc ? new IpcChildServerProcess(childProcess) : new StdioChildServerProcess(childProcess); + return useIpc ? new IpcChildServerProcess(childProcess, useGracefulShutdown) : new StdioChildServerProcess(childProcess, useGracefulShutdown); } } From 757c5ced02f612fd507212b61b046a8da23609ef Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:41:19 -0800 Subject: [PATCH 118/448] Refactor canResolveChatSession to accept session type (#299112) refactor: update canResolveChatSession method to accept session type instead of URI --- .../browser/agentSessions/agentSessionsOpener.ts | 2 +- .../chatSessions/chatSessions.contribution.ts | 12 ++++++------ .../chat/browser/widgetHosts/editor/chatEditor.ts | 2 +- .../browser/widgetHosts/viewPane/chatViewPane.ts | 9 ++++----- .../chat/common/chatService/chatServiceImpl.ts | 2 +- .../contrib/chat/common/chatSessionsService.ts | 2 +- .../chat/test/common/mockChatSessionsService.ts | 4 ++-- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts index be01ac3973c..52b0707f567 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts @@ -103,7 +103,7 @@ async function openSessionDefault(accessor: ServicesAccessor, session: IAgentSes } const isLocalChatSession = session.resource.scheme === Schemas.vscodeChatEditor || session.resource.scheme === Schemas.vscodeLocalChatSession; - if (!isLocalChatSession && !(await chatSessionsService.canResolveChatSession(session.resource))) { + if (!isLocalChatSession && !(await chatSessionsService.canResolveChatSession(session.resource.scheme))) { target = openOptions?.sideBySide ? SIDE_GROUP : ACTIVE_GROUP; // force to open in editor if session cannot be resolved in panel options = { ...options, revealIfOpened: true }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 10dc9a17393..967ae4f9551 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -781,20 +781,20 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return !!controller; } - async canResolveChatSession(chatSessionResource: URI) { + async canResolveChatSession(sessionType: string) { await this._extensionService.whenInstalledExtensionsRegistered(); - const resolvedType = this._resolveToPrimaryType(chatSessionResource.scheme) || chatSessionResource.scheme; + const resolvedType = this._resolveToPrimaryType(sessionType) || sessionType; const contribution = this._contributions.get(resolvedType)?.contribution; if (contribution && !this._isContributionAvailable(contribution)) { return false; } - if (this._contentProviders.has(chatSessionResource.scheme)) { + if (this._contentProviders.has(sessionType)) { return true; } - await this._extensionService.activateByEvent(`onChatSession:${chatSessionResource.scheme}`); - return this._contentProviders.has(chatSessionResource.scheme); + await this._extensionService.activateByEvent(`onChatSession:${sessionType}`); + return this._contentProviders.has(sessionType); } private async tryActivateControllers(providersToResolve: readonly string[] | undefined): Promise { @@ -1024,7 +1024,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } } - if (!(await raceCancellationError(this.canResolveChatSession(sessionResource), token))) { + if (!(await raceCancellationError(this.canResolveChatSession(sessionResource.scheme), token))) { throw Error(`Can not find provider for ${sessionResource}`); } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts index 8a1c71fb615..0a3344392bc 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts @@ -219,7 +219,7 @@ export class ChatEditor extends AbstractEditorWithViewState c.type === chatSessionType); if (contribution) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 4974f0b91ce..905351e3adf 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -707,7 +707,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const model = ref?.object; if (model) { - await this.updateWidgetLockState(model.sessionResource); // Update widget lock state based on session type + await this.updateWidgetLockState(getChatSessionType(model.sessionResource)); // Update widget lock state based on session type // remember as model to restore in view state this.viewState.sessionResource = model.sessionResource; @@ -729,8 +729,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return model; } - private async updateWidgetLockState(sessionResource: URI): Promise { - const sessionType = getChatSessionType(sessionResource); + private async updateWidgetLockState(sessionType: string): Promise { if (sessionType === localChatSessionType) { this._widget.unlockFromCodingAgent(); return; @@ -738,9 +737,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let canResolve = false; try { - canResolve = await this.chatSessionsService.canResolveChatSession(sessionResource); + canResolve = await this.chatSessionsService.canResolveChatSession(sessionType); } catch (error) { - this.logService.warn(`Failed to resolve chat session '${sessionResource.toString()}' for locking`, error); + this.logService.warn(`Failed to resolve chat session type '${sessionType}' for locking`, error); } if (!canResolve) { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 25bf8d21d2e..b5ed3c924c0 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -579,7 +579,7 @@ export class ChatService extends Disposable implements IChatService { } private async loadRemoteSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { - await this.chatSessionService.canResolveChatSession(sessionResource); + await this.chatSessionService.canResolveChatSession(sessionResource.scheme); // Check if session already exists { diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index da4c1b50db3..7d4396c392b 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -256,7 +256,7 @@ export interface IChatSessionsService { getContentProviderSchemes(): string[]; registerChatSessionContentProvider(scheme: string, provider: IChatSessionContentProvider): IDisposable; - canResolveChatSession(sessionResource: URI): Promise; + canResolveChatSession(sessionType: string): Promise; getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise; hasAnySessionOptions(sessionResource: URI): boolean; diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 7ee70008f52..da463673ef1 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -161,8 +161,8 @@ export class MockChatSessionsService implements IChatSessionsService { return provider.provideChatSessionContent(sessionResource, token); } - async canResolveChatSession(chatSessionResource: URI): Promise { - return this.contentProviders.has(chatSessionResource.scheme); + async canResolveChatSession(sessionType: string): Promise { + return this.contentProviders.has(sessionType); } getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined { From 605a07d2f8e97297f56425c95f40ba566d7ba6ac Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:48:04 -0800 Subject: [PATCH 119/448] Don't depend on sessionResource in a few places (#299110) * Don't depend on sessionResource in a few places Removes some antipaterns and unused variables. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../api/browser/mainThreadChatAgents2.ts | 3 +- .../browser/actions/chatContinueInAction.ts | 5 +- .../chat/browser/attachments/chatVariables.ts | 124 ++++++------ .../contrib/chat/browser/widget/chatWidget.ts | 7 +- .../browser/widget/input/chatInputPart.ts | 6 +- .../input/editor/chatInputEditorContrib.ts | 3 +- .../widget/input/editor/chatPasteProviders.ts | 6 +- .../common/requestParser/chatRequestParser.ts | 11 +- .../browser/attachments/chatVariables.test.ts | 191 ++++++++++++++++++ 9 files changed, 281 insertions(+), 75 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 4fdf590d553..bf0ba0049b0 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -32,6 +32,7 @@ import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/prompt import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../../contrib/chat/common/requestParser/chatRequestParser.js'; +import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../../contrib/chat/browser/attachments/chatVariables.js'; import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatTaskSerialized, IChatWarningMessage } from '../../contrib/chat/common/chatService/chatService.js'; import { IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../contrib/chat/common/constants.js'; @@ -459,7 +460,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return; } - const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionResource, model.getValue()).parts; + const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequestWithReferences(getDynamicVariablesForWidget(widget), getSelectedToolAndToolSetsForWidget(widget), model.getValue()).parts; const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); const thisAgentId = this._agents.get(handle)?.id; if (agentPart?.agent.id !== thisAgentId) { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index 40827f04060..3ab18f156f5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -34,6 +34,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; import { ChatModel } from '../../common/model/chatModel.js'; import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js'; +import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../attachments/chatVariables.js'; import { ChatSendResult, IChatService } from '../../common/chatService/chatService.js'; import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { ChatAgentLocation } from '../../common/constants.js'; @@ -403,7 +404,7 @@ export class CreateRemoteAgentJobAction { userPrompt = 'implement this.'; } - const attachedContext = widget.input.getAttachedAndImplicitContext(sessionResource); + const attachedContext = widget.input.getAttachedAndImplicitContext(); widget.input.acceptInput(true); // For inline editor mode, add selection or cursor information @@ -479,7 +480,7 @@ export class CreateRemoteAgentJobAction { const requestParser = instantiationService.createInstance(ChatRequestParser); // Add the request to the model first - const parsedRequest = requestParser.parseChatRequest(sessionResource, userPrompt, ChatAgentLocation.Chat); + const parsedRequest = requestParser.parseChatRequestWithReferences(getDynamicVariablesForWidget(widget), getSelectedToolAndToolSetsForWidget(widget), userPrompt, ChatAgentLocation.Chat); const addedRequest = chatModel.addRequest( parsedRequest, { variables: attachedContext.asArray() }, diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatVariables.ts index 47e14912044..ffdb625d5c9 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatVariables.ts @@ -5,11 +5,72 @@ import { IChatVariablesService, IDynamicVariable } from '../../common/attachments/chatVariables.js'; import { IToolAndToolSetEnablementMap } from '../../common/tools/languageModelToolsService.js'; -import { IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatDynamicVariableModel } from './chatDynamicVariables.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { URI } from '../../../../../base/common/uri.js'; +export function getDynamicVariablesForWidget(widget: IChatWidget): ReadonlyArray { + if (!widget.viewModel || !widget.supportsFileReferences) { + return []; + } + + const model = widget.getContrib(ChatDynamicVariableModel.ID); + if (!model) { + return []; + } + + // track for editing state + if (widget.viewModel.editing && model.variables.length > 0) { + return model.variables; + } + + if (widget.input.attachmentModel.attachments.length > 0 && widget.viewModel.editing) { + const references: IDynamicVariable[] = []; + const editorModel = widget.inputEditor.getModel(); + const modelTextLength = editorModel?.getValueLength() ?? 0; + for (const attachment of widget.input.attachmentModel.attachments) { + // If the attachment has a range, it is a dynamic variable + if (attachment.range) { + if (attachment.range.start >= attachment.range.endExclusive) { + continue; + } + + if (attachment.range.start < 0 || attachment.range.endExclusive > modelTextLength) { + continue; + } + + if (!editorModel) { + continue; + } + + const startPos = editorModel.getPositionAt(attachment.range.start); + const endPos = editorModel.getPositionAt(attachment.range.endExclusive); + + const referenceObj: IDynamicVariable = { + id: attachment.id, + fullName: attachment.name, + modelDescription: attachment.modelDescription, + range: new Range(startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column), + icon: attachment.icon, + isFile: attachment.kind === 'file', + isDirectory: attachment.kind === 'directory', + data: attachment.value + }; + references.push(referenceObj); + } + } + + return references.length > 0 ? references : model.variables; + } + + return model.variables; +} + +export function getSelectedToolAndToolSetsForWidget(widget: IChatWidget): IToolAndToolSetEnablementMap { + return widget.input.selectedToolsModel.entriesMap.get(); +} + export class ChatVariablesService implements IChatVariablesService { declare _serviceBrand: undefined; @@ -18,65 +79,11 @@ export class ChatVariablesService implements IChatVariablesService { ) { } getDynamicVariables(sessionResource: URI): ReadonlyArray { - // This is slightly wrong... the parser pulls dynamic references from the input widget, but there is no guarantee that message came from the input here. - // Need to ... - // - Parser takes list of dynamic references (annoying) - // - Or the parser is known to implicitly act on the input widget, and we need to call it before calling the chat service (maybe incompatible with the future, but easy) const widget = this.chatWidgetService.getWidgetBySessionResource(sessionResource); - if (!widget || !widget.viewModel || !widget.supportsFileReferences) { + if (!widget) { return []; } - - const model = widget.getContrib(ChatDynamicVariableModel.ID); - if (!model) { - return []; - } - - // track for editing state - if (widget.viewModel.editing && model.variables.length > 0) { - return model.variables; - } - - if (widget.input.attachmentModel.attachments.length > 0 && widget.viewModel.editing) { - const references: IDynamicVariable[] = []; - const editorModel = widget.inputEditor.getModel(); - const modelTextLength = editorModel?.getValueLength() ?? 0; - for (const attachment of widget.input.attachmentModel.attachments) { - // If the attachment has a range, it is a dynamic variable - if (attachment.range) { - if (attachment.range.start >= attachment.range.endExclusive) { - continue; - } - - if (attachment.range.start < 0 || attachment.range.endExclusive > modelTextLength) { - continue; - } - - if (!editorModel) { - continue; - } - - const startPos = editorModel.getPositionAt(attachment.range.start); - const endPos = editorModel.getPositionAt(attachment.range.endExclusive); - - const referenceObj: IDynamicVariable = { - id: attachment.id, - fullName: attachment.name, - modelDescription: attachment.modelDescription, - range: new Range(startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column), - icon: attachment.icon, - isFile: attachment.kind === 'file', - isDirectory: attachment.kind === 'directory', - data: attachment.value - }; - references.push(referenceObj); - } - } - - return references.length > 0 ? references : model.variables; - } - - return model.variables; + return getDynamicVariablesForWidget(widget); } getSelectedToolAndToolSets(sessionResource: URI): IToolAndToolSetEnablementMap { @@ -84,7 +91,6 @@ export class ChatVariablesService implements IChatVariablesService { if (!widget) { return new Map(); } - return widget.input.selectedToolsModel.entriesMap.get(); - + return getSelectedToolAndToolSetsForWidget(widget); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 7c172e77ee9..09a203bd5b0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -56,6 +56,7 @@ import { IChatModel, IChatModelInputState, IChatResponseModel } from '../../comm import { ChatMode, getModeNameForTelemetry, IChatModeService } from '../../common/chatModes.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js'; +import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../attachments/chatVariables.js'; import { ChatRequestQueueKind, ChatSendResult, IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js'; @@ -333,7 +334,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser) - .parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { + .parseChatRequestWithReferences(getDynamicVariablesForWidget(this), getSelectedToolAndToolSetsForWidget(this), this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind, attachmentCapabilities: this.attachmentCapabilities, @@ -854,7 +855,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } const previous = this.parsedChatRequest; - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind, attachmentCapabilities: this.attachmentCapabilities }); + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequestWithReferences(getDynamicVariablesForWidget(this), getSelectedToolAndToolSetsForWidget(this), this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind, attachmentCapabilities: this.attachmentCapabilities }); if (!previous || !IParsedChatRequest.equals(previous, this.parsedChatRequest)) { this._onDidChangeParsedInput.fire(); } @@ -2218,7 +2219,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const editorValue = this.getInput(); const requestInputs: IChatRequestInputOptions = { input: !query ? editorValue : query.query, - attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext(this.viewModel.sessionResource) : this.input.getAttachedAndImplicitContext(this.viewModel.sessionResource), + attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext() : this.input.getAttachedAndImplicitContext(), }; const isUserQuery = !query; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 1a4b90632fb..87a0817bba6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -248,15 +248,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly selectedToolsModel: ChatSelectedTools; - public getAttachedContext(sessionResource: URI) { + public getAttachedContext() { const contextArr = new ChatRequestVariableSet(); contextArr.add(...this.attachmentModel.attachments, ...this.chatContextService.getWorkspaceContextItems()); return contextArr; } - public getAttachedAndImplicitContext(sessionResource: URI): ChatRequestVariableSet { + public getAttachedAndImplicitContext(): ChatRequestVariableSet { - const contextArr = this.getAttachedContext(sessionResource); + const contextArr = this.getAttachedContext(); if (this.implicitContext) { const implicitChatVariables = this.implicitContext.enabledBaseEntries(this.configurationService.getValue('chat.implicitContext.suggestedContext')); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts index ce838a3e0a1..0bf73505e4e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts @@ -20,6 +20,7 @@ import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../../. import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../common/widget/chatColors.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../../../common/requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../../../../common/requestParser/chatRequestParser.js'; +import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../../../attachments/chatVariables.js'; import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; import { IChatWidget } from '../../../chat.js'; import { ChatWidget } from '../../chatWidget.js'; @@ -411,7 +412,7 @@ class ChatTokenDeleter extends Disposable { // If this was a simple delete, try to find out whether it was inside a token if (!change.text && this.widget.viewModel) { const attachmentCapabilities = previousSelectedAgent?.capabilities ?? this.widget.attachmentCapabilities; - const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionResource, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent, mode: this.widget.input.currentModeKind, attachmentCapabilities }); + const previousParsedValue = parser.parseChatRequestWithReferences(getDynamicVariablesForWidget(this.widget), getSelectedToolAndToolSetsForWidget(this.widget), previousInputValue, this.widget.location, { selectedAgent: previousSelectedAgent, mode: this.widget.input.currentModeKind, attachmentCapabilities }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestSlashPromptPart || p instanceof ChatRequestToolPart); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts index 65089b20200..eafcf7aa1c6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts @@ -24,8 +24,9 @@ import { IInstantiationService } from '../../../../../../../platform/instantiati import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { IExtensionService, isProposedApiEnabled } from '../../../../../../services/extensions/common/extensions.js'; import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; -import { IChatVariablesService, IDynamicVariable } from '../../../../common/attachments/chatVariables.js'; +import { IDynamicVariable } from '../../../../common/attachments/chatVariables.js'; import { IChatWidgetService } from '../../../chat.js'; +import { getDynamicVariablesForWidget } from '../../../attachments/chatVariables.js'; import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; import { cleanupOldImages, createFileForMedia, resizeImage } from '../../../chatImageUtils.js'; @@ -201,7 +202,6 @@ class CopyAttachmentsProvider implements DocumentPasteEditProvider { constructor( @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatVariablesService private readonly chatVariableService: IChatVariablesService ) { } async prepareDocumentPaste(model: ITextModel, _ranges: readonly IRange[], _dataTransfer: IReadonlyVSDataTransfer, _token: CancellationToken): Promise { @@ -212,7 +212,7 @@ class CopyAttachmentsProvider implements DocumentPasteEditProvider { } const attachments = widget.attachmentModel.attachments; - const dynamicVariables = this.chatVariableService.getDynamicVariables(widget.viewModel.sessionResource); + const dynamicVariables = getDynamicVariablesForWidget(widget); if (attachments.length === 0 && dynamicVariables.length === 0) { return undefined; diff --git a/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts index 9ec0508a489..bf1214267a5 100644 --- a/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts @@ -12,7 +12,7 @@ import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../participants/chatAgents.js'; import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; import { IPromptsService } from '../promptSyntax/service/promptsService.js'; -import { IToolData, IToolSet, isToolSet } from '../tools/languageModelToolsService.js'; +import { IToolAndToolSetEnablementMap, IToolData, IToolSet, isToolSet } from '../tools/languageModelToolsService.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js'; const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent @@ -37,11 +37,16 @@ export class ChatRequestParser { ) { } parseChatRequest(sessionResource: URI, message: string, location: ChatAgentLocation = ChatAgentLocation.Chat, context?: IChatParserContext): IParsedChatRequest { - const parts: IParsedChatRequestPart[] = []; const references = this.variableService.getDynamicVariables(sessionResource); // must access this list before any async calls + const selectedToolAndToolSets = this.variableService.getSelectedToolAndToolSets(sessionResource); + return this.parseChatRequestWithReferences(references, selectedToolAndToolSets, message, location, context); + } + + parseChatRequestWithReferences(references: ReadonlyArray, selectedToolAndToolSets: IToolAndToolSetEnablementMap, message: string, location: ChatAgentLocation = ChatAgentLocation.Chat, context?: IChatParserContext): IParsedChatRequest { + const parts: IParsedChatRequestPart[] = []; const toolsByName = new Map(); const toolSetsByName = new Map(); - for (const [entry, enabled] of this.variableService.getSelectedToolAndToolSets(sessionResource)) { + for (const [entry, enabled] of selectedToolAndToolSets) { if (enabled) { if (isToolSet(entry)) { toolSetsByName.set(entry.referenceName, entry); diff --git a/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts new file mode 100644 index 00000000000..59e05f0411b --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/attachments/chatVariables.test.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { IDynamicVariable } from '../../../common/attachments/chatVariables.js'; +import { IChatWidget } from '../../../browser/chat.js'; +import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../../../browser/attachments/chatVariables.js'; +import { ChatDynamicVariableModel } from '../../../browser/attachments/chatDynamicVariables.js'; +import { IChatRequestVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { IToolData, IToolSet, ToolDataSource } from '../../../common/tools/languageModelToolsService.js'; +import { observableValue } from '../../../../../../base/common/observable.js'; + +function createMockVariable(overrides?: Partial): IDynamicVariable { + return { + id: 'var-1', + fullName: 'test-var', + range: new Range(1, 1, 1, 10), + data: 'test-data', + ...overrides, + }; +} + +function createMockAttachment(overrides?: Partial): IChatRequestVariableEntry { + return { + id: 'attach-1', + name: 'test-attachment', + kind: 'file', + value: 'test-value', + ...overrides, + } as IChatRequestVariableEntry; +} + +function createMockWidget(options: { + hasViewModel?: boolean; + supportsFileReferences?: boolean; + contribVariables?: IDynamicVariable[]; + editing?: boolean; + attachments?: IChatRequestVariableEntry[]; + editorTextLength?: number; +}): IChatWidget { + const { + hasViewModel = true, + supportsFileReferences = true, + contribVariables = [], + editing = false, + attachments = [], + editorTextLength = 100, + } = options; + + const contribModel = { + id: ChatDynamicVariableModel.ID, + variables: contribVariables, + }; + + return { + viewModel: hasViewModel ? { editing: editing ? {} : undefined } : undefined, + supportsFileReferences, + getContrib: (id: string) => id === ChatDynamicVariableModel.ID ? contribModel : undefined, + input: { + attachmentModel: { attachments }, + }, + inputEditor: { + getModel: () => ({ + getValueLength: () => editorTextLength, + getPositionAt: (offset: number) => ({ lineNumber: 1, column: offset + 1 }), + }), + }, + } as unknown as IChatWidget; +} + +suite('getDynamicVariablesForWidget', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('returns empty when no viewModel', () => { + const widget = createMockWidget({ hasViewModel: false }); + assert.deepStrictEqual(getDynamicVariablesForWidget(widget), []); + }); + + test('returns empty when file references not supported', () => { + const widget = createMockWidget({ supportsFileReferences: false }); + assert.deepStrictEqual(getDynamicVariablesForWidget(widget), []); + }); + + test('returns contrib model variables when not editing', () => { + const variables = [createMockVariable()]; + const widget = createMockWidget({ contribVariables: variables }); + assert.deepStrictEqual(getDynamicVariablesForWidget(widget), variables); + }); + + test('returns contrib model variables when editing with existing variables', () => { + const variables = [createMockVariable()]; + const widget = createMockWidget({ editing: true, contribVariables: variables }); + assert.deepStrictEqual(getDynamicVariablesForWidget(widget), variables); + }); + + test('converts attachments to dynamic variables when editing with attachments and no contrib variables', () => { + const attachments = [ + createMockAttachment({ + id: 'a1', + name: 'file.ts', + kind: 'file', + value: 'file-value', + range: { start: 0, endExclusive: 8 }, + }), + ]; + const widget = createMockWidget({ editing: true, attachments, contribVariables: [] }); + const result = getDynamicVariablesForWidget(widget); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'a1'); + assert.strictEqual(result[0].fullName, 'file.ts'); + assert.strictEqual(result[0].isFile, true); + assert.strictEqual(result[0].isDirectory, false); + assert.strictEqual(result[0].data, 'file-value'); + }); + + test('skips attachments without range when editing', () => { + const attachments = [createMockAttachment({ range: undefined })]; + const widget = createMockWidget({ editing: true, attachments, contribVariables: [] }); + const result = getDynamicVariablesForWidget(widget); + + // No ranged attachments, falls back to contrib model variables (empty) + assert.deepStrictEqual(result, []); + }); + + test('skips attachments with empty range', () => { + const attachments = [createMockAttachment({ range: { start: 5, endExclusive: 5 } })]; + const widget = createMockWidget({ editing: true, attachments, contribVariables: [] }); + const result = getDynamicVariablesForWidget(widget); + assert.deepStrictEqual(result, []); + }); + + test('skips attachments with out-of-bounds range', () => { + const attachments = [createMockAttachment({ range: { start: 0, endExclusive: 200 } })]; + const widget = createMockWidget({ editing: true, attachments, editorTextLength: 100, contribVariables: [] }); + const result = getDynamicVariablesForWidget(widget); + assert.deepStrictEqual(result, []); + }); + + test('skips attachments with negative start', () => { + const attachments = [createMockAttachment({ range: { start: -1, endExclusive: 5 } })]; + const widget = createMockWidget({ editing: true, attachments, contribVariables: [] }); + const result = getDynamicVariablesForWidget(widget); + assert.deepStrictEqual(result, []); + }); + + test('sets isDirectory for directory attachments', () => { + const attachments = [ + createMockAttachment({ + kind: 'directory', + range: { start: 0, endExclusive: 5 }, + }), + ]; + const widget = createMockWidget({ editing: true, attachments, contribVariables: [] }); + const result = getDynamicVariablesForWidget(widget); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].isFile, false); + assert.strictEqual(result[0].isDirectory, true); + }); +}); + +suite('getSelectedToolAndToolSetsForWidget', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('returns the entriesMap from the selected tools model', () => { + const toolData: IToolData = { + id: 'tool-1', + toolReferenceName: 'myTool', + displayName: 'My Tool', + modelDescription: 'A test tool', + canBeReferencedInPrompt: true, + source: ToolDataSource.Internal, + }; + const expectedMap = new Map([[toolData, true]]); + const entriesMap = observableValue('test', expectedMap); + + const widget = { + input: { + selectedToolsModel: { entriesMap }, + }, + } as unknown as IChatWidget; + + const result = getSelectedToolAndToolSetsForWidget(widget); + assert.strictEqual(result, expectedMap); + }); +}); From 06c96dcab40c625d51c387b5ea00c7760acb7afa Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 05:56:19 +0100 Subject: [PATCH 120/448] sessions - AI customizations for selfhost (#299053) * sessions - AI customizations for selfhost * more * Update .github/hooks/hooks.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/skills/sessions/SKILL.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 + .github/hooks/hooks.json | 3 +- .github/skills/agent-sessions-layout/SKILL.md | 18 +-- .github/skills/sessions/SKILL.md | 109 +++++++++++------- .vscode/tasks.json | 4 +- 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 06005d9424b..c0a4142db03 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,6 +24,7 @@ Visual Studio Code is built with a layered architecture using TypeScript, web AP - `workbench/api/` - Extension host and VS Code API implementation - `src/vs/code/` - Electron main process specific implementation - `src/vs/server/` - Server specific implementation +- `src/vs/sessions/` - Agent sessions window, a dedicated workbench layer for agentic workflows (sits alongside `vs/workbench`, may import from it but not vice versa) The core architecture follows these principles: - **Layered architecture** - from `base`, `platform`, `editor`, to `workbench` @@ -135,6 +136,7 @@ function f(x: number, y: string): void { } - Prefer regex capture groups with names over numbered capture groups. - If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task - Never duplicate imports. Always reuse existing imports if they are present. +- When removing an import, do not leave behind blank lines where the import was. Ensure the surrounding code remains compact. - Do not use `any` or `unknown` as the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined. - When adding file watching, prefer correlated file watchers (via fileService.createWatcher) to shared ones. - When adding tooltips to UI elements, prefer the use of IHoverService service. diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json index 3e0f178b023..5e27e6db893 100644 --- a/.github/hooks/hooks.json +++ b/.github/hooks/hooks.json @@ -4,7 +4,8 @@ "sessionStart": [ { "type": "command", - "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup bash -c 'npm ci && npm run compile' > /tmp/worktree-setup-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi" + "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup bash -c 'npm ci && npm run compile' > /tmp/worktree-setup-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi", + "powershell": "if (Test-Path \"$env:USERPROFILE\\.vscode-worktree-setup\") { $log = \"$env:TEMP\\worktree-setup-$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log\"; $dir = $PWD.Path; Start-Job -ScriptBlock { param($d, $l) Set-Location $d; & { npm ci; if ($LASTEXITCODE -eq 0) { npm run compile } } *> $l } -ArgumentList $dir, $log | Out-Null }" } ], "sessionEnd": [ diff --git a/.github/skills/agent-sessions-layout/SKILL.md b/.github/skills/agent-sessions-layout/SKILL.md index a76794d9c7d..af4f03a3f60 100644 --- a/.github/skills/agent-sessions-layout/SKILL.md +++ b/.github/skills/agent-sessions-layout/SKILL.md @@ -45,7 +45,7 @@ When proposing or implementing changes, follow these rules from the spec: 4. **New parts go in the right section** — Any new parts should be added to the horizontal branch alongside Chat Bar and Auxiliary Bar 5. **Preserve no-op methods** — Unsupported features (zen mode, centered layout, etc.) should remain as no-ops, not throw errors 6. **Handle pane composite lifecycle** — When hiding/showing parts, manage the associated pane composites -7. **Use agent session parts** — New part functionality goes in the agent session part classes (`SidebarPart`, `AuxiliaryBarPart`, `PanelPart`, `ChatBarPart`), not the standard workbench parts +7. **Use agent session parts** — New part functionality goes in the agent session part classes (`SidebarPart`, `AuxiliaryBarPart`, `PanelPart`, `ChatBarPart`, `ProjectBarPart`), not the standard workbench parts 8. **Use separate storage keys** — Agent session parts use their own storage keys (prefixed with `workbench.agentsession.` or `workbench.chatbar.`) to avoid conflicts with regular workbench state 9. **Use agent session menu IDs** — Actions should use `Menus.*` menu IDs (from `sessions/browser/menus.ts`), not shared `MenuId.*` constants @@ -53,20 +53,24 @@ When proposing or implementing changes, follow these rules from the spec: | File | Purpose | |------|---------| -| `sessions/LAYOUT.md` | Authoritative specification | +| `sessions/LAYOUT.md` | Authoritative layout specification | | `sessions/browser/workbench.ts` | Main layout implementation (`Workbench` class) | | `sessions/browser/menus.ts` | Agent sessions menu IDs (`Menus` export) | | `sessions/browser/layoutActions.ts` | Layout actions (toggle sidebar, panel, secondary sidebar) | | `sessions/browser/paneCompositePartService.ts` | `AgenticPaneCompositePartService` | -| `sessions/browser/style.css` | Layout-specific styles | -| `sessions/browser/parts/` | Agent session part implementations | +| `sessions/browser/media/style.css` | Layout-specific styles | +| `sessions/browser/parts/parts.ts` | `AgenticParts` enum | | `sessions/browser/parts/titlebarPart.ts` | Titlebar part, MainTitlebarPart, AuxiliaryTitlebarPart, TitleService | -| `sessions/browser/parts/sidebarPart.ts` | Sidebar part (with footer) | +| `sessions/browser/parts/sidebarPart.ts` | Sidebar part (with footer and macOS traffic light spacer) | | `sessions/browser/parts/chatBarPart.ts` | Chat Bar part | -| `sessions/browser/widget/` | Agent sessions chat widget | +| `sessions/browser/parts/auxiliaryBarPart.ts` | Auxiliary Bar part (with run script dropdown) | +| `sessions/browser/parts/panelPart.ts` | Panel part | +| `sessions/browser/parts/projectBarPart.ts` | Project Bar part (folder entries, icon customization) | +| `sessions/contrib/configuration/browser/configuration.contribution.ts` | Sets `workbench.editor.useModal` to `'all'` for modal editor overlay | | `sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts` | Title bar widget and session picker | -| `sessions/contrib/chat/browser/runScriptAction.ts` | Run script contribution | +| `sessions/contrib/chat/browser/runScriptAction.ts` | Run script split button for titlebar | | `sessions/contrib/accountMenu/browser/account.contribution.ts` | Account widget for sidebar footer | +| `sessions/electron-browser/parts/titlebarPart.ts` | Desktop (Electron) titlebar part | ## 5. Testing Changes diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index fc49548b7a3..d82e03178ff 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -15,8 +15,6 @@ The `src/vs/sessions/` directory contains authoritative specification documents. | Layout spec | `src/vs/sessions/LAYOUT.md` | Grid structure, part positions, sizing, CSS classes, API reference | | AI Customizations | `src/vs/sessions/AI_CUSTOMIZATIONS.md` | AI customization editor and tree view design | | Chat Widget | `src/vs/sessions/browser/widget/AGENTS_CHAT_WIDGET.md` | Chat widget wrapper architecture, deferred session creation, option delivery | -| AI Customization Mgmt | `src/vs/sessions/contrib/aiCustomizationManagement/browser/SPEC.md` | Management editor specification | -| AI Customization Tree | `src/vs/sessions/contrib/aiCustomizationTreeView/browser/SPEC.md` | Tree view specification | If you modify the implementation, you **must** update the corresponding spec to keep it in sync. Update the Revision History table at the bottom of `LAYOUT.md` with a dated entry. @@ -62,44 +60,57 @@ src/vs/sessions/ ├── AI_CUSTOMIZATIONS.md # AI customization design document ├── sessions.common.main.ts # Common (browser + desktop) entry point ├── sessions.desktop.main.ts # Desktop entry point (imports all contributions) -├── common/ # Shared types and context keys -│ └── contextkeys.ts # ChatBar context keys +├── common/ # Shared types, context keys, and theme +│ ├── categories.ts # Command categories +│ ├── contextkeys.ts # ChatBar and welcome context keys +│ └── theme.ts # Theme contributions ├── browser/ # Core workbench implementation │ ├── workbench.ts # Main Workbench class (implements IWorkbenchLayoutService) │ ├── menus.ts # Agent sessions menu IDs (Menus export) │ ├── layoutActions.ts # Layout toggle actions (sidebar, panel, auxiliary bar) │ ├── paneCompositePartService.ts # AgenticPaneCompositePartService -│ ├── style.css # Layout-specific styles │ ├── widget/ # Agent sessions chat widget -│ │ ├── AGENTS_CHAT_WIDGET.md # Chat widget architecture doc -│ │ ├── agentSessionsChatWidget.ts # Main wrapper around ChatWidget -│ │ ├── agentSessionsChatTargetConfig.ts # Observable target state -│ │ ├── agentSessionsTargetPickerActionItem.ts # Target picker for input toolbar -│ │ └── media/ -│ └── parts/ # Workbench part implementations -│ ├── parts.ts # AgenticParts enum -│ ├── titlebarPart.ts # Titlebar (3-section toolbar layout) -│ ├── sidebarPart.ts # Sidebar (with footer for account widget) -│ ├── chatBarPart.ts # Chat Bar (primary chat surface) -│ ├── auxiliaryBarPart.ts # Auxiliary Bar (with run script dropdown) -│ ├── panelPart.ts # Panel (terminal, output, etc.) -│ ├── projectBarPart.ts # Project bar (folder entries) -│ ├── agentSessionsChatInputPart.ts # Chat input part adapter -│ ├── agentSessionsChatWelcomePart.ts # Welcome view (mascot + target buttons + pickers) -│ └── media/ # Part CSS files +│ │ └── AGENTS_CHAT_WIDGET.md # Chat widget architecture doc +│ ├── parts/ # Workbench part implementations +│ │ ├── parts.ts # AgenticParts enum +│ │ ├── titlebarPart.ts # Titlebar (3-section toolbar layout) +│ │ ├── sidebarPart.ts # Sidebar (with footer for account widget) +│ │ ├── chatBarPart.ts # Chat Bar (primary chat surface) +│ │ ├── auxiliaryBarPart.ts # Auxiliary Bar +│ │ ├── panelPart.ts # Panel (terminal, output, etc.) +│ │ ├── projectBarPart.ts # Project bar (folder entries) +│ │ └── media/ # Part CSS files +│ └── media/ # Layout-specific styles ├── electron-browser/ # Desktop-specific entry points │ ├── sessions.main.ts # Desktop main bootstrap │ ├── sessions.ts # Electron process entry │ ├── sessions.html # Production HTML shell -│ └── sessions-dev.html # Development HTML shell +│ ├── sessions-dev.html # Development HTML shell +│ ├── titleService.ts # Desktop title service override +│ └── parts/ +│ └── titlebarPart.ts # Desktop titlebar part +├── services/ # Service overrides +│ ├── configuration/browser/ # Configuration service overrides +│ └── workspace/browser/ # Workspace service overrides +├── test/ # Unit tests +│ └── browser/ +│ └── layoutActions.test.ts └── contrib/ # Feature contributions ├── accountMenu/browser/ # Account widget for sidebar footer - ├── aiCustomizationManagement/browser/ # AI customization management editor + ├── agentFeedback/browser/ # Agent feedback attachments, overlays, hover ├── aiCustomizationTreeView/browser/ # AI customization tree view sidebar + ├── applyToParentRepo/browser/ # Apply changes to parent repo ├── changesView/browser/ # File changes view ├── chat/browser/ # Chat actions (run script, branch, prompts) ├── configuration/browser/ # Configuration overrides - └── sessions/browser/ # Sessions view, title bar widget, active session service + ├── files/browser/ # File-related contributions + ├── fileTreeView/browser/ # File tree view (filesystem provider) + ├── gitSync/browser/ # Git sync contributions + ├── logs/browser/ # Log contributions + ├── sessions/browser/ # Sessions view, title bar widget, active session service + ├── terminal/browser/ # Terminal contributions + ├── welcome/browser/ # Welcome view contribution + └── workspace/browser/ # Workspace contributions ``` ## 4. Layout @@ -165,18 +176,21 @@ The agent sessions window uses **its own menu IDs** defined in `browser/menus.ts | Menu ID | Purpose | |---------|---------| -| `Menus.TitleBarLeft` | Left toolbar (toggle sidebar) | -| `Menus.TitleBarCenter` | Not used directly (see CommandCenter) | -| `Menus.TitleBarRight` | Right toolbar (run script, open, toggle auxiliary bar) | +| `Menus.ChatBarTitle` | Chat bar title actions | | `Menus.CommandCenter` | Center toolbar with session picker widget | -| `Menus.TitleBarControlMenu` | Submenu intercepted to render `SessionsTitleBarWidget` | +| `Menus.CommandCenterCenter` | Center section of command center | +| `Menus.TitleBarContext` | Titlebar context menu | +| `Menus.TitleBarLeftLayout` | Left layout toolbar | +| `Menus.TitleBarSessionTitle` | Session title in titlebar | +| `Menus.TitleBarSessionMenu` | Session menu in titlebar | +| `Menus.TitleBarRightLayout` | Right layout toolbar | | `Menus.PanelTitle` | Panel title bar actions | | `Menus.SidebarTitle` | Sidebar title bar actions | | `Menus.SidebarFooter` | Sidebar footer (account widget) | +| `Menus.SidebarCustomizations` | Sidebar customizations menu | | `Menus.AuxiliaryBarTitle` | Auxiliary bar title actions | | `Menus.AuxiliaryBarTitleLeft` | Auxiliary bar left title actions | -| `Menus.OpenSubMenu` | "Open..." split button (Open Terminal, Open in VS Code) | -| `Menus.ChatBarTitle` | Chat bar title actions | +| `Menus.AgentFeedbackEditorContent` | Agent feedback editor content menu | ## 7. Context Keys @@ -187,7 +201,7 @@ Defined in `common/contextkeys.ts`: | `activeChatBar` | `string` | ID of the active chat bar panel | | `chatBarFocus` | `boolean` | Whether chat bar has keyboard focus | | `chatBarVisible` | `boolean` | Whether chat bar is visible | - +| `sessionsWelcomeVisible` | `boolean` | Whether the sessions welcome overlay is visible | ## 8. Contributions Feature contributions live under `contrib//browser/` and are registered via imports in `sessions.desktop.main.ts` (desktop) or `sessions.common.main.ts` (browser-compatible). @@ -199,13 +213,18 @@ Feature contributions live under `contrib//browser/` and are regist | **Sessions View** | `contrib/sessions/browser/` | Sessions list in sidebar, session picker, active session service | | **Title Bar Widget** | `contrib/sessions/browser/sessionsTitleBarWidget.ts` | Session picker in titlebar center | | **Account Widget** | `contrib/accountMenu/browser/` | Account button in sidebar footer | -| **Run Script** | `contrib/chat/browser/runScriptAction.ts` | Run configured script in terminal | -| **Branch Chat Session** | `contrib/chat/browser/branchChatSessionAction.ts` | Branch a chat session | -| **Open in VS Code / Terminal** | `contrib/chat/browser/chat.contribution.ts` | Open worktree in VS Code or terminal | -| **Prompts Service** | `contrib/chat/browser/promptsService.ts` | Agentic prompts service override | +| **Chat Actions** | `contrib/chat/browser/` | Chat actions (run script, branch, prompts, customizations debug log) | | **Changes View** | `contrib/changesView/browser/` | File changes in auxiliary bar | -| **AI Customization Editor** | `contrib/aiCustomizationManagement/browser/` | Management editor for prompts, hooks, MCP, etc. | +| **Agent Feedback** | `contrib/agentFeedback/browser/` | Agent feedback attachments, editor overlays, hover | | **AI Customization Tree** | `contrib/aiCustomizationTreeView/browser/` | Sidebar tree for AI customizations | +| **Apply to Parent Repo** | `contrib/applyToParentRepo/browser/` | Apply changes to parent repo | +| **Files** | `contrib/files/browser/` | File-related contributions | +| **File Tree View** | `contrib/fileTreeView/browser/` | File tree view (filesystem provider) | +| **Git Sync** | `contrib/gitSync/browser/` | Git sync contributions | +| **Logs** | `contrib/logs/browser/` | Log contributions | +| **Terminal** | `contrib/terminal/browser/` | Terminal contributions | +| **Welcome** | `contrib/welcome/browser/` | Welcome view contribution | +| **Workspace** | `contrib/workspace/browser/` | Workspace contributions | | **Configuration** | `contrib/configuration/browser/` | Configuration overrides | ### 8.2 Service Overrides @@ -216,6 +235,10 @@ The agent sessions window registers its own implementations for: - `IPromptsService` → `AgenticPromptsService` (scopes prompt discovery to active session worktree) - `IActiveSessionService` → `ActiveSessionService` (tracks active session) +Service overrides also live under `services/`: +- `services/configuration/browser/` - configuration service overrides +- `services/workspace/browser/` - workspace service overrides + ### 8.3 `WindowVisibility.Sessions` Views and contributions that should only appear in the agent sessions window (not in regular VS Code) use `WindowVisibility.Sessions` in their registration. @@ -224,12 +247,14 @@ Views and contributions that should only appear in the agent sessions window (no | File | Purpose | |------|---------| -| `sessions.common.main.ts` | Common entry — imports browser-compatible services, workbench contributions | -| `sessions.desktop.main.ts` | Desktop entry — imports desktop services, electron contributions, all `contrib/` modules | +| `sessions.common.main.ts` | Common entry; imports browser-compatible services, workbench contributions | +| `sessions.desktop.main.ts` | Desktop entry; imports desktop services, electron contributions, all `contrib/` modules | | `electron-browser/sessions.main.ts` | Desktop bootstrap | | `electron-browser/sessions.ts` | Electron process entry | | `electron-browser/sessions.html` | Production HTML shell | | `electron-browser/sessions-dev.html` | Development HTML shell | +| `electron-browser/titleService.ts` | Desktop title service override | +| `electron-browser/parts/titlebarPart.ts` | Desktop titlebar part | ## 10. Development Guidelines @@ -243,7 +268,13 @@ Views and contributions that should only appear in the agent sessions window (no 6. Use agent session part classes, not standard workbench parts 7. Mark views with `WindowVisibility.Sessions` so they only appear in this window -### 10.2 Layout Changes +### 10.2 Validating Changes + +1. Run `npm run compile-check-ts-native` to run a repo-wide TypeScript compilation check (including `src/vs/sessions/`). This is a fast way to catch TypeScript errors introduced by your changes. +2. Run `npm run valid-layers-check` to verify layering rules are not violated. +3. Run tests under `src/vs/sessions/test/` to confirm nothing is broken. + +### 10.3 Layout Changes 1. **Read `LAYOUT.md` first** — it's the authoritative spec 2. Use the `agent-sessions-layout` skill for detailed implementation guidance diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9e9cc12ca99..e6bf967ddf0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -225,8 +225,7 @@ "windows": { "command": ".\\scripts\\code.bat" }, - "problemMatcher": [], - "inSessions": true + "problemMatcher": [] }, { "label": "Run Dev Sessions", @@ -238,7 +237,6 @@ "args": [ "--sessions" ], - "inSessions": true, "problemMatcher": [] }, { From 1f4b2e1a17bc4e5e8956874e5e3fbba2476f7da0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 05:56:40 +0100 Subject: [PATCH 121/448] modal - focus editor on title click (#299038) Co-authored-by: Dmitriy Vasyura --- .../workbench/browser/parts/editor/modalEditorPart.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index f53f8530a87..a5bf50ce859 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -119,7 +119,6 @@ export class ModalEditorPart { role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId, - tabIndex: -1 }); shadowElement.appendChild(editorPartContainer); @@ -230,6 +229,12 @@ export class ModalEditorPart { editorPart.toggleMaximized(); })); + // Focus active editor when clicking into the title area with no other click target + disposables.add(addDisposableListener(headerElement, EventType.CLICK, e => { + EventHelper.stop(e); + + editorPart.activeGroup.focus(); + })); // Layout the modal editor part const layoutModal = () => { @@ -270,8 +275,8 @@ export class ModalEditorPart { this.hostService.setWindowDimmed(mainWindow, true); disposables.add(toDisposable(() => this.hostService.setWindowDimmed(mainWindow, false))); - // Focus the modal - editorPartContainer.focus(); + // Focus + editorPart.activeGroup.focus(); return { part: editorPart, From a7f87d92f9ee0ecd38ee866bf0e884ddbf8cb2d6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 05:57:06 +0100 Subject: [PATCH 122/448] sessions - allow to open preview from markdown files (#299047) * sessions - allow to open preview from markdown files * Update src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/markdownPreview.contribution.ts | 24 +++++++++++++++++++ src/vs/sessions/sessions.desktop.main.ts | 1 + 2 files changed, 25 insertions(+) create mode 100644 src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts diff --git a/src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts b/src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts new file mode 100644 index 00000000000..f186d71637d --- /dev/null +++ b/src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../nls.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; + +// Show a floating "Open Preview" button in the editor content +// area when editing markdown or related prompt/instructions/chatagent/skill +// language content in the sessions window. +MenuRegistry.appendMenuItem(MenuId.EditorContent, { + command: { + id: 'markdown.showPreviewToSide', + title: localize('openPreview', "Open Preview"), + }, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + ContextKeyExpr.regex(EditorContextKeys.languageId.key, /^(markdown|prompt|instructions|chatagent|skill)$/), + ), +}); diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 4b35ada8541..17d622826eb 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -212,6 +212,7 @@ import './contrib/gitSync/browser/gitSync.contribution.js'; import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; +import './contrib/markdownPreview/browser/markdownPreview.contribution.js'; import './contrib/terminal/browser/sessionsTerminalContribution.js'; import './contrib/logs/browser/logs.contribution.js'; From da27892b6d6607425b7f0eb29235e9b456e277a3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 05:57:13 +0100 Subject: [PATCH 123/448] sessions - tweaks to empty message (#299034) --- .../browser/agentSessions/agentSessionsControl.ts | 14 +++++++------- .../agentSessions/media/agentsessionsviewer.css | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 727d81d2f94..57ca104e997 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -157,10 +157,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo hide(this.emptyFilterMessage); const span = append(this.emptyFilterMessage, $('span')); - span.textContent = localize('agentSessions.noFilterResults', "No matching sessions."); + span.textContent = `${localize('agentSessions.noFilterResults', "No matching sessions")} - `; const link = append(this.emptyFilterMessage, $('span.reset-filter-link')); - link.textContent = localize('agentSessions.clearFilters', "Clear Filter"); + link.textContent = localize('agentSessions.resetFilter', "Reset Filter"); link.tabIndex = 0; link.setAttribute('role', 'button'); this._register(addDisposableListener(link, EventType.CLICK, () => this.options.filter.reset())); @@ -235,7 +235,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo })); this._register(sessionFilter.onDidGetChildren(count => { - this.updateEmptyFilterMessage(count); + this.updateEmpty(count === 0); })); const model = this.agentSessionsService.model; @@ -287,18 +287,18 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo })); } - private updateEmptyFilterMessage(visibleChildren: number): void { + private updateEmpty(isEmpty: boolean): void { if (!this.emptyFilterMessage || !this.sessionsList) { return; } const model = this.agentSessionsService.model; const hasSessionsInModel = model.sessions.length > 0; - const hasVisibleChildren = visibleChildren > 0; const isFilterActive = !this.options.filter.isDefault(); - const showMessage = hasSessionsInModel && !hasVisibleChildren && isFilterActive; - setVisibility(showMessage, this.emptyFilterMessage); + const showEmpty = hasSessionsInModel && isEmpty && isFilterActive; + setVisibility(showEmpty, this.emptyFilterMessage); + setVisibility(!showEmpty, this.sessionsList.getHTMLElement()); } private hasTodaySessions(): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 3481a4a075d..3ee43124df5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -352,7 +352,6 @@ color: var(--vscode-descriptionForeground); .reset-filter-link { - margin-left: 4px; color: var(--vscode-textLink-foreground); cursor: pointer; text-decoration: none; From 575c39d1606f3d7318850530884fbdcb5285f9ee Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 05:58:02 +0100 Subject: [PATCH 124/448] sessions - fix close action showing up in modal editors when tabbed (#299041) --- .../contrib/configuration/browser/configuration.contribution.ts | 1 - src/vs/workbench/browser/parts/editor/editorTitleControl.ts | 1 + 2 files changed, 1 insertion(+), 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 25c9f5cb914..843068b74d6 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -41,7 +41,6 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'terminal.integrated.initialHint': false, 'workbench.editor.restoreEditors': false, - 'workbench.editor.showTabs': 'single', 'workbench.startupEditor': 'none', 'workbench.tips.enabled': false, 'workbench.layoutControl.type': 'toggles', diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index 00f7bf67590..132ca85f165 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -176,6 +176,7 @@ export class EditorTitleControl extends Themable { } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { + // Update editor tabs control if options changed if ( oldOptions.showTabs !== newOptions.showTabs || From b16ecea6152e2b4466ab1a767ab384596296b3aa Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Mar 2026 16:05:06 +1100 Subject: [PATCH 125/448] feat: enhance chat session option updates (#299109) --- .../browser/widget/input/chatInputPart.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 87a0817bba6..9423e2431e4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -664,10 +664,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (sessionResource) { const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); if (ctx) { - this.chatSessionsService.notifySessionOptionsChange( - ctx.chatSessionResource, - [{ optionId: agentOptionId, value: mode.isBuiltin ? '' : modeName }] - ).catch(err => this.logService.error('Failed to notify extension of agent change:', err)); + let needsUpdate = true; + const agentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, agentOptionId); + if (typeof agentOption !== 'undefined') { + const agentId = (typeof agentOption === 'string' ? agentOption : agentOption.id) || ChatMode.Agent.id; + const isDefaultAgent = agentId === ChatMode.Agent.id; + needsUpdate = isDefaultAgent + ? mode.id !== ChatMode.Agent.id + : mode.label.read(undefined) !== agentId; // Extensions use Label (name) as identifier for custom agents. + } + if (needsUpdate) { + this.chatSessionsService.notifySessionOptionsChange( + ctx.chatSessionResource, + [{ optionId: agentOptionId, value: mode.isBuiltin ? '' : modeName }] + ).catch(err => this.logService.error('Failed to notify extension of agent change:', err)); + } } } })); From f4e743c4abdd309d99fc1425228281da2ed0e039 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Mar 2026 17:10:14 +1100 Subject: [PATCH 126/448] Ensure to update ChatInputPart state before updating viewmodel (#299119) --- src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 09a203bd5b0..8a391fa71d6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1924,10 +1924,13 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection.clear(); - this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined); - + // Set the input model on the inputPart before assigning this.viewModel. Assigning this.viewModel + // fires onDidChangeViewModel, which ChatInputPart listens to and expects the input model to be initialized. // Pass input model reference to input part for state syncing this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0); + + this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined); + this.listWidget.setViewModel(this.viewModel); if (this._lockedAgent) { From 719f750060e81659a46914affe7305e753d37457 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 3 Mar 2026 22:15:44 -0800 Subject: [PATCH 127/448] Add /troubleshoot command for access to debug logs (#299024) --- .../actions/chatOpenAgentDebugPanelAction.ts | 4 +- .../attachments/chatAttachmentWidgets.ts | 10 + .../chat/browser/chatDebug/chatDebugEditor.ts | 8 +- .../browser/chatDebug/chatDebugFilters.ts | 84 +++++++ .../browser/chatDebug/chatDebugLogsView.ts | 32 ++- .../chat/browser/chatDebug/chatDebugTypes.ts | 2 + .../browser/chatDebug/media/chatDebug.css | 7 + .../contrib/chat/browser/chatSlashCommands.ts | 105 ++++++++- .../chatReferencesContentPart.ts | 3 +- .../input/editor/chatInputCompletions.ts | 4 +- .../chat/common/actions/chatContextKeys.ts | 1 + .../common/attachments/chatVariableEntries.ts | 11 +- .../common/chatService/chatServiceImpl.ts | 5 +- .../common/participants/chatSlashCommands.ts | 10 +- .../resolveDebugEventDetailsTool.ts | 188 +++++++++++++++ .../chat/common/tools/builtinTools/tools.ts | 5 + .../test/browser/chatDebugFilters.test.ts | 220 ++++++++++++++++++ 17 files changed, 680 insertions(+), 19 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts index 9fefe82c3ac..860559d64e3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts @@ -67,7 +67,7 @@ export function registerChatOpenAgentDebugPanelAction() { }); } - async run(accessor: ServicesAccessor, context?: URI | unknown): Promise { + async run(accessor: ServicesAccessor, context?: URI | unknown, filter?: string): Promise { const editorService = accessor.get(IEditorService); const chatWidgetService = accessor.get(IChatWidgetService); const chatDebugService = accessor.get(IChatDebugService); @@ -88,7 +88,7 @@ export function registerChatOpenAgentDebugPanelAction() { } chatDebugService.activeSessionResource = sessionResource; - const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs' }; + const options: IChatDebugEditorOptions = { pinned: true, sessionResource, viewHint: 'logs', filter }; await editorService.openEditor(ChatDebugEditorInput.instance, options); } }); diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index c1763a79436..8a20bd70982 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -640,6 +640,16 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { })); } + // Handle click for debug events attachments + if (attachment.kind === 'debugEvents') { + this.element.style.cursor = 'pointer'; + this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => { + const d = new Date(attachment.snapshotTime); + const filter = `before:${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; + this.commandService.executeCommand('workbench.action.chat.openAgentDebugPanelForSession', attachment.sessionResource, filter); + })); + } + // Setup tooltip hover for string context attachments if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) { this._setupTooltipHover(attachment.tooltip); diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index e9b7160ea45..ec403b30f95 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -341,7 +341,7 @@ export class ChatDebugEditor extends EditorPane { } private _applyNavigationOptions(options: IChatDebugEditorOptions): void { - const { sessionResource, viewHint } = options; + const { sessionResource, viewHint, filter } = options; if (viewHint === 'logs' && sessionResource) { this.navigateToSession(sessionResource, 'logs'); } else if (viewHint === 'flowchart' && sessionResource) { @@ -356,6 +356,12 @@ export class ChatDebugEditor extends EditorPane { } else if (this.viewState === ViewState.Home) { this.showView(ViewState.Home); } + + // Apply filter text if provided (e.g. from debug events snapshot) + if (filter !== undefined && this.filterState) { + this.filterState.setTextFilter(filter); + this.logsView?.setFilterText(filter); + } } override layout(dimension: Dimension): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts index 8f25da718a1..fefa283cceb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFilters.ts @@ -38,6 +38,10 @@ export class ChatDebugFilterState extends Disposable { // Text filter textFilter: string = ''; + // Parsed timestamp filters (epoch ms) + beforeTimestamp: number | undefined; + afterTimestamp: number | undefined; + isKindVisible(kind: string, category?: string): boolean { switch (kind) { case 'toolCall': return this.filterKindToolCall; @@ -70,10 +74,90 @@ export class ChatDebugFilterState extends Disposable { const normalized = text.toLowerCase(); if (this.textFilter !== normalized) { this.textFilter = normalized; + this._parseTimestampFilters(normalized); this._onDidChange.fire(); } } + setBeforeTimestamp(timestamp: number | undefined): void { + if (this.beforeTimestamp !== timestamp) { + this.beforeTimestamp = timestamp; + this._onDidChange.fire(); + } + } + + /** + * Parse `before:YYYY[-MM[-DD[THH[:MM[:SS]]]]]` from the filter text. + * Each component after the year is optional. + */ + private _parseTimestampFilters(text: string): void { + this.beforeTimestamp = ChatDebugFilterState.parseTimeToken(text, 'before'); + this.afterTimestamp = ChatDebugFilterState.parseTimeToken(text, 'after'); + } + + static parseTimeToken(text: string, prefix: string): number | undefined { + const regex = new RegExp(`${prefix}:(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:t(\\d{1,2})(?::(\\d{2})(?::(\\d{2}))?)?)?)?)?(?!\\w)`); + const m = regex.exec(text); + if (!m) { + return undefined; + } + + const year = parseInt(m[1], 10); + const month = m[2] !== undefined ? parseInt(m[2], 10) - 1 : undefined; + const day = m[3] !== undefined ? parseInt(m[3], 10) : undefined; + const hour = m[4] !== undefined ? parseInt(m[4], 10) : undefined; + const minute = m[5] !== undefined ? parseInt(m[5], 10) : undefined; + const second = m[6] !== undefined ? parseInt(m[6], 10) : undefined; + + // For 'before:', round up to the end of the most specific unit given. + // For 'after:', use the start of the most specific unit. + if (prefix === 'before') { + if (second !== undefined) { + return new Date(year, month!, day!, hour!, minute!, second, 999).getTime(); + } else if (minute !== undefined) { + return new Date(year, month!, day!, hour!, minute, 59, 999).getTime(); + } else if (hour !== undefined) { + return new Date(year, month!, day!, hour, 59, 59, 999).getTime(); + } else if (day !== undefined) { + return new Date(year, month!, day, 23, 59, 59, 999).getTime(); + } else if (month !== undefined) { + // End of the given month + return new Date(year, month + 1, 0, 23, 59, 59, 999).getTime(); + } else { + // End of the given year + return new Date(year, 11, 31, 23, 59, 59, 999).getTime(); + } + } else { + return new Date( + year, + month ?? 0, + day ?? 1, + hour ?? 0, + minute ?? 0, + second ?? 0, + 0, + ).getTime(); + } + } + + /** Returns the text filter with before:/after: tokens removed. */ + get textFilterWithoutTimestamps(): string { + return this.textFilter + .replace(/\b(?:before|after):\d{4}(?:-\d{2}(?:-\d{2}(?:t\d{1,2}(?::\d{2}(?::\d{2})?)?)?)?)?\b/g, '') + .trim(); + } + + isTimestampVisible(created: Date): boolean { + const time = created.getTime(); + if (this.beforeTimestamp !== undefined && time > this.beforeTimestamp) { + return false; + } + if (this.afterTimestamp !== undefined && time < this.afterTimestamp) { + return false; + } + return true; + } + fire(): void { this._onDidChange.fire(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index 5fa4cde6b6f..a9bbec0b6d5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -28,6 +28,7 @@ import { ChatDebugEventRenderer, ChatDebugEventDelegate, ChatDebugEventTreeRende import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem, LogsViewMode } from './chatDebugTypes.js'; import { ChatDebugFilterState, bindFilterContextKeys } from './chatDebugFilters.js'; import { ChatDebugDetailPanel } from './chatDebugDetailPanel.js'; +import { IChatWidgetService } from '../chat.js'; const $ = DOM.$; @@ -70,6 +71,7 @@ export class ChatDebugLogsView extends Disposable { @IChatDebugService private readonly chatDebugService: IChatDebugService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); this.container = DOM.append(parent, $('.chat-debug-logs')); @@ -104,7 +106,7 @@ export class ChatDebugLogsView extends Disposable { new ServiceCollection([IContextKeyService, scopedContextKeyService]) )); this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, { - placeholder: localize('chatDebug.search', "Filter (e.g. text, !exclude)"), + placeholder: localize('chatDebug.search', "Filter (e.g. text, !exclude, before:YYYY-MM-DDTHH:MM:SS)"), ariaLabel: localize('chatDebug.filterAriaLabel', "Filter debug events"), })); @@ -119,6 +121,23 @@ export class ChatDebugLogsView extends Disposable { const filterContainer = DOM.append(this.headerContainer, $('.viewpane-filter-container')); filterContainer.appendChild(this.filterWidget.element); + // Troubleshoot button + const troubleshootButton = this._register(new Button(this.headerContainer, { ...defaultButtonStyles, secondary: true, title: localize('chatDebug.troubleshoot', "Add snapshot to Chat") })); + troubleshootButton.element.classList.add('chat-debug-troubleshoot-button', 'monaco-text-button'); + DOM.append(troubleshootButton.element, $(`span${ThemeIcon.asCSSSelector(Codicon.chatSparkle)}`)); + this._register(troubleshootButton.onDidClick(async () => { + if (!this.currentSessionResource) { + return; + } + const widget = await this.chatWidgetService.openSession(this.currentSessionResource); + if (widget) { + const value = '/troubleshoot '; + widget.inputEditor.setValue(value); + widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + widget.focusInput(); + } + })); + this._register(this.filterWidget.onDidChangeFilterText(text => { this.filterState.setTextFilter(text); })); @@ -241,6 +260,10 @@ export class ChatDebugLogsView extends Disposable { this.currentSessionResource = sessionResource; } + setFilterText(text: string): void { + this.filterWidget.setFilterText(text); + } + show(): void { DOM.show(this.container); this.loadEvents(); @@ -297,8 +320,11 @@ export class ChatDebugLogsView extends Disposable { return this.filterState.isKindVisible(e.kind, category); }); - // Filter by text search - const filterText = this.filterState.textFilter; + // Filter by timestamp (before:/after: syntax) + filtered = filtered.filter(e => this.filterState.isTimestampVisible(e.created)); + + // Filter by text search (excluding before:/after: tokens) + const filterText = this.filterState.textFilterWithoutTimestamps; if (filterText) { const terms = filterText.split(/\s*,\s*/).filter(t => t.length > 0); const includeTerms = terms.filter(t => !t.startsWith('!')).map(t => t.trim()); diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts index c3469734712..a6ac1bc9799 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugTypes.ts @@ -19,6 +19,8 @@ const $ = DOM.$; export interface IChatDebugEditorOptions extends IEditorOptions { readonly sessionResource?: URI; readonly viewHint?: 'home' | 'overview' | 'logs' | 'flowchart'; + /** When set, automatically applies this text as the log filter. */ + readonly filter?: string; } export const enum ViewState { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css b/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css index 93068162a11..7efa2352ee7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/media/chatDebug.css @@ -283,6 +283,7 @@ .chat-debug-editor-header .viewpane-filter-container { flex: 1; max-width: 500px; + margin-right: auto; } .chat-debug-editor-header .viewpane-filter-container .monaco-inputbox { border-color: var(--vscode-panelInput-border, transparent) !important; @@ -293,6 +294,12 @@ align-items: center; gap: 6px; } +.chat-debug-troubleshoot-button.monaco-button { + width: auto; + display: inline-flex; + align-items: center; + flex-shrink: 0; +} .chat-debug-view-mode-labels { display: grid; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 3570a436c48..97e45f02809 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -15,9 +15,11 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IChatAgentService } from '../common/participants/chatAgents.js'; +import { IChatDebugEvent, IChatDebugService } from '../common/chatDebugService.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; -import { IChatService } from '../common/chatService/chatService.js'; +import { ChatRequestQueueKind, IChatService } from '../common/chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js'; +import { IChatRequestVariableEntry } from '../common/attachments/chatVariableEntries.js'; import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; import { ChatSubmitAction, OpenModePickerAction, OpenModelPickerAction } from './actions/chatExecuteActions.js'; import { ManagePluginsAction } from './actions/chatPluginActions.js'; @@ -29,11 +31,15 @@ import { CONFIGURE_PROMPTS_ACTION_ID } from './promptSyntax/runPromptAction.js'; import { CONFIGURE_SKILLS_ACTION_ID } from './promptSyntax/skillActions.js'; import { AutoApproveStorageKeys, - globalAutoApproveDescription + globalAutoApproveDescription, } from './tools/languageModelToolsService.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; +import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { Target } from '../common/promptSyntax/promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IChatWidgetService } from './chat.js'; export class ChatSlashCommandsContribution extends Disposable { @@ -46,13 +52,25 @@ export class ChatSlashCommandsContribution extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, + @IChatDebugService chatDebugService: IChatDebugService, @IConfigurationService configurationService: IConfigurationService, @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @IChatWidgetService chatWidgetService: IChatWidgetService, ) { super(); + + const troubleshootSessions = new Set(); + const hasTroubleshootDataKey = ChatContextKeys.chatSessionHasTroubleshootData.bindTo(this.contextKeyService); + this._store.add(chatWidgetService.onDidChangeFocusedSession(() => { + const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource; + hasTroubleshootDataKey.set(!!sessionResource && troubleshootSessions.has(sessionResource.toString())); + languageModelToolsService.flushToolUpdates(); + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'clear', detail: nls.localize('clear', "Start a new chat and archive the current one"), @@ -119,6 +137,54 @@ export class ChatSlashCommandsContribution extends Disposable { await commandService.executeCommand('github.copilot.debug.showChatLogView'); })); } + this._store.add(slashCommandService.registerSlashCommand({ + command: 'troubleshoot', + detail: nls.localize('troubleshoot', "Troubleshoot with a snapshot of debug events from the conversation so far (run again to refresh)"), + sortText: 'z3_troubleshoot', + executeImmediately: false, + silent: true, + locations: [ChatAgentLocation.Chat], + }, async (prompt, _progress, _history, _location, sessionResource, _token, options) => { + troubleshootSessions.add(sessionResource.toString()); + hasTroubleshootDataKey.set(true); + languageModelToolsService.flushToolUpdates(); + await chatDebugService.invokeProviders(sessionResource); + const events = chatDebugService.getEvents(sessionResource); + const summary = events.length > 0 + ? formatDebugEventsForContext(events) + : nls.localize('troubleshoot.noEvents', "No debug events found for this conversation."); + + const attachedContext: IChatRequestVariableEntry[] = [{ + id: 'chatDebugEvents', + name: nls.localize('troubleshoot.contextName', "Debug Events Snapshot"), + kind: 'debugEvents', + snapshotTime: Date.now(), + sessionResource, + value: summary, + modelDescription: 'These are the debug event logs from the current chat conversation. Analyze them to help answer the user\'s troubleshooting question.\n' + + '\n' + + 'CRITICAL INSTRUCTION: You MUST call the resolveDebugEventDetails tool on relevant events BEFORE answering. The log lines below are only summaries — they do NOT contain the actual data (file paths, prompt content, tool I/O, etc.). The real information is only available by resolving events. Never answer based solely on the summary lines. Always resolve first, then answer.\n' + + '\n' + + 'Call resolveDebugEventDetails in parallel on all events that could be relevant to the user\'s question. When in doubt, resolve more events rather than fewer.\n' + + '\n' + + 'IMPORTANT: Do NOT mention event IDs, tool resolution steps, or internal debug mechanics in your response. The user does not know about debug events or event IDs. Present your findings directly and naturally, as if you simply know the answer. Never say things like "I need to resolve events" or show event IDs.\n' + + '\n' + + 'Event types and what resolving them returns:\n' + + '- generic (category: "discovery"): File discovery for instructions, skills, agents, hooks. Resolving returns a fileList with full file paths, load status, skip reasons, and source folders. Always resolve these for questions about customization files.\n' + + '- userMessage: The full prompt sent to the model. Resolving returns the complete message and all prompt sections (system prompt, instructions, context). Essential for understanding what the model received.\n' + + '- agentResponse: The model\'s response. Resolving returns the full response text and sections.\n' + + '- modelTurn: An LLM round-trip. Resolving returns model name, token usage, timing, errors, and prompt sections.\n' + + '- toolCall: A tool invocation. Resolving returns tool name, input, output, status, and duration.\n' + + '- subagentInvocation: A sub-agent spawn. Resolving returns agent name, status, duration, and counts.\n' + + '- generic (other): Miscellaneous logs. Resolving returns additional text details.', + }]; + + chatService.sendRequest(sessionResource, prompt, { + ...options, + queue: ChatRequestQueueKind.Queued, + attachedContext, + }); + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'agents', detail: nls.localize('agents', "Configure custom agents"), @@ -347,3 +413,38 @@ export class ChatSlashCommandsContribution extends Disposable { })); } } + +function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string { + const lines: string[] = []; + for (const event of events) { + const ts = event.created.toISOString(); + const id = event.id ? ` [id=${event.id}]` : ''; + switch (event.kind) { + case 'generic': + lines.push(`[${ts}]${id} ${event.level >= 3 ? 'ERROR' : event.level >= 2 ? 'WARN' : 'INFO'}: ${event.name}${event.details ? ' - ' + event.details : ''}${event.category ? ' (category: ' + event.category + ')' : ''}`); + break; + case 'toolCall': + lines.push(`[${ts}]${id} TOOL_CALL: ${event.toolName}${event.result ? ' result=' + event.result : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'modelTurn': + lines.push(`[${ts}]${id} MODEL_TURN: ${event.requestName ?? 'unknown'}${event.model ? ' model=' + event.model : ''}${event.inputTokens !== undefined ? ' tokens(in=' + event.inputTokens + ',out=' + (event.outputTokens ?? '?') + ')' : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'subagentInvocation': + lines.push(`[${ts}]${id} SUBAGENT: ${event.agentName}${event.status ? ' status=' + event.status : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'userMessage': + lines.push(`[${ts}]${id} USER_MESSAGE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); + break; + case 'agentResponse': + lines.push(`[${ts}]${id} AGENT_RESPONSE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); + break; + default: { + const _: never = event; + void _; + break; + } + } + } + return lines.join('\n'); +} + diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts index 6ff863773db..b0edcc5b860 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -389,8 +389,7 @@ class CollapsibleListRenderer implements IListRenderer { const withSlash = `/${c.command}`; return { - label: withSlash, + label: { label: withSlash, description: c.detail }, insertText: c.executeImmediately ? '' : `${withSlash} `, documentation: c.detail, range, @@ -192,7 +192,7 @@ class SlashCommandCompletions extends Disposable { suggestions: slashCommands.map((c, i): CompletionItem => { const withSlash = `${chatSubcommandLeader}${c.command}`; return { - label: withSlash, + label: { label: withSlash, description: c.detail }, insertText: c.executeImmediately ? '' : `${withSlash} `, documentation: c.detail, range, diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index a85c46b7bfb..926ba9d9f97 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -89,6 +89,7 @@ export namespace ChatContextKeys { export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); export const hasPendingRequests = new RawContextKey('chatHasPendingRequests', false, { type: 'boolean', description: localize('chatHasPendingRequests', "True when there are pending requests in the queue.") }); export const chatSessionHasDebugData = new RawContextKey('chatSessionHasDebugData', false, { type: 'boolean', description: localize('chatSessionHasDebugData', "True when the current chat session has debug log data.") }); + export const chatSessionHasTroubleshootData = new RawContextKey('chatSessionHasTroubleshootData', false, { type: 'boolean', description: localize('chatSessionHasTroubleshootData', "True when the /troubleshoot slash command has been run in the current chat session.") }); export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 5655cc695fd..fbf760fe84d 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -308,13 +308,22 @@ export interface IAgentFeedbackVariableEntry extends IBaseChatRequestVariableEnt }>; } +export interface IChatRequestDebugEventsVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'debugEvents'; + /** Timestamp when the debug events were snapshotted. */ + readonly snapshotTime: number; + /** The session resource these debug events belong to. */ + readonly sessionResource: URI; +} + export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry | IChatRequestToolSetEntry | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry | IPromptFileVariableEntry | IPromptTextVariableEntry | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry | ITerminalVariableEntry - | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry | IDebugVariableEntry | IAgentFeedbackVariableEntry; + | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry | IDebugVariableEntry | IAgentFeedbackVariableEntry + | IChatRequestDebugEventsVariableEntry; export namespace IChatRequestVariableEntry { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index b5ed3c924c0..0d1bb0635de 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1135,7 +1135,7 @@ export class ChatService extends Disposable implements IChatService { const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { progressCallback([p]); - }), history, location, model.sessionResource, token); + }), history, location, model.sessionResource, token, options); agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); rawResult = {}; @@ -1146,6 +1146,9 @@ export class ChatService extends Disposable implements IChatService { if ((token.isCancellationRequested && !rawResult)) { return; } else if (!request) { + // Silent slash command completed successfully — allow queued + // requests to proceed. + shouldProcessPending = !token.isCancellationRequested; return; } else { if (!rawResult) { diff --git a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts index 26cf4257f0a..35c609e3c7d 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatSlashCommands.ts @@ -9,7 +9,7 @@ import { Disposable, IDisposable, toDisposable } from '../../../../../base/commo import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IProgress } from '../../../../../platform/progress/common/progress.js'; import { IChatMessage } from '../languageModels.js'; -import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from '../chatService/chatService.js'; +import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData, IChatSendRequestOptions } from '../chatService/chatService.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -44,7 +44,7 @@ export interface IChatSlashData { export interface IChatSlashFragment { content: string | { treeData: IChatResponseProgressFileTreeData }; } -export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; +export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken, options?: IChatSendRequestOptions): Promise<{ followUp: IChatFollowup[] } | void> }; export const IChatSlashCommandService = createDecorator('chatSlashCommandService'); @@ -55,7 +55,7 @@ export interface IChatSlashCommandService { _serviceBrand: undefined; readonly onDidChangeCommands: Event; registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable; - executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; + executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken, options?: IChatSendRequestOptions): Promise<{ followUp: IChatFollowup[] } | void>; getCommands(location: ChatAgentLocation, mode: ChatModeKind): Array; hasCommand(id: string): boolean; } @@ -105,7 +105,7 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom return this._commands.has(id); } - async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { + async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken, options?: IChatSendRequestOptions): Promise<{ followUp: IChatFollowup[] } | void> { const data = this._commands.get(id); if (!data) { throw new Error('No command with id ${id} NOT registered'); @@ -117,6 +117,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom throw new Error(`No command with id ${id} NOT resolved`); } - return await data.command(prompt, progress, history, location, sessionResource, token); + return await data.command(prompt, progress, history, location, sessionResource, token, options); } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts new file mode 100644 index 00000000000..b3ff4593a43 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { localize } from '../../../../../../nls.js'; +import { ChatContextKeys } from '../../actions/chatContextKeys.js'; +import { IChatDebugEvent, IChatDebugResolvedEventContent, IChatDebugService } from '../../chatDebugService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js'; + +export const ResolveDebugEventDetailsToolId = 'vscode_resolveDebugEventDetails_internal'; + +export const ResolveDebugEventDetailsToolData: IToolData = { + id: ResolveDebugEventDetailsToolId, + toolReferenceName: 'resolveDebugEventDetails', + displayName: localize('resolveDebugEventDetails.displayName', "Resolve Debug Event Details"), + when: ChatContextKeys.chatSessionHasTroubleshootData, + canBeReferencedInPrompt: false, + modelDescription: 'Resolves the full details for a specific chat debug event by its event ID. Use this tool to get detailed information about a debug event such as tool call input/output, model turn details, user message sections, or file lists. The event ID can be found in the debug event log summary provided in the conversation context.', + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + eventId: { + type: 'string', + description: 'The ID of the debug event to resolve details for.', + }, + }, + required: ['eventId'], + }, +}; + +function formatResolvedContent(content: IChatDebugResolvedEventContent): string { + switch (content.kind) { + case 'text': + return content.value; + case 'fileList': { + const lines: string[] = [`File list (${content.discoveryType}):`]; + if (content.sourceFolders) { + for (const folder of content.sourceFolders) { + lines.push(` Source folder: ${folder.uri.toString()} (${folder.storage}, ${folder.fileCount} files${folder.exists ? '' : ', missing'})`); + } + } + for (const file of content.files) { + const status = file.status === 'loaded' ? 'loaded' : `skipped${file.skipReason ? `: ${file.skipReason}` : ''}`; + lines.push(` ${file.uri.toString()} [${status}]`); + } + return lines.join('\n'); + } + case 'message': { + const lines: string[] = [`${content.type === 'user' ? 'User' : 'Agent'} message: ${content.message}`]; + for (const section of content.sections) { + lines.push(`--- ${section.name} ---`); + lines.push(section.content); + } + return lines.join('\n'); + } + case 'toolCall': { + const lines: string[] = [`Tool call: ${content.toolName}`]; + if (content.result) { + lines.push(`Result: ${content.result}`); + } + if (content.durationInMillis !== undefined) { + lines.push(`Duration: ${content.durationInMillis}ms`); + } + if (content.input) { + lines.push(`Input:\n${content.input}`); + } + if (content.output) { + lines.push(`Output:\n${content.output}`); + } + return lines.join('\n'); + } + case 'modelTurn': { + const lines: string[] = [`Model turn: ${content.requestName}`]; + if (content.model) { + lines.push(`Model: ${content.model}`); + } + if (content.status) { + lines.push(`Status: ${content.status}`); + } + if (content.durationInMillis !== undefined) { + lines.push(`Duration: ${content.durationInMillis}ms`); + } + if (content.inputTokens !== undefined || content.outputTokens !== undefined) { + lines.push(`Tokens: input=${content.inputTokens ?? '?'}, output=${content.outputTokens ?? '?'}, cached=${content.cachedTokens ?? '?'}, total=${content.totalTokens ?? '?'}`); + } + if (content.errorMessage) { + lines.push(`Error: ${content.errorMessage}`); + } + if (content.sections) { + for (const section of content.sections) { + lines.push(`--- ${section.name} ---`); + lines.push(section.content); + } + } + return lines.join('\n'); + } + default: { + const _: never = content; + return JSON.stringify(_); + } + } +} + +function truncate(text: string, maxLength = 30): string { + if (text.length <= maxLength) { + return text; + } + const lastSpace = text.lastIndexOf(' ', maxLength); + const cutoff = lastSpace > maxLength / 2 ? lastSpace : maxLength; + return text.substring(0, cutoff) + '\u2026'; +} + +function getEventLabel(event: IChatDebugEvent): string { + switch (event.kind) { + case 'generic': return event.name; + case 'toolCall': return event.toolName; + case 'modelTurn': return event.requestName ?? localize('debugEvent.modelTurn', "Model Turn"); + case 'userMessage': return localize('debugEvent.userMessage', "User Message: {0}", truncate(event.message)); + case 'agentResponse': return localize('debugEvent.agentResponse', "Agent Response: {0}", truncate(event.message)); + case 'subagentInvocation': return event.agentName; + } +} + +export class ResolveDebugEventDetailsTool implements IToolImpl { + constructor( + @IChatDebugService private readonly chatDebugService: IChatDebugService, + ) { } + + async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + const eventId = context.parameters?.eventId; + let eventLabel: string | undefined; + if (typeof eventId === 'string' && context.chatSessionResource) { + const events = this.chatDebugService.getEvents(context.chatSessionResource); + const event = events.find(e => e.id === eventId); + if (event) { + eventLabel = getEventLabel(event); + } + } + + if (eventLabel) { + return { + invocationMessage: localize('resolveDebugEventDetails.invocationMessageNamed', 'Resolving details for "{0}"', eventLabel), + pastTenseMessage: localize('resolveDebugEventDetails.pastTenseMessageNamed', 'Resolved details for "{0}"', eventLabel), + }; + } + return { + invocationMessage: localize('resolveDebugEventDetails.invocationMessage', 'Resolving debug event details'), + pastTenseMessage: localize('resolveDebugEventDetails.pastTenseMessage', 'Resolved debug event details'), + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const eventId = invocation.parameters['eventId']; + if (typeof eventId !== 'string' || !eventId) { + return { + content: [{ kind: 'text', value: 'Error: eventId parameter is required.' }], + }; + } + + const sessionResource = invocation.context?.sessionResource; + if (!sessionResource) { + return { + content: [{ kind: 'text', value: 'Error: no chat session context available.' }], + }; + } + + const sessionEvents = this.chatDebugService.getEvents(sessionResource); + if (!sessionEvents.some(e => e.id === eventId)) { + return { + content: [{ kind: 'text', value: `No event with ID "${eventId}" found in the current session.` }], + }; + } + + const resolved = await this.chatDebugService.resolveEvent(eventId); + if (!resolved) { + return { + content: [{ kind: 'text', value: `No details found for event ID: ${eventId}` }], + }; + } + + return { + content: [{ kind: 'text', value: formatResolvedContent(resolved) }], + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 0258444f00b..619e63406dd 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -11,6 +11,7 @@ import { AskQuestionsTool, AskQuestionsToolData } from './askQuestionsTool.js'; import { ConfirmationTool, ConfirmationToolData, ConfirmationToolWithOptionsData } from './confirmationTool.js'; import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; +import { ResolveDebugEventDetailsTool, ResolveDebugEventDetailsToolData } from './resolveDebugEventDetailsTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -39,6 +40,10 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); + const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); + this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); + this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData)); + const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); let runSubagentRegistration: IDisposable | undefined; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts new file mode 100644 index 00000000000..9bb37a5f829 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatDebugFilters.test.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ChatDebugFilterState } from '../../browser/chatDebug/chatDebugFilters.js'; + +suite('ChatDebugFilterState', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('parseTimeToken', () => { + + suite('before: prefix', () => { + + test('year only — rounds to end of year', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026', 'before'); + assert.strictEqual(result, new Date(2026, 11, 31, 23, 59, 59, 999).getTime()); + }); + + test('year-month — rounds to end of month', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03', 'before'); + // new Date(2026, 3, 0) gives last day of March + assert.strictEqual(result, new Date(2026, 3, 0, 23, 59, 59, 999).getTime()); + }); + + test('year-month (February, non-leap) — rounds to end of Feb', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2025-02', 'before'); + assert.strictEqual(result, new Date(2025, 2, 0, 23, 59, 59, 999).getTime()); + }); + + test('year-month-day — rounds to end of day', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 23, 59, 59, 999).getTime()); + }); + + test('date with hour only — rounds to end of hour', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03t14', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 59, 59, 999).getTime()); + }); + + test('date with hour:minute — rounds to end of minute', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03t14:30', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 59, 999).getTime()); + }); + + test('full date-time with seconds', () => { + const result = ChatDebugFilterState.parseTimeToken('before:2026-03-03t14:30:45', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 45, 999).getTime()); + }); + }); + + suite('after: prefix', () => { + + test('year only — start of year', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026', 'after'); + assert.strictEqual(result, new Date(2026, 0, 1, 0, 0, 0, 0).getTime()); + }); + + test('year-month — start of month', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03', 'after'); + assert.strictEqual(result, new Date(2026, 2, 1, 0, 0, 0, 0).getTime()); + }); + + test('year-month-day — start of day', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 0, 0, 0, 0).getTime()); + }); + + test('date with hour only — start of hour', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03t14', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 0, 0, 0).getTime()); + }); + + test('date with hour:minute — start of minute', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03t14:30', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 0, 0).getTime()); + }); + + test('full date-time with seconds', () => { + const result = ChatDebugFilterState.parseTimeToken('after:2026-03-03t14:30:45', 'after'); + assert.strictEqual(result, new Date(2026, 2, 3, 14, 30, 45, 0).getTime()); + }); + }); + + suite('no match', () => { + + test('returns undefined for empty string', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('', 'before'), undefined); + }); + + test('returns undefined for unrelated text', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('hello world', 'before'), undefined); + }); + + test('returns undefined for wrong prefix', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('after:2026', 'before'), undefined); + }); + + test('returns undefined for bare time without date', () => { + assert.strictEqual(ChatDebugFilterState.parseTimeToken('before:14:30', 'before'), undefined); + }); + }); + + suite('embedded in text', () => { + + test('extracts token from surrounding text', () => { + const result = ChatDebugFilterState.parseTimeToken('some text before:2026-03-03 more text', 'before'); + assert.strictEqual(result, new Date(2026, 2, 3, 23, 59, 59, 999).getTime()); + }); + + test('handles both before and after in same string', () => { + const text = 'after:2026-01 before:2026-03'; + const after = ChatDebugFilterState.parseTimeToken(text, 'after'); + const before = ChatDebugFilterState.parseTimeToken(text, 'before'); + assert.strictEqual(after, new Date(2026, 0, 1, 0, 0, 0, 0).getTime()); + assert.strictEqual(before, new Date(2026, 3, 0, 23, 59, 59, 999).getTime()); + }); + }); + }); + + suite('setTextFilter and timestamp parsing', () => { + let state: ChatDebugFilterState; + + setup(() => { + state = disposables.add(new ChatDebugFilterState()); + }); + + test('sets beforeTimestamp and afterTimestamp from text', () => { + state.setTextFilter('after:2026-01-01 before:2026-12-31'); + assert.strictEqual(state.afterTimestamp, new Date(2026, 0, 1, 0, 0, 0, 0).getTime()); + assert.strictEqual(state.beforeTimestamp, new Date(2026, 11, 31, 23, 59, 59, 999).getTime()); + }); + + test('clears timestamps when tokens removed', () => { + state.setTextFilter('before:2026'); + assert.ok(state.beforeTimestamp !== undefined); + state.setTextFilter('hello'); + assert.strictEqual(state.beforeTimestamp, undefined); + }); + }); + + suite('textFilterWithoutTimestamps', () => { + let state: ChatDebugFilterState; + + setup(() => { + state = disposables.add(new ChatDebugFilterState()); + }); + + test('strips year-only token', () => { + state.setTextFilter('before:2026 hello'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('strips year-month token', () => { + state.setTextFilter('after:2026-03 hello'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('strips full date-time token', () => { + state.setTextFilter('before:2026-03-03t14:30:45 hello'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('strips multiple tokens', () => { + state.setTextFilter('after:2026-01 hello before:2026-12'); + assert.strictEqual(state.textFilterWithoutTimestamps, 'hello'); + }); + + test('returns empty when only tokens', () => { + state.setTextFilter('before:2026'); + assert.strictEqual(state.textFilterWithoutTimestamps, ''); + }); + }); + + suite('isTimestampVisible', () => { + let state: ChatDebugFilterState; + + setup(() => { + state = disposables.add(new ChatDebugFilterState()); + }); + + test('visible when no timestamp filters set', () => { + assert.strictEqual(state.isTimestampVisible(new Date(2026, 5, 15)), true); + }); + + test('hidden when after beforeTimestamp', () => { + state.setTextFilter('before:2026-03'); + // April 1st is after end of March + assert.strictEqual(state.isTimestampVisible(new Date(2026, 3, 1)), false); + }); + + test('visible when before beforeTimestamp', () => { + state.setTextFilter('before:2026-03'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 1, 15)), true); + }); + + test('hidden when before afterTimestamp', () => { + state.setTextFilter('after:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 4, 31)), false); + }); + + test('visible when after afterTimestamp', () => { + state.setTextFilter('after:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 6, 1)), true); + }); + + test('visible when within before/after range', () => { + state.setTextFilter('after:2026-03 before:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 3, 15)), true); + }); + + test('hidden when outside before/after range', () => { + state.setTextFilter('after:2026-03 before:2026-06'); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 0, 1)), false); + assert.strictEqual(state.isTimestampVisible(new Date(2026, 8, 1)), false); + }); + }); +}); From 44c142b1d5b7fb77bb4b8078a0cc436bc7a60322 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 07:50:53 +0100 Subject: [PATCH 128/448] modal - improve handling of Escape key and expand use of modal editors to more kinds (#299060) * modal - improve handling of Escape key and expand use of modal editors to more kinds * Update src/vs/workbench/browser/parts/editor/editorCommands.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/parts/editor/editorCommands.ts | 12 ++++++++---- .../browser/parts/editor/modalEditorPart.ts | 10 +--------- .../preferences/browser/preferencesService.ts | 19 ++++++++++++------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index cedb8c95373..df841939212 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -1517,11 +1517,15 @@ function registerModalEditorCommands(): void { f1: true, icon: Codicon.close, precondition: EditorPartModalContext, - keybinding: { + keybinding: [{ primary: KeyCode.Escape, - weight: KeybindingWeight.WorkbenchContrib + 10, - when: EditorPartModalContext - }, + weight: KeybindingWeight.WorkbenchContrib + 10, // higher when no text editor is focused... + when: EditorContextKeys.focus.toNegated() + }, { + primary: KeyCode.Escape, + weight: KeybindingWeight.EditorContrib - 1, // ...lower to prevent accidental close when text editor is focused + when: EditorContextKeys.focus + }], menu: { id: MenuId.ModalEditorTitle, group: 'navigation', diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index a5bf50ce859..13c86decaf3 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -7,7 +7,6 @@ import './media/modalEditorPart.css'; import { $, addDisposableListener, append, EventHelper, EventType, hide, isHTMLElement, show } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; @@ -90,15 +89,8 @@ export class ModalEditorPart { disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); - // Close on Escape - if (event.equals(KeyCode.Escape)) { - EventHelper.stop(event, true); - - editorPart.close(); - } - // Prevent unsupported commands (not in sessions windows) - else if (!this.environmentService.isSessionsWindow) { + if (!this.environmentService.isSessionsWindow) { const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer); if (resolved.kind === ResultKind.KbFound && resolved.commandId) { if ( diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index a1ca22b3c4a..f871d6dd5d9 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -48,6 +48,7 @@ import { IURLService } from '../../../../platform/url/common/url.js'; import { compareIgnoreCase } from '../../../../base/common/strings.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; const emptyEditableSettingsContent = '{\n}'; @@ -90,7 +91,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic @ITextEditorService private readonly textEditorService: ITextEditorService, @IURLService urlService: IURLService, @IExtensionService private readonly extensionService: IExtensionService, - @IProgressService private readonly progressService: IProgressService + @IProgressService private readonly progressService: IProgressService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, ) { super(); // The default keybindings.json updates based on keyboard layouts, so here we make sure @@ -273,7 +275,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic ...options, focusSearch: true }; - const group = this.getEditorGroupFromOptions(false, options); + const group = this.getEditorGroupFromOptions(options); return this.editorService.openEditor(input, validateSettingsEditorOptions(options), group); } @@ -354,11 +356,11 @@ export class PreferencesService extends Disposable implements IPreferencesServic this.editorService.openEditor({ resource: editableKeybindings, options }, sideEditorGroup.id) ]); } else { - await this.editorService.openEditor({ resource: editableKeybindings, options }, options.groupId); + await this.editorService.openEditor({ resource: editableKeybindings, options }, this.getEditorGroupFromOptions(options)); } } else { - const group = this.getEditorGroupFromOptions(false, options); + const group = this.getEditorGroupFromOptions(options); const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { ...options }, group)) as IKeybindingsEditorPane; if (options.query) { editor.search(options.query); @@ -371,8 +373,11 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor({ resource: this.defaultKeybindingsResource, label: nls.localize('defaultKeybindings', "Default Keybindings") }); } - private getEditorGroupFromOptions(isTextual: boolean, options: { groupId?: number; openToSide?: boolean }): PreferredGroup { - if (!isTextual && this.configurationService.getValue('workbench.editor.useModal') !== 'off') { + private getEditorGroupFromOptions(options: { groupId?: number; openToSide?: boolean }): PreferredGroup { + if ( + this.configurationService.getValue('workbench.editor.useModal') !== 'off' && // modal editors enabled in settings + !this.environmentService.enableSmokeTestDriver && !this.environmentService.extensionTestsLocationURI // but not in smoke test or extension test environments to reduce flakiness + ) { return MODAL_GROUP; } if (options.openToSide) { @@ -385,7 +390,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } private async openSettingsJson(resource: URI, options: IOpenSettingsOptions): Promise { - const group = this.getEditorGroupFromOptions(true, options); + const group = this.getEditorGroupFromOptions(options); const editor = await this.doOpenSettingsJson(resource, options, group); if (editor && options?.revealSetting) { await this.revealSetting(options.revealSetting.key, !!options.revealSetting.edit, editor, resource); From 0d35e5d19e22e3135203ba9edba9bff435e4576b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 07:51:24 +0100 Subject: [PATCH 129/448] eng - explain fallback for how to check for compilation issues fast in CLI envs (#299117) * eng - explain fallback for how to check for compilation issues fast in CLI envs * Update .github/copilot-instructions.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/copilot-instructions.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c0a4142db03..be8c26eeadd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -50,15 +50,15 @@ Each extension follows the standard VS Code extension structure with `package.js ## Validating TypeScript changes -MANDATORY: Always check the `VS Code - Build` watch task output via #runTasks/getTaskOutput for compilation errors before running ANY script or declaring work complete, then fix all compilation errors before moving forward. +MANDATORY: Always check for compilation errors before running any tests or validation scripts, or declaring work complete, then fix all compilation errors before moving forward. - NEVER run tests if there are compilation errors -- NEVER use `npm run compile` to compile TypeScript files but call #runTasks/getTaskOutput instead +- NEVER use `npm run compile` to compile TypeScript files ### TypeScript compilation steps -- Monitor the `VS Code - Build` task outputs for real-time compilation errors as you make changes -- This task runs `Core - Build` and `Ext - Build` to incrementally compile VS Code TypeScript sources and built-in extensions -- Start the task if it's not already running in the background +- If the `#runTasks/getTaskOutput` tool is available, check the `VS Code - Build` watch task output for compilation errors. This task runs `Core - Build` and `Ext - Build` to incrementally compile VS Code TypeScript sources and built-in extensions. Start the task if it's not already running in the background. +- If the tool is not available (e.g. in CLI environments) and you only changed code under `src/`, run `npm run compile-check-ts-native` after making changes to type-check the main VS Code sources (it validates `./src/tsconfig.json`). +- If you changed built-in extensions under `extensions/` and the tool is not available, run the corresponding gulp task `gulp compile-extensions` instead so that TypeScript errors in extensions are also reported. - For TypeScript changes in the `build` folder, you can simply run `npm run typecheck` in the `build` folder. ### TypeScript validation steps From 6e1d1b137fd92ea79bfd81db8ad6a5fefd2bae94 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Mar 2026 18:31:16 +1100 Subject: [PATCH 130/448] Avoid unnecesary updates to model in new background agent sessions (#299121) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 9423e2431e4..f4a5f39c53f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1945,7 +1945,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // when the session type changes (different session types may have // different model pools via targetChatSessionType). const newSessionType = this.getCurrentSessionType(); - if (newSessionType !== this._currentSessionType) { + if (e.currentSessionResource && newSessionType !== this._currentSessionType) { this._currentSessionType = newSessionType; this.initSelectedModel(); } From 910bb74d164b821a559b3e1d978fe02735cbd79a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 08:31:59 +0100 Subject: [PATCH 131/448] sessions - indicate in layout that status bar is hidden (#299131) --- src/vs/sessions/browser/workbench.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 24b0d19b0d4..b54a7e6a0d9 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -81,6 +81,7 @@ enum LayoutClasses { PANEL_HIDDEN = 'nopanel', AUXILIARYBAR_HIDDEN = 'noauxiliarybar', CHATBAR_HIDDEN = 'nochatbar', + STATUSBAR_HIDDEN = 'nostatusbar', FULLSCREEN = 'fullscreen', MAXIMIZED = 'maximized' } @@ -893,6 +894,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !this.partVisibility.panel ? LayoutClasses.PANEL_HIDDEN : undefined, !this.partVisibility.auxiliaryBar ? LayoutClasses.AUXILIARYBAR_HIDDEN : undefined, !this.partVisibility.chatBar ? LayoutClasses.CHATBAR_HIDDEN : undefined, + LayoutClasses.STATUSBAR_HIDDEN, // sessions window never has a status bar this.mainWindowFullscreen ? LayoutClasses.FULLSCREEN : undefined ]); } From b5d4de9c3ceb3306ba53206a4f287330ea74df72 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 09:05:14 +0100 Subject: [PATCH 132/448] sessions - move scrollbar to the right for chat (#299134) --- src/vs/sessions/browser/media/style.css | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 6947f2aff49..299ce8545d8 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -43,6 +43,24 @@ background-color: var(--vscode-sideBar-background); } +/* ---- Chat Layout ---- */ + +/* Remove max-width from the session container so the scrollbar extends full width */ +.agent-sessions-workbench .interactive-session { + max-width: none; +} + +/* Constrain content items to the same max-width, centered */ +.agent-sessions-workbench .interactive-session .interactive-item-container { + max-width: 950px; + margin: 0 auto; +} + +.agent-sessions-workbench .interactive-session > .chat-suggest-next-widget { + max-width: 950px; + margin: 0 auto; +} + /* ---- Chat Input ---- */ .agent-sessions-workbench .interactive-session .chat-input-container { @@ -50,7 +68,8 @@ } .agent-sessions-workbench .interactive-session .interactive-input-part { - margin: 0 8px !important; + max-width: 950px; + margin: 0 auto !important; display: inherit !important; /* Align with changes view */ padding: 4px 0 6px 0 !important; From b5a312a098b113ed27c3377e3b0a0308f1817c79 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 09:22:00 +0100 Subject: [PATCH 133/448] fix - update precondition for `OpenSessionWorktreeInVSCode` (#299140) --- src/vs/sessions/contrib/chat/browser/chat.contribution.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index c10d4b4cdc0..5e49081cc2b 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -47,11 +47,12 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { id: OpenSessionWorktreeInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in VS Code'), icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext), + precondition: IsActiveSessionBackgroundProviderContext, menu: [{ id: Menus.TitleBarSessionMenu, group: 'navigation', order: 10, + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext), }] }); } From 4ff01e687e87f3e8db43db34ff040607cc9257ae Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:58:25 +0100 Subject: [PATCH 134/448] Git - tweak copilot worktree folder detection (#299147) * Git - tweak copilot worktree folder detection * Pull request feedback --- extensions/git/src/artifactProvider.ts | 4 ++-- extensions/git/src/repository.ts | 6 +++--- extensions/git/src/util.ts | 12 +++--------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/extensions/git/src/artifactProvider.ts b/extensions/git/src/artifactProvider.ts index 832b5626ae0..f9e2d99087f 100644 --- a/extensions/git/src/artifactProvider.ts +++ b/extensions/git/src/artifactProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode'; -import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util'; +import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktreeFolder } from './util'; import { Repository } from './repository'; import type { Ref, Worktree } from './api/git'; import { RefType } from './api/git.constants'; @@ -178,7 +178,7 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp ]).join(' \u2022 '), icon: w.main ? new ThemeIcon('repo') - : isCopilotWorktree(w.path) + : isCopilotWorktreeFolder(w.path) ? new ThemeIcon('chat-sparkle') : new ThemeIcon('worktree') })); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index b79bb3bc4aa..bd6b6a5c7ff 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -25,7 +25,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktree, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktreeFolder, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; import { GitArtifactProvider } from './artifactProvider'; @@ -954,7 +954,7 @@ export class Repository implements Disposable { const icon = repository.kind === 'submodule' ? new ThemeIcon('archive') : repository.kind === 'worktree' - ? isCopilotWorktree(repository.root) + ? isCopilotWorktreeFolder(repository.root) ? new ThemeIcon('chat-sparkle') : new ThemeIcon('worktree') : new ThemeIcon('repo'); @@ -967,7 +967,7 @@ export class Repository implements Disposable { // from the Repositories view. this._isHidden = workspace.workspaceFolders === undefined || (repository.kind === 'worktree' && - isCopilotWorktree(repository.root) && parent !== undefined); + isCopilotWorktreeFolder(repository.root) && parent !== undefined); const root = Uri.file(repository.root); this._sourceControl = scm.createSourceControl('git', 'Git', root, icon, this._isHidden, parent); diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index cbf1b56e34e..58a6d06419a 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env, SourceControlHistoryItem } from 'vscode'; -import { dirname, normalize, sep, relative } from 'path'; +import { basename, dirname, normalize, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; import byline from 'byline'; @@ -867,12 +867,6 @@ export function getStashDescription(stash: Stash): string | undefined { return descriptionSegments.join(' \u2022 '); } -export const CopilotWorktreeBranchPrefix = 'copilot-worktree-'; - -export function isCopilotWorktree(path: string): boolean { - const lastSepIndex = path.lastIndexOf(sep); - - return lastSepIndex !== -1 - ? path.substring(lastSepIndex + 1).startsWith(CopilotWorktreeBranchPrefix) - : path.startsWith(CopilotWorktreeBranchPrefix); +export function isCopilotWorktreeFolder(path: string): boolean { + return basename(path).startsWith('copilot-'); } From dede9833a8f8dfa6f019f93fa8d5249d5b6d83cb Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 10:12:24 +0100 Subject: [PATCH 135/448] Update widget and hover for sessions --- .../browser/account.contribution.ts | 197 ++++++------------ .../browser/media/accountWidget.css | 132 +++++++++++- .../browser/media/updateHoverWidget.css | 64 ++++++ .../accountMenu/browser/updateHoverWidget.ts | 187 +++++++++++++++++ .../test/browser/accountWidget.fixture.ts | 152 ++++++++++++++ .../test/browser/updateHoverWidget.fixture.ts | 85 ++++++++ .../test/browser/updateWidget.fixture.ts | 114 ---------- 7 files changed, 677 insertions(+), 254 deletions(-) create mode 100644 src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css create mode 100644 src/vs/sessions/contrib/accountMenu/browser/updateHoverWidget.ts create mode 100644 src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts create mode 100644 src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts delete mode 100644 src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts diff --git a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts index 2cd913c75f6..75afc565594 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts +++ b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts @@ -12,7 +12,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../platform/context import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { appendUpdateMenuItems as registerUpdateMenuItems, CONTEXT_UPDATE_STATE } from '../../../../workbench/contrib/update/browser/update.js'; +import { appendUpdateMenuItems as registerUpdateMenuItems } from '../../../../workbench/contrib/update/browser/update.js'; import { Menus } from '../../../browser/menus.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -23,9 +23,10 @@ import { IAction } from '../../../../base/common/actions.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Downloading, IUpdateService, State, StateType } from '../../../../platform/update/common/update.js'; -import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; -import { sessionsUpdateButtonDownloadingBackground, sessionsUpdateButtonDownloadedBackground } from '../../../common/theme.js'; +import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { UpdateHoverWidget } from './updateHoverWidget.js'; // --- Account Menu Items --- // const AccountMenu = new MenuId('SessionsAccountMenu'); @@ -83,20 +84,26 @@ MenuRegistry.appendMenuItem(AccountMenu, { // Update actions registerUpdateMenuItems(AccountMenu, '3_updates'); -class AccountWidget extends ActionViewItem { +export class AccountWidget extends ActionViewItem { private accountButton: Button | undefined; + private updateButton: Button | undefined; + private readonly updateHoverWidget: UpdateHoverWidget; private readonly viewItemDisposables = this._register(new DisposableStore()); constructor( action: IAction, options: IBaseActionViewItemOptions, @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, + @IUpdateService private readonly updateService: IUpdateService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IHoverService private readonly hoverService: IHoverService, + @IProductService private readonly productService: IProductService, ) { super(undefined, action, { ...options, icon: false, label: false }); + this.updateHoverWidget = new UpdateHoverWidget(this.updateService, this.productService, this.hoverService); } protected override getTooltip(): string | undefined { @@ -121,14 +128,33 @@ class AccountWidget extends ActionViewItem { })); this.accountButton.element.classList.add('account-widget-account-button', 'sidebar-action-button'); + // Update button (right) + const updateContainer = append(container, $('.account-widget-update')); + this.updateButton = this.viewItemDisposables.add(new Button(updateContainer, { + ...defaultButtonStyles, + secondary: true, + title: false, + supportIcons: true, + buttonSecondaryBackground: 'transparent', + buttonSecondaryHoverBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryBorder: undefined, + })); + this.updateButton.element.classList.add('account-widget-update-button', 'sidebar-action-button'); + this.viewItemDisposables.add(this.updateHoverWidget.attachTo(this.updateButton.element)); + this.updateAccountButton(); this.viewItemDisposables.add(this.defaultAccountService.onDidChangeDefaultAccount(() => this.updateAccountButton())); + this.updateUpdateButton(); + this.viewItemDisposables.add(this.updateService.onStateChange(() => this.updateUpdateButton())); this.viewItemDisposables.add(this.accountButton.onDidClick(e => { e?.preventDefault(); e?.stopPropagation(); this.showAccountMenu(this.accountButton!.element); })); + + this.viewItemDisposables.add(this.updateButton.onDidClick(() => this.update())); } private showAccountMenu(anchor: HTMLElement): void { @@ -156,134 +182,57 @@ class AccountWidget extends ActionViewItem { : `$(${Codicon.account.id}) ${localize('signInLabel', "Sign In")}`; } - - override onClick(): void { - // Handled by custom click handlers - } -} - -export class UpdateWidget extends ActionViewItem { - - private updateButton: Button | undefined; - private readonly viewItemDisposables = this._register(new DisposableStore()); - - constructor( - action: IAction, - options: IBaseActionViewItemOptions, - @IUpdateService private readonly updateService: IUpdateService, - ) { - super(undefined, action, { ...options, icon: false, label: false }); - } - - protected override getTooltip(): string | undefined { - return undefined; - } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('update-widget', 'sidebar-action'); - - const updateContainer = append(container, $('.update-widget-action')); - this.updateButton = this.viewItemDisposables.add(new Button(updateContainer, { - ...defaultButtonStyles, - secondary: true, - title: false, - supportIcons: true, - buttonSecondaryBackground: 'transparent', - buttonSecondaryHoverBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryBorder: undefined, - })); - this.updateButton.element.classList.add('update-widget-button', 'sidebar-action-button'); - this.viewItemDisposables.add(this.updateButton.onDidClick(() => this.update())); - - this.updateUpdateButton(); - this.viewItemDisposables.add(this.updateService.onStateChange(() => this.updateUpdateButton())); - } - - private isUpdateReady(): boolean { - return this.updateService.state.type === StateType.Ready; - } - - private isUpdatePending(): boolean { - const type = this.updateService.state.type; - return type === StateType.AvailableForDownload - || type === StateType.CheckingForUpdates - || type === StateType.Downloading - || type === StateType.Downloaded - || type === StateType.Updating - || type === StateType.Overwriting; - } - private updateUpdateButton(): void { if (!this.updateButton) { return; } const state = this.updateService.state; - if (this.isUpdatePending() && !this.isUpdateReady()) { - this.updateButton.enabled = false; - this.updateButton.label = `$(${Codicon.loading.id}~spin) ${this.getUpdateProgressMessage(state.type)}`; - this.updateDownloadProgress(state); - } else { - this.updateButton.enabled = true; - this.updateButton.label = `$(${Codicon.debugRestart.id}) ${localize('update', "Update")}`; - - const el = this.updateButton.element; - if (state.type === StateType.Ready) { - const color = asCssVariable(sessionsUpdateButtonDownloadedBackground); - el.style.backgroundImage = `linear-gradient(to right, ${color} 100%, transparent 100%)`; - } else { - // Ensure non-update states (e.g. Idle, Disabled, Uninitialized) do not look like a completed download - el.style.backgroundImage = ''; - } - } - } - - private updateDownloadProgress(state: State): void { - if (!this.updateButton) { + if (this.shouldHideUpdateButton(state.type)) { + this.clearUpdateButtonStyling(); + this.updateButton.element.classList.add('hidden'); return; } - const el = this.updateButton.element; + this.updateButton.element.classList.remove('hidden'); + this.updateButton.element.style.backgroundImage = ''; + this.updateButton.enabled = state.type === StateType.Ready; + this.updateButton.label = this.getUpdateProgressMessage(state.type); - if (state.type === StateType.Downloading) { - const { downloadedBytes, totalBytes } = state as Downloading; - if (downloadedBytes !== undefined && totalBytes && totalBytes > 0) { - const percent = Math.min(100, Math.round((downloadedBytes / totalBytes) * 100)); - const color = asCssVariable(sessionsUpdateButtonDownloadingBackground); - el.style.backgroundImage = `linear-gradient(to right, ${color} ${percent}%, transparent ${percent}%)`; - } else { - // Indeterminate: show a subtle pulsing background - const color = asCssVariable(sessionsUpdateButtonDownloadingBackground); - el.style.backgroundImage = `linear-gradient(to right, ${color} 0%, transparent 100%)`; - } - } else if (state.type === StateType.Downloaded) { - const color = asCssVariable(sessionsUpdateButtonDownloadedBackground); - el.style.backgroundImage = `linear-gradient(to right, ${color} 100%, transparent 100%)`; - } else { - this.clearDownloadProgress(); + if (state.type === StateType.Ready) { + this.updateButton.element.classList.add('account-widget-update-button-ready'); + return; } + + this.updateButton.element.classList.remove('account-widget-update-button-ready'); } - private clearDownloadProgress(): void { + private shouldHideUpdateButton(type: StateType): boolean { + return type === StateType.Uninitialized + || type === StateType.Idle + || type === StateType.Disabled + || type === StateType.CheckingForUpdates; + } + + private clearUpdateButtonStyling(): void { if (this.updateButton) { this.updateButton.element.style.backgroundImage = ''; + this.updateButton.element.classList.remove('account-widget-update-button-ready'); } } private getUpdateProgressMessage(type: StateType): string { switch (type) { - case StateType.CheckingForUpdates: - return localize('checkingForUpdates', "Checking for Updates..."); + case StateType.Ready: + return localize('update', "Update"); + case StateType.AvailableForDownload: case StateType.Downloading: - return localize('downloadingUpdate', "Downloading Update..."); + case StateType.Overwriting: + return localize('downloadingUpdate', "Downloading..."); case StateType.Downloaded: - return localize('installingUpdate', "Installing Update..."); + return localize('installingUpdate', "Installing..."); case StateType.Updating: return localize('updatingApp', "Updating..."); - case StateType.Overwriting: - return localize('overwritingUpdate', "Downloading Update..."); default: return localize('updating', "Updating..."); } @@ -293,6 +242,7 @@ export class UpdateWidget extends ActionViewItem { await this.updateService.quitAndInstall(); } + override onClick(): void { // Handled by custom click handlers } @@ -315,11 +265,6 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu return instantiationService.createInstance(AccountWidget, action, options); }, undefined)); - const sessionsUpdateWidgetAction = 'sessions.action.updateWidget'; - this._register(actionViewItemService.register(Menus.SidebarFooter, sessionsUpdateWidgetAction, (action, options) => { - return instantiationService.createInstance(UpdateWidget, action, options); - }, undefined)); - // Register the action with menu item after the view item provider // so the toolbar picks up the custom widget this._register(registerAction2(class extends Action2 { @@ -339,30 +284,6 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu } })); - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: sessionsUpdateWidgetAction, - title: localize2('sessionsUpdateWidget', 'Sessions Update'), - menu: { - id: Menus.SidebarFooter, - group: 'navigation', - order: 0, - when: ContextKeyExpr.or( - CONTEXT_UPDATE_STATE.isEqualTo(StateType.Ready), - CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload), - CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloading), - CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloaded), - CONTEXT_UPDATE_STATE.isEqualTo(StateType.Updating), - CONTEXT_UPDATE_STATE.isEqualTo(StateType.Overwriting), - ) - } - }); - } - async run(): Promise { - // Handled by the custom view item - } - })); } } diff --git a/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css b/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css index 01bdd2c100b..aeff16819c7 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css +++ b/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css @@ -3,6 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + min-width: 0; +} + +.account-widget { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + min-width: 0; +} + /* Account Button */ .monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account { overflow: hidden; @@ -10,9 +26,121 @@ flex: 1; } -/* Update Button */ -.monaco-workbench .part.sidebar > .sidebar-footer .update-widget-action { +.account-widget-account { overflow: hidden; min-width: 0; flex: 1; } + +/* Update Button */ +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update { + flex: 0 0 auto; + width: fit-content; + min-width: 0; + overflow: hidden; +} + +.account-widget-update { + flex: 0 0 auto; + width: fit-content; + min-width: 0; + overflow: hidden; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update .account-widget-update-button { + width: auto; + max-width: none; +} + +.account-widget-update .account-widget-update-button { + width: auto; + max-width: none; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update .account-widget-update-button.account-widget-update-button-ready { + background-color: var(--vscode-button-background) !important; + color: var(--vscode-button-foreground) !important; +} + +.account-widget-update .account-widget-update-button.account-widget-update-button-ready { + background-color: var(--vscode-button-background) !important; + color: var(--vscode-button-foreground) !important; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update .account-widget-update-button.account-widget-update-button-ready:hover { + background-color: var(--vscode-button-background) !important; +} + +.account-widget-update .account-widget-update-button.account-widget-update-button-ready:hover { + background-color: var(--vscode-button-background) !important; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update .account-widget-update-button.account-widget-update-button-ready:focus { + background-color: var(--vscode-button-background) !important; +} + +.account-widget-update .account-widget-update-button.account-widget-update-button-ready:focus { + background-color: var(--vscode-button-background) !important; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update .account-widget-update-button.hidden { + display: none; +} + +.account-widget-update .account-widget-update-button.hidden { + display: none; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account .account-widget-account-button { + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.account-widget-account .account-widget-account-button { + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account .account-widget-account-button > .codicon { + flex: 0 0 auto; +} + +.account-widget-account .account-widget-account-button > .codicon { + flex: 0 0 auto; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account .account-widget-account-button > span:last-child { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.account-widget-account .account-widget-account-button > span:last-child { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account .account-widget-account-button > .monaco-button-label { + display: block; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.account-widget-account .account-widget-account-button > .monaco-button-label { + display: block; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} diff --git a/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css b/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css new file mode 100644 index 00000000000..752f4e7faca --- /dev/null +++ b/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.sessions-update-hover { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 200px; +} + +.sessions-update-hover-header { + font-weight: 600; + font-size: 13px; +} + +/* Progress bar track */ +.sessions-update-hover-progress-track { + height: 4px; + border-radius: 2px; + background-color: var(--vscode-editorWidget-border, rgba(128, 128, 128, 0.3)); + overflow: hidden; +} + +/* Progress bar fill */ +.sessions-update-hover-progress-fill { + height: 100%; + border-radius: 2px; + background-color: var(--vscode-progressBar-background, #0078d4); + transition: width 0.2s ease; +} + +/* Details grid */ +.sessions-update-hover-grid { + display: grid; + grid-template-columns: auto auto auto auto; + column-gap: 8px; + row-gap: 2px; + font-size: 12px; + align-items: baseline; +} + +.sessions-update-hover-label { + color: var(--vscode-descriptionForeground); +} + +/* Version number emphasis */ +.sessions-update-hover-version { + color: var(--vscode-textLink-foreground); +} + +/* Compact age label */ +.sessions-update-hover-age { + color: var(--vscode-descriptionForeground); + font-size: 11px; +} + +/* Commit hashes - subtle */ +.sessions-update-hover-commit { + color: var(--vscode-descriptionForeground); + font-family: var(--monaco-monospace-font); + font-size: 11px; +} diff --git a/src/vs/sessions/contrib/accountMenu/browser/updateHoverWidget.ts b/src/vs/sessions/contrib/accountMenu/browser/updateHoverWidget.ts new file mode 100644 index 00000000000..fc80636b0a8 --- /dev/null +++ b/src/vs/sessions/contrib/accountMenu/browser/updateHoverWidget.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { localize } from '../../../../nls.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { Downloading, IUpdate, IUpdateService, State, StateType, Updating } from '../../../../platform/update/common/update.js'; +import './media/updateHoverWidget.css'; + +export class UpdateHoverWidget { + + constructor( + private readonly updateService: IUpdateService, + private readonly productService: IProductService, + private readonly hoverService: IHoverService, + ) { } + + attachTo(target: HTMLElement) { + return this.hoverService.setupDelayedHover( + target, + () => ({ + content: this.createHoverContent(), + position: { hoverPosition: HoverPosition.RIGHT }, + appearance: { showPointer: true } + }), + { groupId: 'sessions-account-update' } + ); + } + + createHoverContent(state: State = this.updateService.state): HTMLElement { + const update = this.getUpdateFromState(state); + const currentVersion = this.productService.version ?? localize('unknownVersion', "Unknown"); + const targetVersion = update?.productVersion ?? update?.version ?? localize('unknownVersion', "Unknown"); + const currentCommit = this.productService.commit; + const targetCommit = update?.version; + const progressPercent = this.getUpdateProgressPercent(state); + + const container = document.createElement('div'); + container.classList.add('sessions-update-hover'); + + // Header: e.g. "Downloading VS Code Insiders" + const header = document.createElement('div'); + header.classList.add('sessions-update-hover-header'); + header.textContent = this.getUpdateHeaderLabel(state.type); + container.appendChild(header); + + // Progress bar + if (progressPercent !== undefined) { + const progressTrack = document.createElement('div'); + progressTrack.classList.add('sessions-update-hover-progress-track'); + const progressFill = document.createElement('div'); + progressFill.classList.add('sessions-update-hover-progress-fill'); + progressFill.style.width = `${progressPercent}%`; + progressTrack.appendChild(progressFill); + container.appendChild(progressTrack); + } + + // Version info grid + const detailsGrid = document.createElement('div'); + detailsGrid.classList.add('sessions-update-hover-grid'); + + const currentDate = this.productService.date ? new Date(this.productService.date) : undefined; + const currentAge = currentDate ? this.formatCompactAge(currentDate.getTime()) : undefined; + const newAge = update?.timestamp ? this.formatCompactAge(update.timestamp) : undefined; + + this.appendGridRow(detailsGrid, localize('updateHoverCurrentVersionLabel', "Current"), currentVersion, currentAge, currentCommit); + this.appendGridRow(detailsGrid, localize('updateHoverNewVersionLabel', "New"), targetVersion, newAge, targetCommit); + + container.appendChild(detailsGrid); + + return container; + } + + private appendGridRow(grid: HTMLElement, label: string, version: string, age?: string, commit?: string): void { + const labelEl = document.createElement('span'); + labelEl.classList.add('sessions-update-hover-label'); + labelEl.textContent = label; + grid.appendChild(labelEl); + + const versionEl = document.createElement('span'); + versionEl.classList.add('sessions-update-hover-version'); + versionEl.textContent = version; + grid.appendChild(versionEl); + + const ageEl = document.createElement('span'); + ageEl.classList.add('sessions-update-hover-age'); + ageEl.textContent = age ?? ''; + grid.appendChild(ageEl); + + const commitEl = document.createElement('span'); + commitEl.classList.add('sessions-update-hover-commit'); + commitEl.textContent = commit ? commit.substring(0, 7) : ''; + grid.appendChild(commitEl); + } + + private formatCompactAge(timestamp: number): string { + const seconds = Math.round((Date.now() - timestamp) / 1000); + if (seconds < 60) { + return localize('compactAgeNow', "now"); + } + const minutes = Math.round(seconds / 60); + if (minutes < 60) { + return localize('compactAgeMinutes', "{0}m ago", minutes); + } + const hours = Math.round(seconds / 3600); + if (hours < 24) { + return localize('compactAgeHours', "{0}h ago", hours); + } + const days = Math.round(seconds / 86400); + if (days < 7) { + return localize('compactAgeDays', "{0}d ago", days); + } + const weeks = Math.round(days / 7); + if (weeks < 5) { + return localize('compactAgeWeeks', "{0}w ago", weeks); + } + const months = Math.round(days / 30); + return localize('compactAgeMonths', "{0}mo ago", months); + } + + private getUpdateFromState(state: State): IUpdate | undefined { + switch (state.type) { + case StateType.AvailableForDownload: + case StateType.Downloaded: + case StateType.Ready: + case StateType.Overwriting: + case StateType.Updating: + return state.update; + case StateType.Downloading: + return state.update; + default: + return undefined; + } + } + + /** + * Returns progress as a percentage (0-100), or undefined if progress is not applicable. + */ + private getUpdateProgressPercent(state: State): number | undefined { + switch (state.type) { + case StateType.Downloading: { + const downloadingState = state as Downloading; + if (downloadingState.downloadedBytes !== undefined && downloadingState.totalBytes && downloadingState.totalBytes > 0) { + return Math.min(100, Math.round((downloadingState.downloadedBytes / downloadingState.totalBytes) * 100)); + } + return 0; + } + case StateType.Updating: { + const updatingState = state as Updating; + if (updatingState.currentProgress !== undefined && updatingState.maxProgress && updatingState.maxProgress > 0) { + return Math.min(100, Math.round((updatingState.currentProgress / updatingState.maxProgress) * 100)); + } + return 0; + } + case StateType.Downloaded: + case StateType.Ready: + return 100; + case StateType.AvailableForDownload: + case StateType.Overwriting: + return 0; + default: + return undefined; + } + } + + private getUpdateHeaderLabel(type: StateType): string { + const productName = this.productService.nameShort; + switch (type) { + case StateType.Ready: + return localize('updateReady', "{0} Update Ready", productName); + case StateType.AvailableForDownload: + return localize('downloadAvailable', "{0} Update Available", productName); + case StateType.Downloading: + case StateType.Overwriting: + return localize('downloadingUpdate', "Downloading {0}", productName); + case StateType.Downloaded: + return localize('installingUpdate', "Installing {0}", productName); + case StateType.Updating: + return localize('updatingApp', "Updating {0}", productName); + default: + return localize('updating', "Updating {0}", productName); + } + } +} diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts new file mode 100644 index 00000000000..26c7d3a822d --- /dev/null +++ b/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICopilotTokenInfo, IDefaultAccount, IPolicyData } from '../../../../../base/common/defaultAccount.js'; +import { Action } from '../../../../../base/common/actions.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { IMenuService } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IUpdateService, State, UpdateType } from '../../../../../platform/update/common/update.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { AccountWidget } from '../../browser/account.contribution.js'; + +// Ensure color registrations are loaded +import '../../../../common/theme.js'; +import '../../../../../platform/theme/common/colors/inputColors.js'; + +// Import the CSS +import '../../../../browser/media/sidebarActionButton.css'; +import '../../browser/media/accountWidget.css'; + +const mockUpdate = { version: '1.0.0' }; + +function createMockUpdateService(state: State): IUpdateService { + const onStateChange = new Emitter(); + const service: IUpdateService = { + _serviceBrand: undefined, + state, + onStateChange: onStateChange.event, + checkForUpdates: async () => { }, + downloadUpdate: async () => { }, + applyUpdate: async () => { }, + quitAndInstall: async () => { }, + isLatestVersion: async () => true, + _applySpecificUpdate: async () => { }, + setInternalOrg: async () => { }, + }; + return service; +} + +function createMockDefaultAccountService(accountPromise: Promise): IDefaultAccountService { + const onDidChangeDefaultAccount = new Emitter(); + const onDidChangePolicyData = new Emitter(); + const onDidChangeCopilotTokenInfo = new Emitter(); + const service: IDefaultAccountService = { + _serviceBrand: undefined, + onDidChangeDefaultAccount: onDidChangeDefaultAccount.event, + onDidChangePolicyData: onDidChangePolicyData.event, + onDidChangeCopilotTokenInfo: onDidChangeCopilotTokenInfo.event, + policyData: null, + copilotTokenInfo: null, + getDefaultAccount: () => accountPromise, + getDefaultAccountAuthenticationProvider: () => ({ id: 'github', name: 'GitHub', enterprise: false }), + setDefaultAccountProvider: () => { }, + refresh: () => accountPromise, + signIn: async () => null, + signOut: async () => { }, + }; + return service; +} + +function renderAccountWidget(ctx: ComponentFixtureContext, state: State, accountPromise: Promise): void { + ctx.container.style.padding = '16px'; + ctx.container.style.width = '340px'; + ctx.container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + + const mockUpdateService = createMockUpdateService(state); + const mockAccountService = createMockDefaultAccountService(accountPromise); + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: registerWorkbenchServices, + }); + + const action = ctx.disposableStore.add(new Action('sessions.action.accountWidget', 'Sessions Account')); + const contextMenuService = instantiationService.get(IContextMenuService); + const menuService = instantiationService.get(IMenuService); + const contextKeyService = instantiationService.get(IContextKeyService); + const hoverService = instantiationService.get(IHoverService); + const productService = instantiationService.get(IProductService); + const widget = new AccountWidget(action, {}, mockAccountService, mockUpdateService, contextMenuService, menuService, contextKeyService, hoverService, productService); + ctx.disposableStore.add(widget); + widget.render(ctx.container); +} + +const signedInAccount: IDefaultAccount = { + authenticationProvider: { + id: 'github', + name: 'GitHub', + enterprise: false, + }, + accountName: 'avery.long.account.name@example.com', + sessionId: 'session-id', + enterprise: false, +}; + +export default defineThemedFixtureGroup({ path: 'sessions/' }, { + LoadingSignedOutNoUpdate: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Idle(UpdateType.Setup), new Promise(() => { })), + }), + + SignedOutNoUpdate: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Idle(UpdateType.Setup), Promise.resolve(null)), + }), + + SignedInNoUpdate: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Idle(UpdateType.Setup), Promise.resolve(signedInAccount)), + }), + + CheckingForUpdatesHidden: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.CheckingForUpdates(true), Promise.resolve(signedInAccount)), + }), + + Ready: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Ready(mockUpdate, true, false), Promise.resolve(signedInAccount)), + }), + + AvailableForDownload: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.AvailableForDownload(mockUpdate), Promise.resolve(signedInAccount)), + }), + + Downloading30Percent: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Downloading(mockUpdate, true, false, 30_000_000, 100_000_000), Promise.resolve(signedInAccount)), + }), + + DownloadedInstalling: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Downloaded(mockUpdate, true, false), Promise.resolve(signedInAccount)), + }), + + Updating: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Updating(mockUpdate), Promise.resolve(signedInAccount)), + }), + + Overwriting: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAccountWidget(ctx, State.Overwriting(mockUpdate, true), Promise.resolve(signedInAccount)), + }), +}); diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts new file mode 100644 index 00000000000..8092585ab6d --- /dev/null +++ b/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../base/common/event.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IUpdateService, State } from '../../../../../platform/update/common/update.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { UpdateHoverWidget } from '../../browser/updateHoverWidget.js'; + +const mockUpdate = { version: 'a1b2c3d4e5f6', productVersion: '1.100.0', timestamp: Date.now() - 2 * 60 * 60 * 1000 }; +const mockUpdateSameVersion = { version: 'a1b2c3d4e5f6', productVersion: '1.99.0', timestamp: Date.now() - 3 * 24 * 60 * 60 * 1000 }; + +function createMockUpdateService(state: State): IUpdateService { + const onStateChange = new Emitter(); + const service: IUpdateService = { + _serviceBrand: undefined, + state, + onStateChange: onStateChange.event, + checkForUpdates: async () => { }, + downloadUpdate: async () => { }, + applyUpdate: async () => { }, + quitAndInstall: async () => { }, + isLatestVersion: async () => true, + _applySpecificUpdate: async () => { }, + setInternalOrg: async () => { }, + }; + return service; +} + +function renderHoverWidget(ctx: ComponentFixtureContext, state: State): void { + ctx.container.style.padding = '16px'; + ctx.container.style.width = '320px'; + ctx.container.style.backgroundColor = 'var(--vscode-editorHoverWidget-background)'; + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + }); + + const updateService = createMockUpdateService(state); + const productService = new class extends mock() { + override readonly version = '1.99.0'; + override readonly nameShort = 'VS Code Insiders'; + override readonly commit = 'f0e1d2c3b4a5'; + override readonly date = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + }; + const hoverService = instantiationService.get(IHoverService); + const widget = new UpdateHoverWidget(updateService, productService, hoverService); + ctx.container.appendChild(widget.createHoverContent(state)); +} + +export default defineThemedFixtureGroup({ path: 'sessions/' }, { + UpdateHoverReady: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderHoverWidget(ctx, State.Ready(mockUpdate, true, false)), + }), + + UpdateHoverAvailableForDownload: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderHoverWidget(ctx, State.AvailableForDownload(mockUpdate)), + }), + + UpdateHoverDownloading30Percent: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderHoverWidget(ctx, State.Downloading(mockUpdate, true, false, 30_000_000, 100_000_000)), + }), + + UpdateHoverInstalling: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderHoverWidget(ctx, State.Downloaded(mockUpdate, true, false)), + }), + + UpdateHoverUpdating: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderHoverWidget(ctx, State.Updating(mockUpdate, 40, 100)), + }), + + UpdateHoverSameVersion: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderHoverWidget(ctx, State.Ready(mockUpdateSameVersion, true, false)), + }), +}); diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts deleted file mode 100644 index 225223a3dda..00000000000 --- a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from '../../../../../base/common/actions.js'; -import { Emitter } from '../../../../../base/common/event.js'; -import { IUpdateService, State } from '../../../../../platform/update/common/update.js'; -import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; -import { UpdateWidget } from '../../browser/account.contribution.js'; - -// Ensure color registrations are loaded -import '../../../../common/theme.js'; -import '../../../../../platform/theme/common/colors/inputColors.js'; - -// Import the CSS -import '../../../../browser/media/sidebarActionButton.css'; -import '../../browser/media/accountWidget.css'; - -const mockUpdate = { version: '1.0.0' }; - -function createMockUpdateService(state: State): IUpdateService { - const onStateChange = new Emitter(); - const service: IUpdateService = { - _serviceBrand: undefined, - state, - onStateChange: onStateChange.event, - checkForUpdates: async () => { }, - downloadUpdate: async () => { }, - applyUpdate: async () => { }, - quitAndInstall: async () => { }, - isLatestVersion: async () => true, - _applySpecificUpdate: async () => { }, - setInternalOrg: async () => { }, - }; - return service; -} - -function renderUpdateWidget(ctx: ComponentFixtureContext, state: State): void { - ctx.container.style.padding = '16px'; - ctx.container.style.width = '300px'; - ctx.container.style.backgroundColor = 'var(--vscode-sideBar-background)'; - - const mockService = createMockUpdateService(state); - - const instantiationService = createEditorServices(ctx.disposableStore, { - colorTheme: ctx.theme, - additionalServices: (reg) => { - reg.defineInstance(IUpdateService, mockService); - }, - }); - - const action = ctx.disposableStore.add(new Action('sessions.action.updateWidget', 'Sessions Update')); - const widget = instantiationService.createInstance(UpdateWidget, action, {}); - ctx.disposableStore.add(widget); - widget.render(ctx.container); -} - -export default defineThemedFixtureGroup({ path: 'sessions/' }, { - Ready: defineComponentFixture({ - labels: { kind: 'screenshot' }, - render: (ctx) => renderUpdateWidget(ctx, State.Ready(mockUpdate, true, false)), - }), - - CheckingForUpdates: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.CheckingForUpdates(true)), - }), - - AvailableForDownload: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.AvailableForDownload(mockUpdate)), - }), - - Downloading0Percent: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 0, 100_000_000)), - }), - - Downloading30Percent: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 30_000_000, 100_000_000)), - }), - - Downloading65Percent: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 65_000_000, 100_000_000)), - }), - - Downloading100Percent: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 100_000_000, 100_000_000)), - }), - - DownloadingIndeterminate: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false)), - }), - - Downloaded: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Downloaded(mockUpdate, true, false)), - }), - - Updating: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Updating(mockUpdate)), - }), - - Overwriting: defineComponentFixture({ - labels: { kind: 'animated' }, - render: (ctx) => renderUpdateWidget(ctx, State.Overwriting(mockUpdate, true)), - }), -}); From 450aae82d85466e48f84393eb1bb2c44b8a22183 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:44:16 +0100 Subject: [PATCH 136/448] Update src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 4ed04cbc113..dbe1402b6a1 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -326,7 +326,7 @@ KeybindingsRegistry.registerKeybindingRule({ }); const CLOSE_SESSION_COMMAND_ID = 'agentSession.close'; -registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { +registerAction2(class CloseSessionAction extends Action2 { constructor() { super({ id: CLOSE_SESSION_COMMAND_ID, From 5bf6c54c2216f8a1679a4428fbaafbdbbf466f37 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:45:12 +0100 Subject: [PATCH 137/448] Update src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index dbe1402b6a1..41865aac2fb 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -342,7 +342,7 @@ registerAction2(class CloseSessionAction extends Action2 { } }); -// Register Cmd+W / Ctrl+W to open new session when the current session is non-empty, +// Register Cmd+W / Ctrl+W to close the current session and navigate to the new-session view, // mirroring how Cmd+W closes the active editor in the normal workbench. KeybindingsRegistry.registerKeybindingRule({ id: CLOSE_SESSION_COMMAND_ID, From 240196b5955050f4be2d98839d02483dda0465a8 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 3 Mar 2026 20:32:42 +0100 Subject: [PATCH 138/448] Adds support for stronglyRecommended extensions. Implements #299039 --- .vscode/extensions.json | 6 +- src/vs/platform/dialogs/common/dialogs.ts | 12 ++ .../common/extensionRecommendations.ts | 2 + .../common/extensionRecommendationsIpc.ts | 9 + .../browser/parts/dialogs/dialogHandler.ts | 9 +- ...ensionRecommendationNotificationService.ts | 169 ++++++++++++++++++ .../extensionRecommendationsService.ts | 10 ++ .../browser/extensions.contribution.ts | 11 ++ .../stronglyRecommendedExtensionList.ts | 99 ++++++++++ .../browser/workspaceRecommendations.ts | 29 ++- .../common/extensionsFileTemplate.ts | 9 + .../common/workspaceExtensionsConfig.ts | 8 + .../stronglyRecommendedDialog.fixture.ts | 85 +++++++++ 13 files changed, 455 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3fb87652c81..bd45eb0e570 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,11 +4,15 @@ "recommendations": [ "dbaeumer.vscode-eslint", "editorconfig.editorconfig", - "github.vscode-pull-request-github", "ms-vscode.vscode-github-issue-notebooks", "ms-vscode.extension-test-runner", "jrieken.vscode-pr-pinger", "typescriptteam.native-preview", "ms-vscode.ts-customized-language-service" + ], + "stronglyRecommended": [ + "github.vscode-pull-request-github", + "ms-vscode.vscode-extras", + "ms-vscode.vscode-selfhost-test-provider" ] } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index fc73e57f824..925b82a8a28 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Event } from '../../../base/common/event.js'; +import { DisposableStore } from '../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { basename } from '../../../base/common/resources.js'; @@ -286,12 +287,23 @@ export const IDialogService = createDecorator('dialogService'); export interface ICustomDialogOptions { readonly buttonDetails?: string[]; + readonly buttonOptions?: Array; readonly markdownDetails?: ICustomDialogMarkdown[]; + readonly renderBody?: (container: HTMLElement, disposables: DisposableStore) => void; readonly classes?: string[]; readonly icon?: ThemeIcon; readonly disableCloseAction?: boolean; } +export interface ICustomDialogButtonOptions { + readonly sublabel?: string; + readonly styleButton?: (button: ICustomDialogButtonControl) => void; +} + +export interface ICustomDialogButtonControl { + set enabled(value: boolean); +} + export interface ICustomDialogMarkdown { readonly markdown: IMarkdownString; readonly classes?: string[]; diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index fe258ed580c..00b2a6ce993 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -45,5 +45,7 @@ export interface IExtensionRecommendationNotificationService { promptImportantExtensionsInstallNotification(recommendations: IExtensionRecommendations): Promise; promptWorkspaceRecommendations(recommendations: Array): Promise; + promptStronglyRecommendedExtensions(recommendations: Array): Promise; + resetStronglyRecommendedIgnoreState(): void; } diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts index da863e3c6e2..28ae56a38be 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../base/common/event.js'; +import { URI } from '../../../base/common/uri.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult } from './extensionRecommendations.js'; @@ -23,10 +24,18 @@ export class ExtensionRecommendationNotificationServiceChannelClient implements throw new Error('not supported'); } + promptStronglyRecommendedExtensions(recommendations: Array): Promise { + throw new Error('not supported'); + } + hasToIgnoreRecommendationNotifications(): boolean { throw new Error('not supported'); } + resetStronglyRecommendedIgnoreState(): void { + throw new Error('not supported'); + } + } export class ExtensionRecommendationNotificationServiceChannel implements IServerChannel { diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 5af4f540bac..be802600908 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -9,6 +9,7 @@ import { IConfirmation, IConfirmationResult, IInputResult, ICheckbox, IInputElem import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import Severity from '../../../../base/common/severity.js'; +import { IButton } from '../../../../base/browser/ui/button/button.js'; import { Dialog, IDialogResult } from '../../../../base/browser/ui/dialog/dialog.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -105,8 +106,14 @@ export class BrowserDialogHandler extends AbstractDialogHandler { parent.appendChild(result.element); result.element.classList.add(...(markdownDetail.classes || [])); }); + customOptions.renderBody?.(parent, dialogDisposables); } : undefined; + const buttonOptions = customOptions?.buttonOptions?.map(opt => opt ? { + sublabel: opt.sublabel, + styleButton: opt.styleButton ? (button: IButton) => opt.styleButton!(button) : undefined + } : undefined) ?? customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })); + const dialog = new Dialog( this.layoutService.activeContainer, message, @@ -118,7 +125,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler { renderBody, icon: customOptions?.icon, disableCloseAction: customOptions?.disableCloseAction, - buttonOptions: customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })), + buttonOptions, checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, inputs diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index b70a49c4016..3caf43bdc45 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -6,6 +6,7 @@ import { distinct } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -13,11 +14,13 @@ import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { renderStronglyRecommendedExtensionList, StronglyRecommendedExtensionListResult } from './stronglyRecommendedExtensionList.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; @@ -42,6 +45,18 @@ type ExtensionWorkspaceRecommendationsNotificationClassification = { const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore'; const donotShowWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; +const stronglyRecommendedIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedIgnore'; +const stronglyRecommendedMajorVersionIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedMajorVersionIgnore'; + +interface MajorVersionIgnoreEntry { + readonly id: string; + readonly majorVersion: number; +} + +function parseMajorVersion(version: string): number { + const major = parseInt(version.split('.')[0], 10); + return isNaN(major) ? 0 : major; +} type RecommendationsNotificationActions = { onDidInstallRecommendedExtensions(extensions: IExtension[]): void; @@ -132,6 +147,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple constructor( @IConfigurationService private readonly configurationService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -468,6 +484,159 @@ export class ExtensionRecommendationNotificationService extends Disposable imple } } + async promptStronglyRecommendedExtensions(recommendations: Array): Promise { + if (this.hasToIgnoreRecommendationNotifications()) { + return; + } + + const ignoredList = this._getStronglyRecommendedIgnoreList(); + recommendations = recommendations.filter(rec => { + const key = isString(rec) ? rec.toLowerCase() : rec.toString(); + return !ignoredList.includes(key); + }); + if (!recommendations.length) { + return; + } + + let installed = await this.extensionManagementService.getInstalled(); + installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); + recommendations = recommendations.filter(recommendation => + installed.every(local => + isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location) + ) + ); + if (!recommendations.length) { + return; + } + + const allExtensions = await this.getInstallableExtensions(recommendations); + if (!allExtensions.length) { + return; + } + + const majorVersionIgnoreList = this._getStronglyRecommendedMajorVersionIgnoreList(); + const extensions = allExtensions.filter(ext => { + const ignored = majorVersionIgnoreList.find(e => e.id === ext.identifier.id.toLowerCase()); + return !ignored || parseMajorVersion(ext.version) > ignored.majorVersion; + }); + if (!extensions.length) { + return; + } + + const message = extensions.length === 1 + ? localize('stronglyRecommended1', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName) + : localize('stronglyRecommendedN', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length); + + let listResult!: StronglyRecommendedExtensionListResult; + + const { result } = await this.dialogService.prompt({ + message, + buttons: [ + { + label: localize('install', "Install"), + run: () => true, + }, + ], + cancelButton: localize('cancel', "Cancel"), + custom: { + icon: Codicon.extensions, + renderBody: (container, disposables) => { + listResult = renderStronglyRecommendedExtensionList(container, disposables, extensions); + }, + buttonOptions: [{ + styleButton: (button) => listResult.styleInstallButton(button), + }], + }, + }); + + if (result) { + const selected = extensions.filter(e => listResult.checkboxStates.get(e)); + const unselected = extensions.filter(e => !listResult.checkboxStates.get(e)); + if (unselected.length) { + this._addToStronglyRecommendedIgnoreList( + unselected.map(e => e.identifier.id) + ); + } + if (listResult.doNotShowAgainUnlessMajorVersionChange()) { + this._addToStronglyRecommendedIgnoreWithMajorVersion( + extensions.map(e => ({ id: e.identifier.id, majorVersion: parseMajorVersion(e.version) })) + ); + } + if (selected.length) { + const galleryExtensions: IGalleryExtension[] = []; + const resourceExtensions: IExtension[] = []; + for (const extension of selected) { + if (extension.gallery) { + galleryExtensions.push(extension.gallery); + } else if (extension.resourceExtension) { + resourceExtensions.push(extension); + } + } + await Promises.settled([ + Promises.settled(selected.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), + galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: {} }))) : Promise.resolve(), + resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve(), + ]); + } + } + } + + private _getStronglyRecommendedIgnoreList(): string[] { + const raw = this.storageService.get(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE); + if (raw === undefined) { + return []; + } + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + + private _addToStronglyRecommendedIgnoreList(recommendations: Array): void { + const list = this._getStronglyRecommendedIgnoreList(); + for (const rec of recommendations) { + const key = isString(rec) ? rec.toLowerCase() : rec.toString(); + if (!list.includes(key)) { + list.push(key); + } + } + this.storageService.store(stronglyRecommendedIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + private _getStronglyRecommendedMajorVersionIgnoreList(): MajorVersionIgnoreEntry[] { + const raw = this.storageService.get(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE); + if (raw === undefined) { + return []; + } + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + + private _addToStronglyRecommendedIgnoreWithMajorVersion(entries: MajorVersionIgnoreEntry[]): void { + const list = this._getStronglyRecommendedMajorVersionIgnoreList(); + for (const entry of entries) { + const key = entry.id.toLowerCase(); + const existing = list.findIndex(e => e.id === key); + if (existing !== -1) { + list[existing] = { id: key, majorVersion: entry.majorVersion }; + } else { + list.push({ id: key, majorVersion: entry.majorVersion }); + } + } + this.storageService.store(stronglyRecommendedMajorVersionIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + resetStronglyRecommendedIgnoreState(): void { + this.storageService.remove(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE); + this.storageService.remove(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE); + } + private setIgnoreRecommendationsConfig(configVal: boolean) { this.configurationService.updateValue('extensions.ignoreRecommendations', configVal); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 12cf0a6c61f..9cba507bb0b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -110,6 +110,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); this.promptWorkspaceRecommendations(); + this.promptStronglyRecommendedExtensions(); } private isEnabled(): boolean { @@ -274,6 +275,15 @@ export class ExtensionRecommendationsService extends Disposable implements IExte } } + private async promptStronglyRecommendedExtensions(): Promise { + const allowedRecommendations = this.workspaceRecommendations.stronglyRecommended + .filter(rec => !isString(rec) || this.isExtensionAllowedToBeRecommended(rec)); + + if (allowedRecommendations.length) { + await this.extensionRecommendationNotificationService.promptStronglyRecommendedExtensions(allowedRecommendations); + } + } + private _registerP(o: CancelablePromise): CancelablePromise { this._register(toDisposable(() => o.cancel())); return o; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 37e6e916e16..218741bdd99 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1895,6 +1895,17 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations') }); + this.registerExtensionAction({ + id: 'workbench.extensions.action.resetStronglyRecommendedIgnoreState', + title: localize2('workbench.extensions.action.resetStronglyRecommendedIgnoreState', "Reset Strongly Recommended Extensions Ignore State"), + category: EXTENSIONS_CATEGORY, + menu: { + id: MenuId.CommandPalette, + when: WorkbenchStateContext.notEqualsTo('empty'), + }, + run: async (accessor: ServicesAccessor) => accessor.get(IExtensionRecommendationNotificationService).resetStronglyRecommendedIgnoreState() + }); + this.registerExtensionAction({ id: ConfigureWorkspaceRecommendedExtensionsAction.ID, title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, diff --git a/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts b/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts new file mode 100644 index 00000000000..c4cb34ce443 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, addDisposableListener } from '../../../../base/browser/dom.js'; +import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { ICustomDialogButtonControl } from '../../../../platform/dialogs/common/dialogs.js'; +import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; + +export interface StronglyRecommendedExtensionEntry { + readonly displayName: string; + readonly publisherDisplayName: string; + readonly version: string; +} + +export interface StronglyRecommendedExtensionListResult { + readonly checkboxStates: ReadonlyMap; + readonly hasSelection: boolean; + readonly doNotShowAgainUnlessMajorVersionChange: () => boolean; + styleInstallButton(button: ICustomDialogButtonControl): void; +} + +export function renderStronglyRecommendedExtensionList( + container: HTMLElement, + disposables: DisposableStore, + extensions: readonly T[], +): StronglyRecommendedExtensionListResult { + const checkboxStates = new Map(); + const onSelectionChanged = disposables.add(new Emitter()); + + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '8px'; + container.style.padding = '8px 0'; + + const updateCheckbox = (ext: T, cb: Checkbox) => { + checkboxStates.set(ext, cb.checked); + onSelectionChanged.fire(); + }; + + for (const ext of extensions) { + checkboxStates.set(ext, true); + + const row = container.appendChild($('.strongly-recommended-extension-row')); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + + const cb = disposables.add(new Checkbox(ext.displayName, true, defaultCheckboxStyles)); + disposables.add(cb.onChange(() => updateCheckbox(ext, cb))); + row.appendChild(cb.domNode); + + const label = row.appendChild($('span')); + label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`; + label.style.cursor = 'pointer'; + disposables.add(addDisposableListener(label, 'click', () => { + cb.checked = !cb.checked; + updateCheckbox(ext, cb); + })); + } + + const separator = container.appendChild($('div')); + separator.style.borderTop = '1px solid var(--vscode-widget-border)'; + separator.style.marginTop = '4px'; + separator.style.paddingTop = '4px'; + + const doNotShowRow = container.appendChild($('.strongly-recommended-do-not-show-row')); + doNotShowRow.style.display = 'flex'; + doNotShowRow.style.alignItems = 'center'; + doNotShowRow.style.gap = '8px'; + + const doNotShowCb = disposables.add(new Checkbox( + localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"), + false, + defaultCheckboxStyles, + )); + doNotShowRow.appendChild(doNotShowCb.domNode); + + const doNotShowLabel = doNotShowRow.appendChild($('span')); + doNotShowLabel.textContent = localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"); + doNotShowLabel.style.cursor = 'pointer'; + disposables.add(addDisposableListener(doNotShowLabel, 'click', () => { doNotShowCb.checked = !doNotShowCb.checked; })); + + const hasSelection = () => [...checkboxStates.values()].some(v => v); + + return { + checkboxStates, + get hasSelection() { return hasSelection(); }, + doNotShowAgainUnlessMajorVersionChange: () => doNotShowCb.checked, + styleInstallButton(button: ICustomDialogButtonControl) { + const updateEnabled = () => { button.enabled = hasSelection(); }; + disposables.add(onSelectionChanged.event(updateEnabled)); + }, + }; +} diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 69ff685c658..8563dcc4c15 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -25,6 +25,9 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { private _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } + private _stronglyRecommended: Array = []; + get stronglyRecommended(): ReadonlyArray { return this._stronglyRecommended; } + private _onDidChangeRecommendations = this._register(new Emitter()); readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; @@ -32,6 +35,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } private workspaceExtensions: URI[] = []; + private workspaceExtensionIds = new Map(); private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler; constructor( @@ -90,8 +94,12 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { // ignore } } + this.workspaceExtensionIds.clear(); if (workspaceExtensions.length) { const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions); + for (const ext of resourceExtensions) { + this.workspaceExtensionIds.set(ext.identifier.id.toLowerCase(), ext.location); + } return resourceExtensions.map(extension => extension.location); } return []; @@ -110,6 +118,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } this._recommendations = []; + this._stronglyRecommended = []; this._ignoredRecommendations = []; for (const extensionsConfig of extensionsConfigs) { @@ -133,6 +142,24 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } } + if (extensionsConfig.stronglyRecommended) { + for (const extensionId of extensionsConfig.stronglyRecommended) { + if (invalidRecommendations.indexOf(extensionId) === -1) { + const workspaceExtUri = this.workspaceExtensionIds.get(extensionId.toLowerCase()); + const extension = workspaceExtUri ?? extensionId; + const reason = { + reasonId: ExtensionRecommendationReason.Workspace, + reasonText: localize('stronglyRecommendedExtension', "This extension is strongly recommended by users of the current workspace.") + }; + this._stronglyRecommended.push(extension); + if (workspaceExtUri) { + this._recommendations.push({ extension: workspaceExtUri, reason }); + } else { + this._recommendations.push({ extension: extensionId, reason }); + } + } + } + } } for (const extension of this.workspaceExtensions) { @@ -152,7 +179,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { const invalidExtensions: string[] = []; let message = ''; - const allRecommendations = distinct(contents.flatMap(({ recommendations }) => recommendations || [])); + const allRecommendations = distinct(contents.flatMap(({ recommendations, stronglyRecommended }) => [...(recommendations || []), ...(stronglyRecommended || [])])); const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN); for (const extensionId of allRecommendations) { if (regEx.test(extensionId)) { diff --git a/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts b/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts index 818e662847e..574806a1bc8 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts @@ -25,6 +25,15 @@ export const ExtensionsConfigurationSchema: IJSONSchema = { errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }, }, + stronglyRecommended: { + type: 'array', + description: localize('app.extensions.json.stronglyRecommended', "List of extensions that are strongly recommended for users of this workspace. Users will be prompted with a dialog to install these extensions. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN, + errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") + }, + }, unwantedRecommendations: { type: 'array', description: localize('app.extensions.json.unwantedRecommendations', "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), diff --git a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts index a48ce69a12b..ba02eaf679f 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts @@ -24,6 +24,7 @@ export const EXTENSIONS_CONFIG = '.vscode/extensions.json'; export interface IExtensionsConfigContent { recommendations?: string[]; + stronglyRecommended?: string[]; unwantedRecommendations?: string[]; } @@ -35,6 +36,7 @@ export interface IWorkspaceExtensionsConfigService { readonly onDidChangeExtensionsConfigs: Event; getExtensionsConfigs(): Promise; getRecommendations(): Promise; + getStronglyRecommended(): Promise; getUnwantedRecommendations(): Promise; toggleRecommendation(extensionId: string): Promise; @@ -84,6 +86,11 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor return distinct(configs.flatMap(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : [])); } + async getStronglyRecommended(): Promise { + const configs = await this.getExtensionsConfigs(); + return distinct(configs.flatMap(c => c.stronglyRecommended ? c.stronglyRecommended.map(c => c.toLowerCase()) : [])); + } + async getUnwantedRecommendations(): Promise { const configs = await this.getExtensionsConfigs(); return distinct(configs.flatMap(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : [])); @@ -296,6 +303,7 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent { return { recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())), + stronglyRecommended: distinct((extensionsConfigContent.stronglyRecommended || []).map(e => e.toLowerCase())), unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase())) }; } diff --git a/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts new file mode 100644 index 00000000000..def925aad9c --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; +import { localize } from '../../../../nls.js'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { StronglyRecommendedExtensionEntry, renderStronglyRecommendedExtensionList } from '../../../contrib/extensions/browser/stronglyRecommendedExtensionList.js'; +import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; + +export default defineThemedFixtureGroup({ + TwoExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions) }), + SingleExtension: defineComponentFixture({ render: ctx => renderDialog(ctx, singleExtension) }), + ManyExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, manyExtensions) }), + NoneSelected: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions, { allUnchecked: true }) }), +}); + +const twoExtensions: StronglyRecommendedExtensionEntry[] = [ + { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, + { displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' }, +]; + +const singleExtension: StronglyRecommendedExtensionEntry[] = [ + { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, +]; + +const manyExtensions: StronglyRecommendedExtensionEntry[] = [ + { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, + { displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' }, + { displayName: 'ESLint', publisherDisplayName: 'Dirk Baeumer', version: '2.4.4' }, + { displayName: 'Prettier', publisherDisplayName: 'Esben Petersen', version: '10.1.0' }, + { displayName: 'GitLens', publisherDisplayName: 'GitKraken', version: '15.6.2' }, +]; + +function renderDialog({ container, disposableStore }: ComponentFixtureContext, extensions: StronglyRecommendedExtensionEntry[], options?: { allUnchecked?: boolean }): void { + container.style.width = '700px'; + container.style.height = '500px'; + container.style.position = 'relative'; + container.style.overflow = 'hidden'; + + // The dialog uses position:fixed on its modal block, which escapes the shadow DOM container. + // Override to position:absolute so it stays within the fixture bounds. + const fixtureStyle = new CSSStyleSheet(); + fixtureStyle.replaceSync('.monaco-dialog-modal-block { position: absolute; }'); + const shadowRoot = container.getRootNode() as ShadowRoot; + shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, fixtureStyle]; + + const message = extensions.length === 1 + ? localize('strongExtensionFixture', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName) + : localize('strongExtensionsFixture', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length); + + let listResult!: ReturnType; + + const dialog = disposableStore.add(new Dialog( + container, + message, + [ + localize('install', "Install"), + localize('cancel', "Cancel"), + ], + { + type: 'info', + renderBody: (bodyContainer: HTMLElement) => { + listResult = renderStronglyRecommendedExtensionList(bodyContainer, disposableStore, extensions); + }, + buttonOptions: [{ + styleButton: (button) => listResult.styleInstallButton(button), + }], + cancelId: 1, + buttonStyles: defaultButtonStyles, + checkboxStyles: defaultCheckboxStyles, + inputBoxStyles: defaultInputBoxStyles, + dialogStyles: defaultDialogStyles, + } + )); + + dialog.show(); + + if (options?.allUnchecked) { + for (const cb of container.querySelectorAll('.strongly-recommended-extension-row .monaco-custom-toggle')) { + cb.click(); + } + } +} From 843f795ede4dc5ba7c4040eaf815f116b7180d47 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 4 Mar 2026 11:24:19 +0100 Subject: [PATCH 139/448] towards schema based prompt file validation (#299067) --- .../promptSyntax/newPromptFileActions.ts | 2 +- .../promptToolsCodeLensProvider.ts | 4 +- .../contrib/chat/common/chatModes.ts | 3 +- .../PromptHeaderDefinitionProvider.ts | 2 +- .../languageProviders/promptCodeActions.ts | 3 +- .../promptDocumentSemanticTokensProvider.ts | 2 +- .../languageProviders/promptFileAttributes.ts | 436 ++++++++++++++++++ .../promptHeaderAutocompletion.ts | 72 +-- .../languageProviders/promptHovers.ts | 8 +- .../languageProviders/promptValidator.ts | 9 +- .../common/promptSyntax/promptFileParser.ts | 14 - .../service/promptsServiceImpl.ts | 2 +- 12 files changed, 467 insertions(+), 90 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 54a379abecc..a5c04f02c1e 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -27,7 +27,7 @@ import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js' import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js'; import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; -import { getTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js'; +import { getTarget } from '../../common/promptSyntax/languageProviders/promptFileAttributes.js'; /** * Options to override the default folder-picker and editor-open behaviour diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts index e231b63d6d7..599946c97bf 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts @@ -20,9 +20,9 @@ import { registerEditorFeature } from '../../../../../editor/common/editorFeatur import { PromptFileRewriter } from './promptFileRewriter.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IEditorModel } from '../../../../../editor/common/editorCommon.js'; -import { isTarget, parseCommaSeparatedList, PromptHeaderAttributes } from '../../common/promptSyntax/promptFileParser.js'; -import { getTarget, isVSCodeOrDefaultTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js'; +import { parseCommaSeparatedList, PromptHeaderAttributes } from '../../common/promptSyntax/promptFileParser.js'; import { isBoolean } from '../../../../../base/common/types.js'; +import { getTarget, isTarget, isVSCodeOrDefaultTarget } from '../../common/promptSyntax/languageProviders/promptFileAttributes.js'; class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider { diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 8b0e548eaf2..c13b64519ee 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -19,13 +19,14 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IChatAgentService } from './participants/chatAgents.js'; import { ChatContextKeys } from './actions/chatContextKeys.js'; import { ChatConfiguration, ChatModeKind } from './constants.js'; -import { IHandOff, isTarget } from './promptSyntax/promptFileParser.js'; +import { IHandOff } from './promptSyntax/promptFileParser.js'; import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, ICustomAgentVisibility, IPromptsService, isCustomAgentVisibility, PromptsStorage } from './promptSyntax/service/promptsService.js'; import { Target } from './promptSyntax/promptTypes.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { hash } from '../../../../base/common/hash.js'; import { isString } from '../../../../base/common/types.js'; +import { isTarget } from './promptSyntax/languageProviders/promptFileAttributes.js'; export const IChatModeService = createDecorator('chatModeService'); export interface IChatModeService { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts index 8de8c06dfba..b991d1d4db5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts @@ -9,8 +9,8 @@ import { Range } from '../../../../../../editor/common/core/range.js'; import { Definition, DefinitionProvider } from '../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { IChatModeService } from '../../chatModes.js'; -import { getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptHeaderAttributes } from '../promptFileParser.js'; +import { getPromptsTypeForLanguageId } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; export class PromptHeaderDefinitionProvider implements DefinitionProvider { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts index 704d5cd6208..eba207145d1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts @@ -16,9 +16,10 @@ import { Selection } from '../../../../../../editor/common/core/selection.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { getTarget, isVSCodeOrDefaultTarget, MARKERS_OWNER_ID } from './promptValidator.js'; +import { MARKERS_OWNER_ID } from './promptValidator.js'; import { IMarkerData, IMarkerService } from '../../../../../../platform/markers/common/markers.js'; import { CodeActionKind } from '../../../../../../editor/contrib/codeAction/common/types.js'; +import { getTarget, isVSCodeOrDefaultTarget } from './promptFileAttributes.js'; export class PromptCodeActionProvider implements CodeActionProvider { /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts index 3fdf4aa385e..8b5ffb7ae41 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts @@ -8,7 +8,7 @@ import { DocumentSemanticTokensProvider, ProviderResult, SemanticTokens, Semanti import { ITextModel } from '../../../../../../editor/common/model.js'; import { getPromptsTypeForLanguageId } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { getTarget, isVSCodeOrDefaultTarget } from './promptValidator.js'; +import { getTarget, isVSCodeOrDefaultTarget } from './promptFileAttributes.js'; export class PromptDocumentSemanticTokensProvider implements DocumentSemanticTokensProvider { /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts new file mode 100644 index 00000000000..2e99a13df82 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts @@ -0,0 +1,436 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { dirname } from '../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { SpecedToolAliases } from '../../tools/languageModelToolsService.js'; +import { CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder } from '../config/promptFileLocations.js'; +import { PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { PromptsType, Target } from '../promptTypes.js'; + +export namespace GithubPromptHeaderAttributes { + export const mcpServers = 'mcp-servers'; + export const github = 'github'; +} + +export namespace ClaudeHeaderAttributes { + export const disallowedTools = 'disallowedTools'; +} + +export function isTarget(value: unknown): value is Target { + return value === Target.VSCode || value === Target.GitHubCopilot || value === Target.Claude || value === Target.Undefined; +} + + +interface IAttributeDefinition { + readonly type: string; + readonly description: string; + readonly defaults?: readonly string[]; + readonly items?: readonly { name: string; description?: string }[]; + readonly enums?: readonly { name: string; description?: string }[]; +} + +const booleanAttributeEnumValues: readonly IValueEntry[] = [ + { name: 'true' }, + { name: 'false' } +]; + +const targetAttributeEnumValues: readonly IValueEntry[] = [ + { name: 'vscode' }, + { name: 'github-copilot' }, +]; + +// Attribute metadata for prompt files (`*.prompt.md`). +export const promptFileAttributes: Record = { + [PromptHeaderAttributes.name]: { + type: 'scalar', + description: localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'), + }, + [PromptHeaderAttributes.description]: { + type: 'scalar', + description: localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'), + }, + [PromptHeaderAttributes.argumentHint]: { + type: 'scalar', + description: localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'), + }, + [PromptHeaderAttributes.model]: { + type: 'scalar | sequence', + description: localize('promptHeader.prompt.model', 'The model to use in this prompt. Can also be a list of models. The first available model will be used.'), + }, + [PromptHeaderAttributes.tools]: { + type: 'scalar | sequence', + description: localize('promptHeader.prompt.tools', 'The tools to use in this prompt.'), + defaults: ['[]', '[\'search\', \'edit\', \'web\']'], + }, + [PromptHeaderAttributes.agent]: { + type: 'scalar', + description: localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.'), + }, + [PromptHeaderAttributes.mode]: { + type: 'scalar', + description: localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.'), + }, +}; + +// Attribute metadata for instructions files (`*.instructions.md`). +export const instructionAttributes: Record = { + [PromptHeaderAttributes.name]: { + type: 'scalar', + description: localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'), + }, + [PromptHeaderAttributes.description]: { + type: 'scalar', + description: localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'), + }, + [PromptHeaderAttributes.applyTo]: { + type: 'scalar', + description: localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: `**/*.ts`, `**/*.js`, `client/**`'), + defaults: [ + '\'**\'', + '\'**/*.ts, **/*.js\'', + '\'**/*.php\'', + '\'**/*.py\'' + ], + }, + [PromptHeaderAttributes.excludeAgent]: { + type: 'scalar | sequence', + description: localize('promptHeader.instructions.excludeAgent', 'One or more agents to exclude from using this instruction file.'), + }, +}; + +// Attribute metadata for custom agent files (`*.agent.md`). +export const customAgentAttributes: Record = { + [PromptHeaderAttributes.name]: { + type: 'scalar', + description: localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'), + }, + [PromptHeaderAttributes.description]: { + type: 'scalar', + description: localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'), + }, + [PromptHeaderAttributes.argumentHint]: { + type: 'scalar', + description: localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'), + }, + [PromptHeaderAttributes.model]: { + type: 'scalar | sequence', + description: localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'), + }, + [PromptHeaderAttributes.tools]: { + type: 'scalar | sequence', + description: localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'), + defaults: ['[]', '[search, edit, web]'], + }, + [PromptHeaderAttributes.handOffs]: { + type: 'sequence', + description: localize('promptHeader.agent.handoffs', 'Possible handoff actions when the agent has completed its task.'), + }, + [PromptHeaderAttributes.target]: { + type: 'scalar', + description: localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), + enums: targetAttributeEnumValues, + }, + [PromptHeaderAttributes.infer]: { + type: 'scalar', + description: localize('promptHeader.agent.infer', 'Controls visibility of the agent.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.agents]: { + type: 'sequence', + description: localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'), + defaults: ['["*"]'], + }, + [PromptHeaderAttributes.userInvocable]: { + type: 'scalar', + description: localize('promptHeader.agent.userInvocable', 'Whether the agent can be selected and invoked by users in the UI.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.userInvokable]: { + type: 'scalar', + description: localize('promptHeader.agent.userInvokable', 'Deprecated. Use user-invocable instead.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.disableModelInvocation]: { + type: 'scalar', + description: localize('promptHeader.agent.disableModelInvocation', 'If true, prevents the agent from being invoked as a subagent.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.advancedOptions]: { + type: 'map', + description: localize('promptHeader.agent.advancedOptions', 'Advanced options for custom agent behavior.'), + }, + [GithubPromptHeaderAttributes.github]: { + type: 'map', + description: localize('promptHeader.agent.github', 'GitHub-specific configuration for the agent, such as token permissions.'), + }, +}; + +// Attribute metadata for skill files (`SKILL.md`). +export const skillAttributes: Record = { + [PromptHeaderAttributes.name]: { + type: 'scalar', + description: localize('promptHeader.skill.name', 'The name of the skill.'), + }, + [PromptHeaderAttributes.description]: { + type: 'scalar', + description: localize('promptHeader.skill.description', 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'), + }, + [PromptHeaderAttributes.argumentHint]: { + type: 'scalar', + description: localize('promptHeader.skill.argumentHint', 'Hint shown during autocomplete to indicate expected arguments. Example: [issue-number] or [filename] [format]'), + }, + [PromptHeaderAttributes.userInvocable]: { + type: 'scalar', + description: localize('promptHeader.skill.userInvocable', 'Set to false to hide from the / menu. Use for background knowledge users should not invoke directly. Default: true.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.userInvokable]: { + type: 'scalar', + description: localize('promptHeader.skill.userInvokable', 'Deprecated. Use user-invocable instead.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.disableModelInvocation]: { + type: 'scalar', + description: localize('promptHeader.skill.disableModelInvocation', 'Set to true to prevent the agent from automatically loading this skill. Use for workflows you want to trigger manually with /name. Default: false.'), + enums: booleanAttributeEnumValues, + }, + [PromptHeaderAttributes.license]: { + type: 'scalar | map', + description: localize('promptHeader.skill.license', 'License information for the skill.'), + }, + [PromptHeaderAttributes.compatibility]: { + type: 'scalar | map', + description: localize('promptHeader.skill.compatibility', 'Compatibility metadata for environments or runtimes.'), + }, + [PromptHeaderAttributes.metadata]: { + type: 'map', + description: localize('promptHeader.skill.metadata', 'Additional metadata for the skill.'), + }, +}; + +const allAttributeNames: Record = { + [PromptsType.prompt]: Object.keys(promptFileAttributes), + [PromptsType.instructions]: Object.keys(instructionAttributes), + [PromptsType.agent]: Object.keys(customAgentAttributes), + [PromptsType.skill]: Object.keys(skillAttributes), + [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter +}; +const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers, GithubPromptHeaderAttributes.github, PromptHeaderAttributes.infer]; +const recommendedAttributeNames: Record = { + [PromptsType.prompt]: allAttributeNames[PromptsType.prompt].filter(name => !isNonRecommendedAttribute(name)), + [PromptsType.instructions]: allAttributeNames[PromptsType.instructions].filter(name => !isNonRecommendedAttribute(name)), + [PromptsType.agent]: allAttributeNames[PromptsType.agent].filter(name => !isNonRecommendedAttribute(name)), + [PromptsType.skill]: allAttributeNames[PromptsType.skill].filter(name => !isNonRecommendedAttribute(name)), + [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter +}; + +export function getValidAttributeNames(promptType: PromptsType, includeNonRecommended: boolean, target: Target): string[] { + if (target === Target.Claude) { + if (promptType === PromptsType.instructions) { + return Object.keys(claudeRulesAttributes); + } + return Object.keys(claudeAgentAttributes); + } else if (target === Target.GitHubCopilot) { + if (promptType === PromptsType.agent) { + return githubCopilotAgentAttributeNames; + } + } + return includeNonRecommended ? allAttributeNames[promptType] : recommendedAttributeNames[promptType]; +} + +export function isNonRecommendedAttribute(attributeName: string): boolean { + return attributeName === PromptHeaderAttributes.advancedOptions || attributeName === PromptHeaderAttributes.excludeAgent || attributeName === PromptHeaderAttributes.mode || attributeName === PromptHeaderAttributes.infer || attributeName === PromptHeaderAttributes.userInvokable; +} + +export function getAttributeDefinition(attributeName: string, promptType: PromptsType, target: Target): IAttributeDefinition | undefined { + switch (promptType) { + case PromptsType.instructions: + if (target === Target.Claude) { + return claudeRulesAttributes[attributeName]; + } + return instructionAttributes[attributeName]; + case PromptsType.skill: + return skillAttributes[attributeName]; + case PromptsType.agent: + if (target === Target.Claude) { + return claudeAgentAttributes[attributeName]; + } + return customAgentAttributes[attributeName]; + case PromptsType.prompt: + return promptFileAttributes[attributeName]; + default: + return undefined; + } +} + +// The list of tools known to be used by GitHub Copilot custom agents +export const knownGithubCopilotTools = [ + { name: SpecedToolAliases.execute, description: localize('githubCopilot.execute', 'Execute commands') }, + { name: SpecedToolAliases.read, description: localize('githubCopilot.read', 'Read files') }, + { name: SpecedToolAliases.edit, description: localize('githubCopilot.edit', 'Edit files') }, + { name: SpecedToolAliases.search, description: localize('githubCopilot.search', 'Search files') }, + { name: SpecedToolAliases.agent, description: localize('githubCopilot.agent', 'Use subagents') }, +]; + +export interface IValueEntry { + readonly name: string; + readonly description?: string; +} + +export const knownClaudeTools = [ + { name: 'Bash', description: localize('claude.bash', 'Execute shell commands'), toolEquivalent: [SpecedToolAliases.execute] }, + { name: 'Edit', description: localize('claude.edit', 'Make targeted file edits'), toolEquivalent: ['edit/editNotebook', 'edit/editFiles'] }, + { name: 'Glob', description: localize('claude.glob', 'Find files by pattern'), toolEquivalent: ['search/fileSearch'] }, + { name: 'Grep', description: localize('claude.grep', 'Search file contents with regex'), toolEquivalent: ['search/textSearch'] }, + { name: 'Read', description: localize('claude.read', 'Read file contents'), toolEquivalent: ['read/readFile', 'read/getNotebookSummary'] }, + { name: 'Write', description: localize('claude.write', 'Create/overwrite files'), toolEquivalent: ['edit/createDirectory', 'edit/createFile', 'edit/createJupyterNotebook'] }, + { name: 'WebFetch', description: localize('claude.webFetch', 'Fetch URL content'), toolEquivalent: [SpecedToolAliases.web] }, + { name: 'WebSearch', description: localize('claude.webSearch', 'Perform web searches'), toolEquivalent: [SpecedToolAliases.web] }, + { name: 'Task', description: localize('claude.task', 'Run subagents for complex tasks'), toolEquivalent: [SpecedToolAliases.agent] }, + { name: 'Skill', description: localize('claude.skill', 'Execute skills'), toolEquivalent: [] }, + { name: 'LSP', description: localize('claude.lsp', 'Code intelligence (requires plugin)'), toolEquivalent: [] }, + { name: 'NotebookEdit', description: localize('claude.notebookEdit', 'Modify Jupyter notebooks'), toolEquivalent: ['edit/editNotebook'] }, + { name: 'AskUserQuestion', description: localize('claude.askUserQuestion', 'Ask multiple-choice questions'), toolEquivalent: ['vscode/askQuestions'] }, + { name: 'MCPSearch', description: localize('claude.mcpSearch', 'Searches for MCP tools when tool search is enabled'), toolEquivalent: [] } +]; + +export const knownClaudeModels = [ + { name: 'sonnet', description: localize('claude.sonnet', 'Latest Claude Sonnet'), modelEquivalent: 'Claude Sonnet 4.5 (copilot)' }, + { name: 'opus', description: localize('claude.opus', 'Latest Claude Opus'), modelEquivalent: 'Claude Opus 4.6 (copilot)' }, + { name: 'haiku', description: localize('claude.haiku', 'Latest Claude Haiku, fast for simple tasks'), modelEquivalent: 'Claude Haiku 4.5 (copilot)' }, + { name: 'inherit', description: localize('claude.inherit', 'Inherit model from parent agent or prompt'), modelEquivalent: undefined }, +]; + +export function mapClaudeModels(claudeModelNames: readonly string[]): readonly string[] { + const result = []; + for (const name of claudeModelNames) { + const claudeModel = knownClaudeModels.find(model => model.name === name); + if (claudeModel && claudeModel.modelEquivalent) { + result.push(claudeModel.modelEquivalent); + } + } + return result; +} + +/** + * Maps Claude tool names to their VS Code tool equivalents. + */ +export function mapClaudeTools(claudeToolNames: readonly string[]): string[] { + const result: string[] = []; + for (const name of claudeToolNames) { + const claudeTool = knownClaudeTools.find(tool => tool.name === name); + if (claudeTool) { + result.push(...claudeTool.toolEquivalent); + } + } + return result; +} + +export const claudeAgentAttributes: Record = { + 'name': { + type: 'scalar', + description: localize('attribute.name', "Unique identifier using lowercase letters and hyphens (required)"), + }, + 'description': { + type: 'scalar', + description: localize('attribute.description', "When to delegate to this subagent (required)"), + }, + 'tools': { + type: 'sequence', + description: localize('attribute.tools', "Array of tools the subagent can use. Inherits all tools if omitted"), + defaults: ['Read, Edit, Bash'], + items: knownClaudeTools + }, + 'disallowedTools': { + type: 'sequence', + description: localize('attribute.disallowedTools', "Tools to deny, removed from inherited or specified list"), + defaults: ['Write, Edit, Bash'], + items: knownClaudeTools + }, + 'model': { + type: 'scalar', + description: localize('attribute.model', "Model to use: sonnet, opus, haiku, or inherit. Defaults to inherit."), + defaults: ['sonnet', 'opus', 'haiku', 'inherit'], + enums: knownClaudeModels + }, + 'permissionMode': { + type: 'scalar', + description: localize('attribute.permissionMode', "Permission mode: default, acceptEdits, dontAsk, bypassPermissions, or plan."), + defaults: ['default', 'acceptEdits', 'dontAsk', 'bypassPermissions', 'plan'], + enums: [ + { name: 'default', description: localize('claude.permissionMode.default', 'Standard behavior: prompts for permission on first use of each tool.') }, + { name: 'acceptEdits', description: localize('claude.permissionMode.acceptEdits', 'Automatically accepts file edit permissions for the session.') }, + { name: 'plan', description: localize('claude.permissionMode.plan', 'Plan Mode: Claude can analyze but not modify files or execute commands.') }, + { name: 'delegate', description: localize('claude.permissionMode.delegate', 'Coordination-only mode for agent team leads. Only available when an agent team is active.') }, + { name: 'dontAsk', description: localize('claude.permissionMode.dontAsk', 'Auto-denies tools unless pre-approved via /permissions or permissions.allow rules.') }, + { name: 'bypassPermissions', description: localize('claude.permissionMode.bypassPermissions', 'Skips all permission prompts (requires safe environment like containers).') } + ] + }, + 'skills': { + type: 'sequence', + description: localize('attribute.skills', "Skills to load into the subagent's context at startup."), + }, + 'mcpServers': { + type: 'sequence', + description: localize('attribute.mcpServers', "MCP servers available to this subagent."), + }, + 'hooks': { + type: 'object', + description: localize('attribute.hooks', "Lifecycle hooks scoped to this subagent."), + }, + 'memory': { + type: 'scalar', + description: localize('attribute.memory', "Persistent memory scope: user, project, or local. Enables cross-session learning."), + defaults: ['user', 'project', 'local'], + enums: [ + { name: 'user', description: localize('claude.memory.user', "Remember learnings across all projects.") }, + { name: 'project', description: localize('claude.memory.project', "The subagent's knowledge is project-specific and shareable via version control.") }, + { name: 'local', description: localize('claude.memory.local', "The subagent's knowledge is project-specific but should not be checked into version control.") } + ] + } +}; + +/** + * Attributes supported in Claude rules files (`.claude/rules/*.md`). + * Claude rules use `paths` instead of `applyTo` for glob patterns. + */ +export const claudeRulesAttributes: Record = { + 'description': { + type: 'scalar', + description: localize('attribute.rules.description', "A description of what this rule covers, used to provide context about when it applies."), + }, + 'paths': { + type: 'sequence', + description: localize('attribute.rules.paths', "Array of glob patterns that describe for which files the rule applies. Based on these patterns, the file is automatically included in the prompt when the context contains a file that matches.\nExample: `['src/**/*.ts', 'test/**']`"), + }, +}; + +export function isVSCodeOrDefaultTarget(target: Target): boolean { + return target === Target.VSCode || target === Target.Undefined; +} + +export function getTarget(promptType: PromptsType, header: PromptHeader | URI): Target { + const uri = header instanceof URI ? header : header.uri; + if (promptType === PromptsType.agent) { + const parentDir = dirname(uri); + if (parentDir.path.endsWith(`/${CLAUDE_AGENTS_SOURCE_FOLDER}`)) { + return Target.Claude; + } + if (!(header instanceof URI)) { + const target = header.target; + if (target === Target.GitHubCopilot || target === Target.VSCode) { + return target; + } + } + return Target.Undefined; + } else if (promptType === PromptsType.instructions) { + if (isInClaudeRulesFolder(uri)) { + return Target.Claude; + } + } + return Target.Undefined; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 66f81749bf5..14009b90d44 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -15,12 +15,11 @@ import { IChatModeService } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; -import { ClaudeHeaderAttributes, ISequenceValue, IValue, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; -import { getAttributeDescription, getTarget, getValidAttributeNames, claudeAgentAttributes, claudeRulesAttributes, knownClaudeTools, knownGithubCopilotTools, IValueEntry } from './promptValidator.js'; +import { ISequenceValue, IValue, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { getAttributeDefinition, getTarget, getValidAttributeNames, knownClaudeTools, knownGithubCopilotTools, IValueEntry, ClaudeHeaderAttributes, } from './promptFileAttributes.js'; import { localize } from '../../../../../../nls.js'; import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.js'; - export class PromptHeaderAutocompletion implements CompletionItemProvider { /** * Debug display name for this provider. @@ -129,7 +128,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { for (const attribute of attributesToPropose) { const item: CompletionItem = { label: attribute, - documentation: getAttributeDescription(attribute, promptType, target), + documentation: getAttributeDefinition(attribute, promptType, target)?.description, kind: CompletionItemKind.Property, insertText: getInsertText(attribute), insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, @@ -233,29 +232,15 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return { suggestions }; } - private getValueSuggestions(promptType: PromptsType, attribute: string, target: Target): IValueEntry[] { - if (target === Target.Claude) { - const attributeDesc = promptType === PromptsType.instructions ? claudeRulesAttributes[attribute] : claudeAgentAttributes[attribute]; - if (attributeDesc) { - if (attributeDesc.enums) { - return attributeDesc.enums; - } else if (attributeDesc.defaults) { - return attributeDesc.defaults.map(value => ({ name: value })); - } - } - return []; + private getValueSuggestions(promptType: PromptsType, attribute: string, target: Target): readonly IValueEntry[] { + const attributeDesc = getAttributeDefinition(attribute, promptType, target); + if (attributeDesc?.enums) { + return attributeDesc.enums; + } + if (attributeDesc?.defaults) { + return attributeDesc.defaults.map(value => ({ name: value })); } switch (attribute) { - case PromptHeaderAttributes.applyTo: - if (promptType === PromptsType.instructions) { - return [ - { name: `'**'` }, - { name: `'**/*.ts, **/*.js'` }, - { name: `'**/*.php'` }, - { name: `'**/*.py'` } - ]; - } - break; case PromptHeaderAttributes.agent: case PromptHeaderAttributes.mode: if (promptType === PromptsType.prompt) { @@ -268,47 +253,12 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return suggestions; } break; - case PromptHeaderAttributes.target: - if (promptType === PromptsType.agent) { - return [{ name: 'vscode' }, { name: 'github-copilot' }]; - } - break; - case PromptHeaderAttributes.tools: - if (promptType === PromptsType.prompt || promptType === PromptsType.agent) { - return [ - { name: '[]' }, - { name: `['search', 'edit', 'web']` } - ]; - } - break; case PromptHeaderAttributes.model: if (promptType === PromptsType.prompt || promptType === PromptsType.agent) { return this.getModelNames(promptType === PromptsType.agent); } break; - case PromptHeaderAttributes.infer: - if (promptType === PromptsType.agent) { - return [ - { name: 'true' }, - { name: 'false' } - ]; - } - break; - case PromptHeaderAttributes.agents: - if (promptType === PromptsType.agent) { - return [{ name: '["*"]' }]; - } - break; - case PromptHeaderAttributes.userInvocable: - if (promptType === PromptsType.agent || promptType === PromptsType.skill) { - return [{ name: 'true' }, { name: 'false' }]; - } - break; - case PromptHeaderAttributes.disableModelInvocation: - if (promptType === PromptsType.agent || promptType === PromptsType.skill) { - return [{ name: 'true' }, { name: 'false' }]; - } - break; + } return []; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index c223dc31451..273ceef3be0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -15,8 +15,8 @@ import { ILanguageModelToolsService, isToolSet, IToolSet } from '../../tools/lan import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { ClaudeHeaderAttributes, IHeaderAttribute, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; -import { getAttributeDescription, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptValidator.js'; +import { IHeaderAttribute, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { ClaudeHeaderAttributes, getAttributeDefinition, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptFileAttributes.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -73,7 +73,7 @@ export class PromptHoverProvider implements HoverProvider { private async provideHeaderHover(position: Position, promptType: PromptsType, header: PromptHeader, target: Target): Promise { for (const attribute of header.attributes) { if (attribute.range.containsPosition(position)) { - const description = getAttributeDescription(attribute.key, promptType, target); + const description = getAttributeDefinition(attribute.key, promptType, target)?.description; if (description) { switch (attribute.key) { case PromptHeaderAttributes.model: @@ -233,7 +233,7 @@ export class PromptHoverProvider implements HoverProvider { } private getHandsOffHover(attribute: IHeaderAttribute, position: Position, target: Target): Hover | undefined { - const handoffsBaseMessage = getAttributeDescription(PromptHeaderAttributes.handOffs, PromptsType.agent, target)!; + const handoffsBaseMessage = getAttributeDefinition(PromptHeaderAttributes.handOffs, PromptsType.agent, target)?.description!; if (!isVSCodeOrDefaultTarget(target)) { return this.createHover(handoffsBaseMessage + '\n\n' + localize('promptHeader.agent.handoffs.githubCopilot', 'Note: This attribute is not used in GitHub Copilot or Claude targets.'), attribute.range); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 838dfadb33c..569e024d3c8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -16,17 +16,19 @@ import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, SpecedToolAliases } from '../../tools/languageModelToolsService.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; -import { GithubPromptHeaderAttributes, ISequenceValue, IHeaderAttribute, IScalarValue, parseCommaSeparatedList, ParsedPromptFile, PromptHeader, PromptHeaderAttributes, IValue } from '../promptFileParser.js'; +import { ISequenceValue, IHeaderAttribute, IScalarValue, parseCommaSeparatedList, ParsedPromptFile, PromptHeader, IValue, PromptHeaderAttributes } from '../promptFileParser.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IPromptsService } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { AGENTS_SOURCE_FOLDER, isInClaudeAgentsFolder, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; +import { AGENTS_SOURCE_FOLDER, CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { GithubPromptHeaderAttributes } from './promptFileAttributes.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -1022,7 +1024,8 @@ export function isVSCodeOrDefaultTarget(target: Target): boolean { export function getTarget(promptType: PromptsType, header: PromptHeader | URI): Target { const uri = header instanceof URI ? header : header.uri; if (promptType === PromptsType.agent) { - if (isInClaudeAgentsFolder(uri)) { + const parentDir = dirname(uri); + if (parentDir.path.endsWith(`/${CLAUDE_AGENTS_SOURCE_FOLDER}`)) { return Target.Claude; } if (!(header instanceof URI)) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 42ed43f0300..3d04a7cfec3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -10,7 +10,6 @@ import { URI } from '../../../../../base/common/uri.js'; import { parse, YamlNode, YamlParseError } from '../../../../../base/common/yaml.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { PositionOffsetTransformer } from '../../../../../editor/common/core/text/positionToOffsetImpl.js'; -import { Target } from './promptTypes.js'; export class PromptFileParser { constructor() { @@ -87,19 +86,6 @@ export namespace PromptHeaderAttributes { export const disableModelInvocation = 'disable-model-invocation'; } -export namespace GithubPromptHeaderAttributes { - export const mcpServers = 'mcp-servers'; - export const github = 'github'; -} - -export namespace ClaudeHeaderAttributes { - export const disallowedTools = 'disallowedTools'; -} - -export function isTarget(value: unknown): value is Target { - return value === Target.VSCode || value === Target.GitHubCopilot || value === Target.Claude || value === Target.Undefined; -} - export class PromptHeader { private _parsed: ParsedHeader | undefined; 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 05f8827bd0f..75acb1fe94b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -41,7 +41,7 @@ import { HookType } from '../hookTypes.js'; import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js'; 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 { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptFileAttributes.js'; import { StopWatch } from '../../../../../../base/common/stopwatch.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { getCanonicalPluginCommandId, IAgentPlugin, IAgentPluginService } from '../../plugins/agentPluginService.js'; From 780451d291df8d1dc7d2681fb2a0ca095c3f1375 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 4 Mar 2026 11:40:32 +0100 Subject: [PATCH 140/448] Inline chat affordance fixes (#299169) * Add ESC to dismiss inline chat editor affordance Adds a new action bound to Escape that hides the editor affordance without collapsing the selection. Fixes https://github.com/Microsoft/vscode/issues/297994 * fix orphaned separators when toolbar items are hidden Fixes https://github.com/microsoft/vscode/issues/298659 * Add tests for InlineChatAffordance telemetry events * undo instruct-changes --- src/vs/base/common/actions.ts | 18 +++ src/vs/platform/actions/browser/toolbar.ts | 3 +- .../browser/inlineChat.contribution.ts | 1 + .../inlineChat/browser/inlineChatActions.ts | 22 ++- .../browser/inlineChatAffordance.ts | 20 ++- .../contrib/inlineChat/common/inlineChat.ts | 1 + .../test/browser/inlineChatAffordance.test.ts | 153 ++++++++++++++++++ 7 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/test/browser/inlineChatAffordance.test.ts diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 6d3e3f2b3db..18641db33f9 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -220,6 +220,24 @@ export class Separator implements IAction { return out; } + /** + * Removes leading, trailing, and consecutive duplicate separators in-place and returns the actions. + */ + public static clean(actions: IAction[]): IAction[] { + while (actions.length > 0 && actions[0].id === Separator.ID) { + actions.shift(); + } + while (actions.length > 0 && actions[actions.length - 1].id === Separator.ID) { + actions.pop(); + } + for (let i = actions.length - 2; i >= 0; i--) { + if (actions[i].id === Separator.ID && actions[i + 1].id === Separator.ID) { + actions.splice(i + 1, 1); + } + } + return actions; + } + static readonly ID = 'vs.actions.separator'; readonly id: string = Separator.ID; diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index 9304d12db48..e44cdb4eae0 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -184,7 +184,8 @@ export class WorkbenchToolBar extends ToolBar { // coalesce turns Array into IAction[] coalesceInPlace(primary); coalesceInPlace(extraSecondary); - super.setActions(primary, Separator.join(extraSecondary, secondary)); + + super.setActions(Separator.clean(primary), Separator.join(extraSecondary, secondary)); // add context menu for toggle and configure keybinding actions if (toggleActions.length > 0 || primary.length > 0) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 2220f7d1474..85237c14100 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -96,6 +96,7 @@ registerAction2(InlineChatActions.SubmitInlineChatInputAction); registerAction2(InlineChatActions.QueueInChatAction); registerAction2(InlineChatActions.HideInlineChatInputAction); registerAction2(InlineChatActions.FixDiagnosticsAction); +registerAction2(InlineChatActions.DismissEditorAffordanceAction); 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 ec5ac5e2167..d282737cbbc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, 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, CTX_INLINE_CHAT_PENDING_CONFIRMATION, InlineChatConfigKeys, CTX_FIX_DIAGNOSTICS_ENABLED } 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, CTX_INLINE_CHAT_PENDING_CONFIRMATION, InlineChatConfigKeys, CTX_FIX_DIAGNOSTICS_ENABLED, CTX_INLINE_CHAT_AFFORDANCE_VISIBLE } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; @@ -506,6 +506,26 @@ export class AskInChatAction extends EditorAction2 { } } +export class DismissEditorAffordanceAction extends EditorAction2 { + + constructor() { + super({ + id: 'inlineChat.dismissEditorAffordance', + title: localize2('dismissAffordance', "Dismiss Editor Affordance"), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_AFFORDANCE_VISIBLE, ContextKeyExpr.equals('config.inlineChat.affordance', 'editor')), + keybinding: { + when: EditorContextKeys.editorTextFocus, + weight: KeybindingWeight.EditorContrib, + primary: KeyCode.Escape, + } + }); + } + + override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): void { + InlineChatController.get(editor)?.inputOverlayWidget.dismiss(); + } +} + export class QueueInChatAction extends AbstractInlineChatAction { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 4e3cf4c6600..961c1943e74 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -9,7 +9,7 @@ import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { ScrollType } from '../../../../editor/common/editorCommon.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { InlineChatConfigKeys } from '../common/inlineChat.js'; +import { InlineChatConfigKeys, CTX_INLINE_CHAT_AFFORDANCE_VISIBLE } from '../common/inlineChat.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; @@ -24,6 +24,7 @@ import { CodeActionController } from '../../../../editor/contrib/codeAction/brow import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { Event } from '../../../../base/common/event.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; type InlineChatAffordanceEvent = { mode: string; @@ -45,6 +46,7 @@ export class InlineChatAffordance extends Disposable { readonly #inputWidget: InlineChatInputWidget; readonly #instantiationService: IInstantiationService; readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number; placeholder: string } | undefined>(this, undefined); + readonly #selectionData = observableValue(this, undefined); constructor( editor: ICodeEditor, @@ -54,6 +56,7 @@ export class InlineChatAffordance extends Disposable { @IChatEntitlementService chatEntiteldService: IChatEntitlementService, @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @ITelemetryService telemetryService: ITelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); this.#editor = editor; @@ -64,7 +67,10 @@ export class InlineChatAffordance extends Disposable { const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService); const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); - const selectionData = observableValue(this, undefined); + const selectionData = this.#selectionData; + + const ctxAffordanceVisible = CTX_INLINE_CHAT_AFFORDANCE_VISIBLE.bindTo(contextKeyService); + this._store.add({ dispose: () => ctxAffordanceVisible.reset() }); let explicitSelection = false; let affordanceId: string | undefined; @@ -114,6 +120,12 @@ export class InlineChatAffordance extends Disposable { selectionData.set(undefined, undefined); })); + this._store.add(autorun(r => { + const sel = selectionData.read(r); + const mode = affordance.read(r); + ctxAffordanceVisible.set(sel !== undefined && (mode === 'editor' || mode === 'gutter')); + })); + const gutterAffordance = this._store.add(this.#instantiationService.createInstance( InlineChatGutterAffordance, editorObs, @@ -167,6 +179,10 @@ export class InlineChatAffordance extends Disposable { })); } + dismiss(): void { + this.#selectionData.set(undefined, undefined); + } + async showMenuAtSelection(placeholder: string): Promise { assertType(this.#editor.hasModel()); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 99e23c1d696..69fd6f6674c 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -130,6 +130,7 @@ export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('i 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_PENDING_CONFIRMATION = new RawContextKey('inlineChatPendingConfirmation', false, localize('inlineChatPendingConfirmation', "Whether an inline chat request is pending user confirmation")); +export const CTX_INLINE_CHAT_AFFORDANCE_VISIBLE = new RawContextKey('inlineChatAffordanceVisible', false, localize('inlineChatAffordanceVisible', "Whether an inline chat affordance widget is visible")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE) diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatAffordance.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatAffordance.test.ts new file mode 100644 index 00000000000..85719bf152c --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatAffordance.test.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { Selection } from '../../../../../editor/common/core/selection.js'; +import { CursorChangeReason } from '../../../../../editor/common/cursorEvents.js'; +import { CursorState } from '../../../../../editor/common/cursorCommon.js'; +import { createTextModel } from '../../../../../editor/test/common/testTextModel.js'; +import { instantiateTestCodeEditor, ITestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; +import { Event } from '../../../../../base/common/event.js'; +import { InlineChatAffordance } from '../../browser/inlineChatAffordance.js'; +import { InlineChatInputWidget } from '../../browser/inlineChatOverlayWidget.js'; +import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { timeout } from '../../../../../base/common/async.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { InlineChatConfigKeys } from '../../common/inlineChat.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { mock } from '../../../../../base/test/common/mock.js'; + +function createMockInputWidget(): InlineChatInputWidget { + return new class extends mock() { + override readonly position = observableValue('test.position', null); + override show() { } + override hide() { } + override dispose() { } + }; +} + +suite('InlineChatAffordance - Telemetry', () => { + + const store = new DisposableStore(); + let editor: ITestCodeEditor; + let model: ITextModel; + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let telemetryEvents: { eventName: string; data: Record }[]; + + setup(() => { + telemetryEvents = []; + + instantiationService = workbenchInstantiationService({ + configurationService: () => new TestConfigurationService({ + [InlineChatConfigKeys.Affordance]: 'editor', + }), + }, store); + + configurationService = instantiationService.get(IConfigurationService) as TestConfigurationService; + + instantiationService.stub(ITelemetryService, new class extends mock() { + override publicLog2(eventName: string, data?: Record) { + telemetryEvents.push({ eventName, data: data ?? {} }); + } + }); + + instantiationService.stub(IInlineChatSessionService, new class extends mock() { + override readonly onWillStartSession = Event.None; + override readonly onDidChangeSessions = Event.None; + override getSessionByTextModel() { return undefined; } + override getSessionBySessionUri() { return undefined; } + }); + + model = store.add(createTextModel('hello world\nfoo bar\nbaz qux')); + editor = store.add(instantiateTestCodeEditor(instantiationService, model)); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function setExplicitSelection(sel: Selection): void { + editor.getViewModel()!.setCursorStates( + 'test', + CursorChangeReason.Explicit, + [CursorState.fromModelSelection(sel)] + ); + } + + test('shown event includes mode "editor"', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + store.add(instantiationService.createInstance(InlineChatAffordance, editor, createMockInputWidget())); + + setExplicitSelection(new Selection(1, 1, 1, 6)); + await timeout(600); + + const shown = telemetryEvents.filter(e => e.eventName === 'inlineChatAffordance/shown'); + assert.strictEqual(shown.length, 1); + assert.strictEqual(shown[0].data.mode, 'editor'); + assert.ok(typeof shown[0].data.id === 'string'); + assert.strictEqual(shown[0].data.commandId, ''); + })); + + test('shown event does NOT fire when mode is off', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + configurationService.setUserConfiguration(InlineChatConfigKeys.Affordance, 'off'); + configurationService.onDidChangeConfigurationEmitter.fire(new class extends mock() { + override affectsConfiguration(key: string) { return key === InlineChatConfigKeys.Affordance; } + }); + + store.add(instantiationService.createInstance(InlineChatAffordance, editor, createMockInputWidget())); + + setExplicitSelection(new Selection(1, 1, 1, 6)); + await timeout(600); + + assert.strictEqual(telemetryEvents.filter(e => e.eventName === 'inlineChatAffordance/shown').length, 0); + })); + + test('shown event does NOT fire for whitespace-only selection', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + model.setValue(' \nhello'); + + store.add(instantiationService.createInstance(InlineChatAffordance, editor, createMockInputWidget())); + + setExplicitSelection(new Selection(1, 1, 1, 4)); + await timeout(600); + + assert.strictEqual(telemetryEvents.filter(e => e.eventName === 'inlineChatAffordance/shown').length, 0); + })); + + test('shown event does NOT fire for empty selection', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + store.add(instantiationService.createInstance(InlineChatAffordance, editor, createMockInputWidget())); + + setExplicitSelection(new Selection(1, 1, 1, 1)); + await timeout(600); + + assert.strictEqual(telemetryEvents.filter(e => e.eventName === 'inlineChatAffordance/shown').length, 0); + })); + + test('each selection gets a unique affordanceId', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + store.add(instantiationService.createInstance(InlineChatAffordance, editor, createMockInputWidget())); + + setExplicitSelection(new Selection(1, 1, 1, 6)); + await timeout(600); + + // Clear selection, then make a new one + setExplicitSelection(new Selection(2, 1, 2, 1)); + await timeout(100); + setExplicitSelection(new Selection(2, 1, 2, 4)); + await timeout(600); + + const shown = telemetryEvents.filter(e => e.eventName === 'inlineChatAffordance/shown'); + assert.strictEqual(shown.length, 2); + assert.notStrictEqual(shown[0].data.id, shown[1].data.id); + })); +}); From 0ac17b9c3ba3371296d13e8cd15c0d2a0275e801 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 10:30:31 +0000 Subject: [PATCH 141/448] 2026 theme: update misc UI component styles --- extensions/theme-2026/themes/2026-dark.json | 1 + extensions/theme-2026/themes/2026-light.json | 6 + extensions/theme-2026/themes/styles.css | 103 ------------------ src/vs/base/browser/ui/menu/menu.ts | 2 + .../contrib/find/browser/findWidget.css | 2 +- src/vs/editor/contrib/hover/browser/hover.css | 4 +- .../parts/editor/media/breadcrumbscontrol.css | 4 + .../preferences/browser/media/keybindings.css | 1 + 8 files changed, 18 insertions(+), 105 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 032cdd12cf3..4645a08fa61 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -10,6 +10,7 @@ "descriptionForeground": "#8C8C8C", "icon.foreground": "#8C8C8C", "focusBorder": "#3994BCB3", + "contrastBorder": "#333536", "textBlockQuote.background": "#242526", "textBlockQuote.border": "#2A2B2CFF", "textCodeBlock.background": "#242526", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 13dcdef5c3e..a3a4951704d 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -10,6 +10,7 @@ "descriptionForeground": "#606060", "icon.foreground": "#606060", "focusBorder": "#0069CCFF", + "contrastBorder": "#F0F1F2FF", "textBlockQuote.background": "#EAEAEA", "textBlockQuote.border": "#F0F1F2FF", "textCodeBlock.background": "#EAEAEA", @@ -135,6 +136,10 @@ "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F0F1F2FF", "editorWidget.background": "#FAFAFD", + "editorWidget.border": "#F0F1F2FF", + "editorWidget.foreground": "#202020", + "editorSuggestWidget.background": "#FAFAFD", + "editorSuggestWidget.border": "#F0F1F2FF", "editorWidget.border": "#EEEEF1", "editorWidget.foreground": "#202020", "editorSuggestWidget.background": "#FAFAFD", @@ -143,6 +148,7 @@ "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", "editorHoverWidget.background": "#FAFAFD", + "editorHoverWidget.border": "#F0F1F2FF", "editorHoverWidget.border": "#EEEEF1", "peekView.border": "#0069CC", "peekViewEditor.background": "#FAFAFD", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 050f706aa1e..d76e07491ed 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -22,27 +22,6 @@ } /* Quick Input (Command Palette) */ -.monaco-workbench.vs-dark .quick-input-widget { - border: 1px solid var(--vscode-menu-border) !important; -} - -.monaco-workbench .quick-input-widget .quick-input-header, -.monaco-workbench .quick-input-widget .quick-input-list, -.monaco-workbench .quick-input-widget .quick-input-titlebar, -.monaco-workbench .quick-input-widget .quick-input-title, -.monaco-workbench .quick-input-widget .quick-input-description, -.monaco-workbench .quick-input-widget .quick-input-filter, -.monaco-workbench .quick-input-widget .quick-input-action, -.monaco-workbench .quick-input-widget .quick-input-message, -.monaco-workbench .quick-input-widget .monaco-list, -.monaco-workbench .quick-input-widget .monaco-list-row:not(:has(.quick-input-list-separator-border)) { - border-color: transparent !important; - outline: none !important; -} - -.monaco-workbench .quick-input-widget .quick-input-list .monaco-list-rows { - background: transparent !important; -} .monaco-workbench .quick-input-list .quick-input-list-entry .quick-input-list-separator { height: 16px; @@ -67,82 +46,6 @@ padding: 0; } -.monaco-workbench .quick-input-widget .monaco-list-rows { - background: transparent !important; -} - -.monaco-workbench .quick-input-widget .monaco-inputbox { - background: transparent !important; -} - -.monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { - background: color-mix(in srgb, var(--vscode-input-background) 60%, transparent) !important; -} - -/* Chat Widget */ - -.monaco-workbench .interactive-session .chat-question-carousel-container { - border-radius: var(--radius-lg); -} - -.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; -} - -.monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { - border-radius: 0 0 var(--radius-lg) var(--radius-lg); -} - -.monaco-workbench.vs .interactive-session .chat-input-container { - box-shadow: inset var(--shadow-sm); -} -.monaco-workbench .part.panel .interactive-session, -.monaco-workbench .part.auxiliarybar .interactive-session { - position: relative; -} - -.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { - background-color: transparent !important; -} - -/* Notifications */ - -.monaco-workbench .notifications-list-container .monaco-list-rows { - background: transparent !important; -} - -/* Context Menus */ -.monaco-workbench .action-widget .action-widget-action-bar { - background: transparent; -} - -/* Suggest Widget */ -.monaco-workbench.vs-dark .monaco-editor .suggest-widget { - border: 1px solid var(--vscode-editorWidget-border); -} - -/* Dialog */ -.monaco-workbench .monaco-dialog-box { - border: 1px solid var(--vscode-dialog-border); -} - -/* Peek View */ -.monaco-workbench .monaco-editor .peekview-widget .head, -.monaco-workbench .monaco-editor .peekview-widget .body { - background: transparent !important; -} - -.monaco-workbench .defineKeybindingWidget { - border: 1px solid var(--vscode-editorWidget-border); -} - -/* Chat Editor Overlay */ -.monaco-workbench.vs-dark .chat-editor-overlay-widget, -.monaco-workbench.vs-dark .chat-diff-change-content-widget { - border: 1px solid var(--vscode-editorWidget-border); -} - /* Settings */ .monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { border-radius: var(--radius-sm); @@ -151,12 +54,6 @@ border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } -/* Breadcrumbs */ - -.monaco-workbench.vs .breadcrumbs-control { - border-bottom: 1px solid var(--vscode-editorWidget-border); -} - /* Input Boxes */ .monaco-inputbox .monaco-action-bar .action-item .codicon, .monaco-workbench .search-container .input-box, diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 0b6afe0e2eb..500b0614ade 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -1240,6 +1240,8 @@ ${formatRule(Codicon.menuSubmenu)} animation: fadeIn 0.083s linear; -webkit-app-region: no-drag; box-shadow: var(--vscode-shadow-lg${style.shadowColor ? `, 0 0 12px ${style.shadowColor}` : ''}); + border-radius: var(--vscode-cornerRadius-large); + overflow: hidden; } .context-view.monaco-menu-container :focus, diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index 42e63375f2c..62c6056c1d9 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -7,7 +7,7 @@ .monaco-editor .find-widget { position: absolute; z-index: 35; - height: 33px; + height: 34px; overflow: hidden; line-height: 19px; transition: transform 200ms linear; diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 5817cd9a3c5..d9d64ffc216 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -15,7 +15,8 @@ .monaco-editor .monaco-resizable-hover > .monaco-hover { border: none; - border-radius: unset; + border-radius: inherit; + overflow: hidden; } .monaco-editor .monaco-hover { @@ -35,6 +36,7 @@ } .monaco-editor .monaco-hover .hover-row { + border-radius: var(--vscode-cornerRadius-large); display: flex; } diff --git a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css index 4e4e923ad9b..a80a925d97a 100644 --- a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench.vs .breadcrumbs-control { + border-bottom: 1px solid var(--vscode-editorWidget-border); +} + .monaco-workbench .part.editor > .content .editor-group-container .breadcrumbs-control.hidden { display: none; } diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css index 75905e2e6d4..482f819ee58 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css @@ -8,6 +8,7 @@ border-radius: var(--vscode-cornerRadius-large); position: absolute; box-shadow: var(--vscode-shadow-lg); + border: 1px solid var(--vscode-editorWidget-border); } .defineKeybindingWidget .message { From 033cffbdaea378f11dc724b37d5c06f848688c95 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 10:53:17 +0000 Subject: [PATCH 142/448] 2026 theme: refine editor widget styles and remove redundant properties --- extensions/theme-2026/themes/2026-light.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index a3a4951704d..f5a8730719e 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -140,16 +140,11 @@ "editorWidget.foreground": "#202020", "editorSuggestWidget.background": "#FAFAFD", "editorSuggestWidget.border": "#F0F1F2FF", - "editorWidget.border": "#EEEEF1", - "editorWidget.foreground": "#202020", - "editorSuggestWidget.background": "#FAFAFD", - "editorSuggestWidget.border": "#EEEEF1", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", "editorHoverWidget.background": "#FAFAFD", "editorHoverWidget.border": "#F0F1F2FF", - "editorHoverWidget.border": "#EEEEF1", "peekView.border": "#0069CC", "peekViewEditor.background": "#FAFAFD", "peekViewEditor.matchHighlightBackground": "#0069CC33", From 34659c0ef113dc827fc5b1f190621adb09ae3f7b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 12:51:00 +0100 Subject: [PATCH 143/448] Revert "sessions - improve session hover title rendering and persistence" (#299168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "sessions - improve session hover title rendering and persistence (#29…" This reverts commit ff7ffa542f145bbd0bad82c38ed1ba1c54f72ec3. --- .../sessions/contrib/sessions/browser/sessionsViewPane.ts | 2 +- .../chat/browser/agentSessions/agentSessionsControl.ts | 2 +- .../chat/browser/agentSessions/agentSessionsViewer.ts | 6 ++---- .../componentFixtures/agentSessionsViewer.fixture.ts | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 9a9f060ad70..41865aac2fb 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -143,7 +143,7 @@ export class AgenticSessionsViewPane extends ViewPane { source: 'agentSessionsViewPane', filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, - useSimpleHover: true, + disableHover: true, showIsolationIcon: true, enableApprovalRow: true, getHoverPosition: () => this.getSessionHoverPosition(), diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 57ca104e997..6f09474087b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -44,7 +44,7 @@ export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOption readonly overrideStyles: IStyleOverride; readonly filter: IAgentSessionsFilter; readonly source: string; - readonly useSimpleHover?: boolean; + readonly disableHover?: boolean; readonly showIsolationIcon?: boolean; readonly enableApprovalRow?: boolean; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 9e8462abd00..5efe1358381 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -87,7 +87,7 @@ interface IAgentSessionItemTemplate { } export interface IAgentSessionRendererOptions { - readonly useSimpleHover?: boolean; + readonly disableHover?: boolean; readonly showIsolationIcon?: boolean; getHoverPosition(): HoverPosition; } @@ -402,9 +402,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private renderHover(session: ITreeNode, template: IAgentSessionItemTemplate): void { - if (this.options.useSimpleHover) { - const title = renderAsPlaintext(new MarkdownString(session.element.label)); - template.elementDisposable.add(this.hoverService.setupDelayedHover(template.element, { content: title, position: { hoverPosition: this.options.getHoverPosition() } }, { groupId: 'agent.sessions' })); + if (this.options.disableHover) { return; } diff --git a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts index 90ff4a6f730..f88f88719f4 100644 --- a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts @@ -68,7 +68,7 @@ function wrapAsTreeNode(element: T): ITreeNode { } const rendererOptions: IAgentSessionRendererOptions = { - useSimpleHover: true, + disableHover: true, getHoverPosition: () => HoverPosition.BELOW, }; From d724d414562a3690dc83f9a95a8e622c4bd740d4 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 4 Mar 2026 12:58:44 +0100 Subject: [PATCH 144/448] send workspace data to extension host behind a flag (#299179) --- .../browser/workspaceFolderManagement.ts | 4 ++- .../electron-browser/sessions.main.ts | 32 +++++++++++-------- .../browser/workspaceContextService.ts | 13 ++++---- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts index ecbd59a5213..3bdba7d96a6 100644 --- a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts +++ b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts @@ -15,10 +15,12 @@ import { URI } from '../../../../base/common/uri.js'; import { autorun } from '../../../../base/common/observable.js'; import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js'; import { getGitHubRemoteFileDisplayName } from '../../fileTreeView/browser/githubFileSystemProvider.js'; +import { Queue } from '../../../../base/common/async.js'; export class WorkspaceFolderManagementContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.workspaceFolderManagement'; + private queue = this._register(new Queue()); constructor( @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, @@ -30,7 +32,7 @@ export class WorkspaceFolderManagementContribution extends Disposable implements super(); this._register(autorun(reader => { const activeSession = this.sessionManagementService.activeSession.read(reader); - this.updateWorkspaceFoldersForSession(activeSession); + this.queue.queue(() => this.updateWorkspaceFoldersForSession(activeSession)); })); } diff --git a/src/vs/sessions/electron-browser/sessions.main.ts b/src/vs/sessions/electron-browser/sessions.main.ts index 23f71e9d66a..3a5ed7dff30 100644 --- a/src/vs/sessions/electron-browser/sessions.main.ts +++ b/src/vs/sessions/electron-browser/sessions.main.ts @@ -15,7 +15,7 @@ import { INativeWorkbenchEnvironmentService, NativeWorkbenchEnvironmentService } import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js'; import { ILoggerService, ILogService, LogLevel } from '../../platform/log/common/log.js'; import { NativeWorkbenchStorageService } from '../../workbench/services/storage/electron-browser/storageService.js'; -import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier } from '../../platform/workspace/common/workspace.js'; +import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier, IWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; import { IWorkbenchConfigurationService } from '../../workbench/services/configuration/common/configuration.js'; import { IStorageService } from '../../platform/storage/common/storage.js'; import { Disposable } from '../../base/common/lifecycle.js'; @@ -67,6 +67,7 @@ import { NativeMenubarControl } from '../../workbench/electron-browser/parts/tit import { IWorkspaceEditingService } from '../../workbench/services/workspaces/common/workspaceEditing.js'; import { ConfigurationService } from '../services/configuration/browser/configurationService.js'; import { SessionsWorkspaceContextService } from '../services/workspace/browser/workspaceContextService.js'; +import { getWorkspaceIdentifier } from '../../workbench/services/workspaces/browser/workspaces.js'; export class SessionsMain extends Disposable { @@ -291,21 +292,21 @@ export class SessionsMain extends Disposable { // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // Workspace - const workspaceContextService = new SessionsWorkspaceContextService(uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(userDataProfilesService.profilesHome), 'agent-sessions.code-workspace'), uriIdentityService); - serviceCollection.set(IWorkspaceContextService, workspaceContextService); - serviceCollection.set(IWorkspaceEditingService, workspaceContextService); + const workspaceIdentifier = getWorkspaceIdentifier(uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(userDataProfilesService.profilesHome), 'agent-sessions.code-workspace')); - const [configurationService, storageService] = await Promise.all([ - this.createConfigurationService(userDataProfileService, fileService, logService, policyService).then(service => { + const [{ configurationService, workspaceContextService }, storageService] = await Promise.all([ + this.createWorkspaceAndConfigurationService(workspaceIdentifier, userDataProfileService, uriIdentityService, fileService, logService, policyService).then(services => { // Configuration - serviceCollection.set(IWorkbenchConfigurationService, service); + serviceCollection.set(IWorkbenchConfigurationService, services.configurationService); + // Workspace + serviceCollection.set(IWorkspaceContextService, services.workspaceContextService); + serviceCollection.set(IWorkspaceEditingService, services.workspaceContextService); - return service; + return services; }), - this.createStorageService(workspaceContextService.getWorkspace(), environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => { + this.createStorageService(workspaceIdentifier, environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => { // Storage serviceCollection.set(IStorageService, service); @@ -343,20 +344,23 @@ export class SessionsMain extends Disposable { return { serviceCollection, logService, storageService, configurationService }; } - private async createConfigurationService( + private async createWorkspaceAndConfigurationService( + workspaceIdentifier: IWorkspaceIdentifier, userDataProfileService: IUserDataProfileService, + uriIdentityService: IUriIdentityService, fileService: FileService, logService: ILogService, policyService: IPolicyService - ): Promise { + ): Promise<{ configurationService: ConfigurationService; workspaceContextService: SessionsWorkspaceContextService }> { const configurationService = new ConfigurationService(userDataProfileService.currentProfile.settingsResource, fileService, policyService, logService); try { await configurationService.initialize(); - return configurationService; } catch (error) { onUnexpectedError(error); - return configurationService; } + + const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService, configurationService); + return { configurationService, workspaceContextService }; } private async createStorageService(workspace: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, mainProcessService: IMainProcessService): Promise { diff --git a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts index 58b4dfb5889..9d8b6763dab 100644 --- a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts +++ b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts @@ -11,8 +11,9 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { Workspace, WorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js'; import { getWorkspaceIdentifier } from '../../../../workbench/services/workspaces/browser/workspaces.js'; -import { IDidEnterWorkspaceEvent, IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; +import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; export class SessionsWorkspaceContextService extends Disposable implements IWorkspaceContextService, IWorkspaceEditingService { @@ -20,7 +21,7 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork readonly onDidChangeWorkbenchState = Event.None; readonly onDidChangeWorkspaceName = Event.None; - readonly onDidEnterWorkspace = Event.None as Event; + readonly onDidEnterWorkspace = Event.None; private readonly _onWillChangeWorkspaceFolders = new Emitter(); readonly onWillChangeWorkspaceFolders = this._onWillChangeWorkspaceFolders.event; @@ -32,11 +33,11 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork private readonly _updateFoldersQueue = this._register(new Queue()); constructor( - sessionsWorkspaceUri: URI, - private readonly uriIdentityService: IUriIdentityService + workspaceIdentifier: IWorkspaceIdentifier, + private readonly uriIdentityService: IUriIdentityService, + private readonly configurationService: IConfigurationService, ) { super(); - const workspaceIdentifier = getWorkspaceIdentifier(sessionsWorkspaceUri); this.workspace = new Workspace(workspaceIdentifier.id, [], false, workspaceIdentifier.configPath, uri => uriIdentityService.extUri.ignorePathCasing(uri)); } @@ -53,7 +54,7 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork } hasWorkspaceData(): boolean { - return false; + return this.configurationService.getValue('sessions.workspace.sendWorkspaceDataToExtHost') === true; } getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { From 0be0030e0edb344cbb7fbafddb0039e8663824a3 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 13:06:00 +0100 Subject: [PATCH 145/448] Support multi line terminal command approvals with ellipsis and aligned approve button --- .../agentSessions/agentSessionsViewer.ts | 33 ++++-- .../media/agentsessionsviewer.css | 3 +- .../agentSessionsViewer.fixture.ts | 110 ++++++++++++++++++ 3 files changed, 137 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 9e8462abd00..74140b20870 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -96,7 +96,14 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre static readonly TEMPLATE_ID = 'agent-session'; - static readonly APPROVAL_ROW_HEIGHT = 40; + static readonly APPROVAL_ROW_MAX_LINES = 3; + private static readonly _APPROVAL_ROW_LINE_HEIGHT = 18; + private static readonly _APPROVAL_ROW_OVERHEAD = 14; // 4px margin-top + 4px padding-top + 4px padding-bottom + 2px border + + static getApprovalRowHeight(label: string): number { + const lineCount = Math.min(label.split('\n').length, AgentSessionRenderer.APPROVAL_ROW_MAX_LINES); + return lineCount * AgentSessionRenderer._APPROVAL_ROW_LINE_HEIGHT + AgentSessionRenderer._APPROVAL_ROW_OVERHEAD; + } readonly templateId = AgentSessionRenderer.TEMPLATE_ID; @@ -459,13 +466,24 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre template.approvalRow.classList.toggle('visible', visible); if (info) { - // Render as a syntax-highlighted code block - const codeblockContent = new MarkdownString().appendCodeblock(info.languageId ?? 'json', info.label); - this.renderMarkdownOrText(codeblockContent, template.approvalLabel, buttonStore); + // Render up to 3 lines, each as a separate code block so CSS can truncate per-line + const lines = info.label.split('\n'); + const maxLines = AgentSessionRenderer.APPROVAL_ROW_MAX_LINES; + const visibleLines = lines.slice(0, maxLines); + if (lines.length > maxLines) { + visibleLines[maxLines - 1] = `${visibleLines[maxLines - 1]} \u2026`; + } + const langId = info.languageId ?? 'json'; + const labelContent = new MarkdownString(); + for (const line of visibleLines) { + labelContent.appendCodeblock(langId, line); + } + this.renderMarkdownOrText(labelContent, template.approvalLabel, buttonStore); // Hover with full content as a code block + const fullContent = new MarkdownString().appendCodeblock(info.languageId ?? 'json', info.label); buttonStore.add(this.hoverService.setupDelayedHover(template.approvalLabel, { - content: codeblockContent, + content: fullContent, style: HoverStyle.Pointer, position: { hoverPosition: HoverPosition.BELOW }, })); @@ -607,8 +625,9 @@ export class AgentSessionsListDelegate implements IListVirtualDelegate { + const resource = URI.parse('vscode-chat-session://local/approval-1line'); + const approvalModel = createMockApprovalModel(resource, { + label: 'npm install --save express@latest', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Install express', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 3 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRow2Lines: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-2lines'); + const approvalModel = createMockApprovalModel(resource, { + label: 'cd /workspace/project\nnpm install', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Setup project dependencies', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 3 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRow3Lines: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-3lines'); + const approvalModel = createMockApprovalModel(resource, { + label: 'cd /workspace/project\nnpm install\nnpm run build', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Build the project', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 2 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRow4Lines: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-4lines'); + const approvalModel = createMockApprovalModel(resource, { + label: 'cd /workspace/project\nnpm install\nnpm run build\nnpm run test -- --coverage', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Build and test project', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 2 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRow3LongLines: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-3longlines'); + const approvalModel = createMockApprovalModel(resource, { + label: 'RUSTFLAGS="-C target-cpu=native -C opt-level=3" cargo build --release --target x86_64-unknown-linux-gnu\nfind ./target/release -name "*.so" -exec strip --strip-unneeded {} \\; && tar czf release-bundle.tar.gz -C target/release .\ncurl -X POST https://deploy.internal.example.com/api/v2/artifacts/upload --header "Authorization: Bearer $DEPLOY_TOKEN" --form "bundle=@release-bundle.tar.gz"', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Build and deploy native release', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 2 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), }); From 3674f5ad58f12d2fa12c50c9a21633dc20cc3e6b Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:09:28 +0100 Subject: [PATCH 146/448] Add optional codeSelection property and refactor feedback comment (#299005) * feat: add optional codeSelection property to feedback comment and agent feedback variable entries * refactor: remove unused range property and model service from feedback comment renderer --- .../browser/agentFeedbackAttachment.ts | 1 + .../browser/agentFeedbackHover.ts | 23 ++++++------------- .../common/attachments/chatVariableEntries.ts | 1 + 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts index e45f96e488d..04e66cf8dd0 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts @@ -74,6 +74,7 @@ export class AgentFeedbackAttachmentContribution extends Disposable { text: f.text, resourceUri: f.resourceUri, range: f.range, + codeSelection: this._snippetCache.get(f.id), })), value, }; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts index 9b38ad3c63b..ea4414455e5 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts @@ -15,10 +15,8 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { basename } from '../../../../base/common/path.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { IRange } from '../../../../editor/common/core/range.js'; import { URI } from '../../../../base/common/uri.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { localize } from '../../../../nls.js'; import { FileKind } from '../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; @@ -44,7 +42,7 @@ interface IFeedbackCommentElement { readonly id: string; readonly text: string; readonly resourceUri: URI; - readonly range: IRange; + readonly codeSelection?: string; } type FeedbackTreeElement = IFeedbackFileElement | IFeedbackCommentElement; @@ -151,7 +149,6 @@ class FeedbackCommentRenderer implements ITreeRenderer; } From f7d4b70acba251ecfffb0b05b21c152f730b1920 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:12:11 +0100 Subject: [PATCH 147/448] Update src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/agentSessions/agentSessionsViewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 74140b20870..235ef27d683 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -101,7 +101,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre private static readonly _APPROVAL_ROW_OVERHEAD = 14; // 4px margin-top + 4px padding-top + 4px padding-bottom + 2px border static getApprovalRowHeight(label: string): number { - const lineCount = Math.min(label.split('\n').length, AgentSessionRenderer.APPROVAL_ROW_MAX_LINES); + const lineCount = Math.min(label.split(/\r?\n/).length, AgentSessionRenderer.APPROVAL_ROW_MAX_LINES); return lineCount * AgentSessionRenderer._APPROVAL_ROW_LINE_HEIGHT + AgentSessionRenderer._APPROVAL_ROW_OVERHEAD; } From 1722624c4ace5469c3c6e68304d7a85efcdfd67e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:17:09 +0100 Subject: [PATCH 148/448] Git - expose `rebase()` though the extension API (#299181) --- extensions/git/src/api/api1.ts | 4 ++++ extensions/git/src/api/git.d.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index e5820c0ded7..d65c75bbf01 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -320,6 +320,10 @@ export class ApiRepository implements Repository { return this.#repository.mergeAbort(); } + rebase(branch: string): Promise { + return this.#repository.rebase(branch); + } + createStash(options?: { message?: string; includeUntracked?: boolean; staged?: boolean }): Promise { return this.#repository.createStash(options?.message, options?.includeUntracked, options?.staged); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 122134c2c8b..84560e038f4 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -315,6 +315,7 @@ export interface Repository { commit(message: string, opts?: CommitOptions): Promise; merge(ref: string): Promise; mergeAbort(): Promise; + rebase(branch: string): Promise; createStash(options?: { message?: string; includeUntracked?: boolean; staged?: boolean }): Promise; applyStash(index?: number): Promise; From c1184bc26164d2ebe0500dac6c1994e3302e5f03 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 4 Mar 2026 13:20:02 +0100 Subject: [PATCH 149/448] Remove holdToSpeech feature for inline chat (#299182) Fixes https://github.com/microsoft/vscode/issues/297811 --- src/vs/sessions/sessions.desktop.main.ts | 1 - .../inlineChat/browser/inlineChatActions.ts | 16 +--- .../contrib/inlineChat/common/inlineChat.ts | 6 -- .../inlineChat.contribution.ts | 11 --- .../electron-browser/inlineChatActions.ts | 83 ------------------- src/vs/workbench/workbench.desktop.main.ts | 2 +- 6 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 src/vs/workbench/contrib/inlineChat/electron-browser/inlineChat.contribution.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 17d622826eb..9ede9e80baa 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -177,7 +177,6 @@ import '../workbench/contrib/remoteTunnel/electron-browser/remoteTunnel.contribu // Chat import '../workbench/contrib/chat/electron-browser/chat.contribution.js'; -//import '../workbench/contrib/inlineChat/electron-browser/inlineChat.contribution.js'; import './contrib/agentFeedback/browser/agentFeedback.contribution.js'; // Encryption diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index d282737cbbc..8829e4bb912 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -16,7 +16,7 @@ import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/br import { localize, localize2 } from '../../../../nls.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 { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; @@ -37,16 +37,6 @@ CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.')); -// some gymnastics to enable hold for speech without moving the StartSessionAction into the electron-layer - -export interface IHoldForSpeech { - (accessor: ServicesAccessor, controller: InlineChatController, source: Action2): void; -} -let _holdForSpeech: IHoldForSpeech | undefined = undefined; -export function setHoldForSpeech(holdForSpeech: IHoldForSpeech) { - _holdForSpeech = holdForSpeech; -} - const inlineChatContextKey = ContextKeyExpr.and( ContextKeyExpr.or(CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_V2_ENABLED), CTX_INLINE_CHAT_POSSIBLE, @@ -114,10 +104,6 @@ export class StartSessionAction extends Action2 { return; } - if (_holdForSpeech) { - accessor.get(IInstantiationService).invokeFunction(_holdForSpeech, ctrl, this); - } - let options: InlineChatRunOptions | undefined; const arg = args[0]; if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 69fd6f6674c..6331855f6a0 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -15,7 +15,6 @@ import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../notebook/common/notebookContext export const enum InlineChatConfigKeys { FinishOnType = 'inlineChat.finishOnType', - HoldToSpeech = 'inlineChat.holdToSpeech', /** @deprecated do not read on client */ EnableV2 = 'inlineChat.enableV2', notebookAgent = 'inlineChat.notebookAgent', @@ -33,11 +32,6 @@ Registry.as(Extensions.Configuration).registerConfigurat default: false, type: 'boolean' }, - [InlineChatConfigKeys.HoldToSpeech]: { - description: localize('holdToSpeech', "Whether holding the inline chat keybinding will automatically enable speech recognition."), - default: true, - type: 'boolean' - }, [InlineChatConfigKeys.EnableV2]: { description: localize('enableV2', "Whether to use the next version of inline chat."), default: false, diff --git a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChat.contribution.ts deleted file mode 100644 index 99549752c8a..00000000000 --- a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChat.contribution.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { HoldToSpeak } from './inlineChatActions.js'; - -// start and hold for voice - -registerAction2(HoldToSpeak); diff --git a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts deleted file mode 100644 index 0a9c91d1859..00000000000 --- a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts +++ /dev/null @@ -1,83 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { InlineChatController } from '../browser/inlineChatController.js'; -import { AbstractInlineChatAction, setHoldForSpeech } from '../browser/inlineChatActions.js'; -import { disposableTimeout } from '../../../../base/common/async.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { StartVoiceChatAction, StopListeningAction, VOICE_KEY_HOLD_THRESHOLD } from '../../chat/electron-browser/actions/voiceChatActions.js'; -import { IChatExecuteActionContext } from '../../chat/browser/actions/chatExecuteActions.js'; -import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { HasSpeechProvider, ISpeechService } from '../../speech/common/speechService.js'; -import { localize2 } from '../../../../nls.js'; -import { Action2 } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js'; - -export class HoldToSpeak extends EditorAction2 { - - constructor() { - super({ - id: 'inlineChat.holdForSpeech', - category: AbstractInlineChatAction.category, - precondition: ContextKeyExpr.and(HasSpeechProvider, CTX_INLINE_CHAT_VISIBLE), - title: localize2('holdForSpeech', "Hold for Speech"), - keybinding: { - when: EditorContextKeys.textInputFocus, - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyI, - }, - }); - } - - override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) { - const ctrl = InlineChatController.get(editor); - if (ctrl) { - holdForSpeech(accessor, ctrl, this); - } - } -} - -function holdForSpeech(accessor: ServicesAccessor, ctrl: InlineChatController, action: Action2): void { - - const configService = accessor.get(IConfigurationService); - const speechService = accessor.get(ISpeechService); - const keybindingService = accessor.get(IKeybindingService); - const commandService = accessor.get(ICommandService); - - // enabled or possible? - if (!configService.getValue(InlineChatConfigKeys.HoldToSpeech || !speechService.hasSpeechProvider)) { - return; - } - - const holdMode = keybindingService.enableKeybindingHoldMode(action.desc.id); - if (!holdMode) { - return; - } - let listening = false; - const handle = disposableTimeout(() => { - // start VOICE input - commandService.executeCommand(StartVoiceChatAction.ID, { voice: { disableTimeout: true } } satisfies IChatExecuteActionContext); - listening = true; - }, VOICE_KEY_HOLD_THRESHOLD); - - holdMode.finally(() => { - if (listening) { - commandService.executeCommand(StopListeningAction.ID).finally(() => { - ctrl.widget.chatWidget.acceptInput(); - }); - } - handle.dispose(); - }); -} - -// make this accessible to the chat actions from the browser layer -setHoldForSpeech(holdForSpeech); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 8291f38e8d1..68821267b86 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -178,7 +178,7 @@ import './contrib/remoteTunnel/electron-browser/remoteTunnel.contribution.js'; // Chat import './contrib/chat/electron-browser/chat.contribution.js'; -import './contrib/inlineChat/electron-browser/inlineChat.contribution.js'; + // Encryption import './contrib/encryption/electron-browser/encryption.contribution.js'; From 856ea291a5701ed17952fa79b17aad2f96f3b283 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:50:35 +0100 Subject: [PATCH 150/448] No need to throw when an element with the same ID comes in (#299154) * No need to throw when an element with the same ID comes in Fixes microsoft/vscode-pull-request-github#8073 * Fix tests --- extensions/vscode-api-tests/package.json | 5 + .../src/singlefolder-tests/tree.test.ts | 124 ++++++++++++++++-- .../workbench/api/common/extHostTreeViews.ts | 11 +- .../api/test/browser/extHostTreeViews.test.ts | 51 ++++++- 4 files changed, 170 insertions(+), 21 deletions(-) diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 58ec85b1450..96ad4d41c5a 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -146,6 +146,11 @@ "id": "test.treeId", "name": "test-tree", "when": "never" + }, + { + "id": "test.treeSwitchUpdate", + "name": "test-tree-switch-update", + "when": "never" } ] }, diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts index a9d9bc5aa34..02259dc98c2 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts @@ -98,10 +98,11 @@ suite('vscode API - tree', () => { await provider.resolveNextRequest(); const [firstResult, secondResult] = await Promise.all([revealFirst, revealSecond]); - const error = firstResult.error ?? secondResult.error; - if (error && /Element with id .+ is already registered/.test(error.message)) { - assert.fail(error.message); - } + // Two concurrent root fetches race: the stale one gets invalidated and + // its reveal fails with "Cannot resolve". The other succeeds. + const errors = [firstResult.error, secondResult.error].filter((e): e is Error => !!e); + assert.strictEqual(errors.length, 1, 'Exactly one reveal should fail from the stale fetch'); + assert.ok(/Cannot resolve tree item/.test(errors[0].message), `Expected "Cannot resolve" error but got: ${errors[0].message}`); }); test('TreeView - element already registered after rapid root refresh', async function () { @@ -206,10 +207,113 @@ suite('vscode API - tree', () => { provider.resolveRequestWithElement(1, provider.getElement2()); const [firstResult, secondResult] = await Promise.all([firstReveal, secondReveal]); - const error = firstResult.error ?? secondResult.error; - if (error && /Element with id .+ is already registered/.test(error.message)) { - assert.fail(error.message); + const errors = [firstResult.error, secondResult.error].filter((e): e is Error => !!e); + assert.strictEqual(errors.length, 1, 'Exactly one reveal should fail from the stale fetch'); + assert.ok(/Cannot resolve tree item/.test(errors[0].message), `Expected "Cannot resolve" error but got: ${errors[0].message}`); + }); + + test('TreeView - element already registered during switch and update', async function () { + this.timeout(60_000); + + // This test reproduces a race condition where the tree is being "switched to" + // (via reveal, which triggers getChildren) while simultaneously the tree data + // is being updated with a new element being added. Both operations trigger + // concurrent getChildren calls. The first resolves with the old set of elements, + // the second resolves with a new set that includes a new element. If both try + // to register elements with the same ID, the error is thrown. + + type TreeElement = { readonly kind: 'leaf'; readonly instance: number }; + + class SwitchAndUpdateTreeDataProvider implements vscode.TreeDataProvider { + private readonly changeEmitter = new vscode.EventEmitter(); + private readonly requestEmitter = new vscode.EventEmitter(); + private readonly pendingRequests: DeferredPromise[] = []; + private readonly existingOld: TreeElement = { kind: 'leaf', instance: 1 }; + private readonly existingNew: TreeElement = { kind: 'leaf', instance: 2 }; + private readonly addedElement: TreeElement = { kind: 'leaf', instance: 3 }; + + readonly onDidChangeTreeData = this.changeEmitter.event; + + getChildren(element?: TreeElement): Thenable { + if (!element) { + const deferred = new DeferredPromise(); + this.pendingRequests.push(deferred); + this.requestEmitter.fire(this.pendingRequests.length); + return deferred.p; + } + return Promise.resolve([]); + } + + getTreeItem(element: TreeElement): vscode.TreeItem { + if (element === this.addedElement) { + const item = new vscode.TreeItem('added', vscode.TreeItemCollapsibleState.None); + item.id = 'added-elem'; + return item; + } + const item = new vscode.TreeItem('existing', vscode.TreeItemCollapsibleState.None); + item.id = 'existing-elem'; + return item; + } + + getParent(): TreeElement | undefined { + return undefined; + } + + async waitForRequestCount(count: number): Promise { + while (this.pendingRequests.length < count) { + await asPromise(this.requestEmitter.event); + } + } + + resolveRequestAt(index: number, elements: TreeElement[]): void { + const request = this.pendingRequests[index]; + if (request) { + request.complete(elements); + } + } + + getExistingOld(): TreeElement { return this.existingOld; } + getExistingNew(): TreeElement { return this.existingNew; } + getAddedElement(): TreeElement { return this.addedElement; } + + dispose(): void { + this.changeEmitter.dispose(); + this.requestEmitter.dispose(); + while (this.pendingRequests.length) { + this.pendingRequests.shift()!.complete([]); + } + } } + + const provider = new SwitchAndUpdateTreeDataProvider(); + disposables.push(provider); + + const treeView = vscode.window.createTreeView('test.treeSwitchUpdate', { treeDataProvider: provider }); + disposables.push(treeView); + + // Two concurrent reveals simulate the tree being "switched to" while also + // being updated: both trigger getChildren calls on the ext host directly. + const revealFirst = (treeView.reveal(provider.getExistingOld(), { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + const revealSecond = (treeView.reveal(provider.getExistingNew(), { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + + // Wait for both getChildren calls to be pending + await provider.waitForRequestCount(2); + + // Resolve first request with old data (just the existing element, old instance) + provider.resolveRequestAt(0, [provider.getExistingOld()]); + await delay(0); + + // Resolve second request with new data: different instance of existing + added element + provider.resolveRequestAt(1, [provider.getExistingNew(), provider.getAddedElement()]); + + const [firstResult, secondResult] = await Promise.all([revealFirst, revealSecond]); + const errors = [firstResult.error, secondResult.error].filter((e): e is Error => !!e); + assert.strictEqual(errors.length, 1, 'Exactly one reveal should fail from the stale fetch'); + assert.ok(/Cannot resolve tree item/.test(errors[0].message), `Expected "Cannot resolve" error but got: ${errors[0].message}`); }); test('TreeView - element already registered after refresh', async function () { @@ -345,9 +449,7 @@ suite('vscode API - tree', () => { await provider.resolveChildRequestAt(0, [staleChild]); const [firstResult, secondResult] = await Promise.all([firstReveal, secondReveal]); - const error = firstResult.error ?? secondResult.error; - if (error && /Element with id .+ is already registered/.test(error.message)) { - assert.fail(error.message); - } + assert.strictEqual(firstResult.error, undefined, `First reveal should not fail: ${firstResult.error?.message}`); + assert.strictEqual(secondResult.error, undefined, `Second reveal should not fail: ${secondResult.error?.message}`); }); }); diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index fce5e9d47db..c0d05cbb486 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../nls.js'; import type * as vscode from 'vscode'; import { basename } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; @@ -876,10 +875,14 @@ class ExtHostTreeView extends Disposable { if (duplicateHandle) { const existingElement = this._elements.get(duplicateHandle); if (existingElement) { - if (existingElement !== element) { - throw new Error(localize('treeView.duplicateElement', 'Element with id {0} is already registered', extTreeItem.id)); - } const existingNode = this._nodes.get(existingElement); + if (existingElement !== element) { + // A different element object was registered with the same ID. + // This can happen during concurrent tree operations (e.g., tree + // being switched to while data is updated). Clean up the stale + // element reference before re-registering with the new one. + this._nodes.delete(existingElement); + } if (existingNode) { const newNode = this._createTreeNode(element, extTreeItem, parentNode); this._updateNodeCache(element, newNode, existingNode, parentNode); diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 751a4ff73f9..2ca97fac8a8 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -204,7 +204,7 @@ suite('ExtHostTreeView', function () { }); }); - test('error is thrown if id is not unique', (done) => { + test('duplicate id across siblings is handled gracefully', (done) => { tree['a'] = { 'aa': {}, }; @@ -212,7 +212,6 @@ suite('ExtHostTreeView', function () { 'aa': {}, 'ba': {} }; - let caughtExpectedError = false; store.add(target.onRefresh.event(() => { testObject.$getChildren('testNodeWithIdTreeProvider') .then(elements => { @@ -220,14 +219,54 @@ suite('ExtHostTreeView', function () { assert.deepStrictEqual(actuals, ['1/a', '1/b']); return testObject.$getChildren('testNodeWithIdTreeProvider', ['1/a']) .then(() => testObject.$getChildren('testNodeWithIdTreeProvider', ['1/b'])) - .then(() => assert.fail('Should fail with duplicate id')) - .catch(() => caughtExpectedError = true) - .finally(() => caughtExpectedError ? done() : assert.fail('Expected duplicate id error not thrown.')); - }); + .then(elements => { + // Children of 'b' should include both 'aa' and 'ba' + const children = unBatchChildren(elements)?.map(e => e.handle); + assert.deepStrictEqual(children, ['1/aa', '1/ba']); + done(); + }); + }).catch(done); })); onDidChangeTreeNode.fire(undefined); }); + test('different element instances with same id are replaced gracefully', async () => { + // Simulates the race condition: two concurrent getChildren calls return + // different element objects that map to the same tree item ID. The second + // call should replace the first's registration without error. + let callCount = 0; + const element1 = { key: 'x' }; + const element2 = { key: 'x' }; + + const treeView = testObject.createTreeView('testRaceProvider', { + treeDataProvider: { + getChildren: (): { key: string }[] => { + callCount++; + // Return a different object instance each time + return callCount === 1 ? [element1] : [element2]; + }, + getTreeItem: (element: { key: string }): TreeItem => { + return { label: { label: element.key }, id: 'same-id', collapsibleState: TreeItemCollapsibleState.None }; + }, + onDidChangeTreeData: onDidChangeTreeNode.event, + } + }, extensionsDescription); + + store.add(treeView); + + // First fetch — registers element1 with id 'same-id' + const first = await testObject.$getChildren('testRaceProvider'); + const firstChildren = unBatchChildren(first); + assert.strictEqual(firstChildren?.length, 1); + assert.strictEqual(firstChildren![0].handle, '1/same-id'); + + // Second fetch — different element instance, same id. Should not throw. + const second = await testObject.$getChildren('testRaceProvider'); + const secondChildren = unBatchChildren(second); + assert.strictEqual(secondChildren?.length, 1); + assert.strictEqual(secondChildren![0].handle, '1/same-id'); + }); + test('refresh root', function (done) { store.add(target.onRefresh.event(actuals => { assert.strictEqual(undefined, actuals); From c0782402b7918cac5fe4d685a5bb2aac4c82419d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:56:02 +0100 Subject: [PATCH 151/448] Sessions - hide the apply change action (#299188) --- .../browser/applyChangesToParentRepo.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts index 60bbbed6769..1c02efa68e6 100644 --- a/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts +++ b/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts @@ -68,8 +68,9 @@ class ApplyChangesToParentRepoAction extends Action2 { group: 'navigation', order: 2, when: ContextKeyExpr.and( + ContextKeyExpr.false(), IsSessionsWindowContext, - hasWorktreeAndRepositoryContextKey, + hasWorktreeAndRepositoryContextKey ), }, ], From c5e1a4bdeac36fd9544a828f097bec6f414502f3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 4 Mar 2026 14:08:00 +0100 Subject: [PATCH 152/448] sessions - disconnect inactive pickers when switching local/cloud target (#299189) * fix: update session handling in NewChatWidget to disconnect inactive pickers * fix: update repoPicker session handling in NewChatWidget --- .../sessions/contrib/chat/browser/newChatViewPane.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index fd5104e8dc5..1a649c0634e 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -344,15 +344,17 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private _setNewSession(session: INewSession): void { this._newSession.value = session; - // Wire pickers to the new session + // Wire pickers to the new session and disconnect inactive ones const target = this._targetPicker.selectedTarget; if (target === AgentSessionProviders.Background) { this._folderPicker.setNewSession(session); this._isolationModePicker.setNewSession(session); this._branchPicker.setNewSession(session); - } - - if (target === AgentSessionProviders.Cloud) { + this._repoPicker.setNewSession(undefined); + } else { + this._folderPicker.setNewSession(undefined); + this._isolationModePicker.setNewSession(undefined); + this._branchPicker.setNewSession(undefined); this._repoPicker.setNewSession(session); } From cf8f3944b5ffc024b3e2e93a3389ce28816916b9 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 13:22:47 +0000 Subject: [PATCH 153/448] update: bump @vscode/codicons version to 0.0.45-12 in package.json and package-lock.json --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03536ba0360..9728ddebc18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-11", + "@vscode/codicons": "^0.0.45-12", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -2983,9 +2983,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-11", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-11.tgz", - "integrity": "sha512-fLjx4i7pfSYJJzzmQ6tZnshWWSLYUfg8Ru6xNRBWRSFj8yZkuuXEZGMxju4mt/tuu8Y/gjhEGmIVmVC16fg+yQ==", + "version": "0.0.45-12", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-12.tgz", + "integrity": "sha512-omdtI6hEzpa901Q1s53ndM2vp3ROIVFFCGdz8I6hl4DZ/eKQzEdGYlY09Lnxfh+r9PfSDoyafChGIMIXmNnsRQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { diff --git a/package.json b/package.json index 2cbaf453e16..91ffe79555c 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-11", + "@vscode/codicons": "^0.0.45-12", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 6767fc211f0..0cd6aee9fb7 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-11", + "@vscode/codicons": "^0.0.45-12", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-11", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-11.tgz", - "integrity": "sha512-fLjx4i7pfSYJJzzmQ6tZnshWWSLYUfg8Ru6xNRBWRSFj8yZkuuXEZGMxju4mt/tuu8Y/gjhEGmIVmVC16fg+yQ==", + "version": "0.0.45-12", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-12.tgz", + "integrity": "sha512-omdtI6hEzpa901Q1s53ndM2vp3ROIVFFCGdz8I6hl4DZ/eKQzEdGYlY09Lnxfh+r9PfSDoyafChGIMIXmNnsRQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index 29fb7392340..a641d6346c0 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-11", + "@vscode/codicons": "^0.0.45-12", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", From 284bd98ce3c1b582db4010624bc04da868cfd138 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Mar 2026 00:38:47 +1100 Subject: [PATCH 154/448] Add support for custom chat agents in the API (#298227) * Add support for custom chat agents in the API - Introduced `chatCustomAgents` proposal in extensions API. - Implemented methods to handle custom agents in `MainThreadChatAgents2`. - Added `ICustomAgentDto` interface and related functionality in extHost. - Created new type definitions for custom agents in `vscode.proposed.chatCustomAgents.d.ts`. * Filter custom agents by visibility before pushing to the proxy * Refactor onDidChangeCustomAgents to use direct event listener * Update custom agent tools property to allow undefined values * Add chatCustomAgents to enabledApiProposals in package.json * update * update * support skills * support instructions * update * update --------- Co-authored-by: Martin Aeschlimann --- extensions/vscode-api-tests/package.json | 1 + .../api/browser/mainThreadChatAgents2.ts | 50 ++++++++++++++++++- .../workbench/api/common/extHost.api.impl.ts | 24 +++++++++ .../workbench/api/common/extHost.protocol.ts | 15 ++++++ .../api/common/extHostChatAgents2.ts | 40 ++++++++++++++- .../promptSyntax/service/promptsService.ts | 10 ++++ .../service/promptsServiceImpl.ts | 10 ++++ .../service/mockPromptsService.ts | 9 ++-- .../vscode.proposed.chatPromptFiles.d.ts | 33 ++++++++++++ 9 files changed, 187 insertions(+), 5 deletions(-) diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 96ad4d41c5a..e5c6ce5f767 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -9,6 +9,7 @@ "authSession", "environmentPower", "chatParticipantPrivate", + "chatPromptFiles", "chatProvider", "contribStatusBarItems", "contribViewsRemote", diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index bf0ba0049b0..9459b611a98 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -40,7 +40,7 @@ import { ILanguageModelToolsService } from '../../contrib/chat/common/tools/lang import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js'; +import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, IInstructionDto, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js'; import { NotebookDto } from './mainThreadNotebookDto.js'; interface AgentData { @@ -153,6 +153,24 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA // Push the initial active session if there is already a focused widget this._acceptActiveChatSession(this._chatWidgetService.lastFocusedWidget); + + // Push custom agents to ext host + void this._pushCustomAgents(); + this._register(this._promptsService.onDidChangeCustomAgents(() => { + void this._pushCustomAgents(); + })); + + // Push instructions to ext host + void this._pushInstructions(); + this._register(this._promptsService.onDidChangeInstructions(() => { + void this._pushInstructions(); + })); + + // Push skills to ext host + void this._pushSkills(); + this._register(this._promptsService.onDidChangeSkills(() => { + void this._pushSkills(); + })); } private _acceptActiveChatSession(widget: IChatWidget | undefined): void { @@ -161,6 +179,36 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._proxy.$acceptActiveChatSession(isLocal ? sessionResource : undefined); } + private async _pushCustomAgents(): Promise { + try { + const customAgents = await this._promptsService.getCustomAgents(CancellationToken.None); + const dtos: ICustomAgentDto[] = customAgents.map(agent => ({ uri: agent.uri })); + this._proxy.$acceptCustomAgents(dtos); + } catch (error) { + this._logService.error('[chat] Failed to push custom agents to extension host', error); + } + } + + private async _pushInstructions(): Promise { + try { + const instructions = await this._promptsService.getInstructionFiles(CancellationToken.None); + const dtos: IInstructionDto[] = instructions.map(instruction => ({ uri: instruction.uri })); + this._proxy.$acceptInstructions(dtos); + } catch (error) { + this._logService.error('[chat] Failed to push instructions to extension host', error); + } + } + + private async _pushSkills(): Promise { + try { + const skills = await this._promptsService.findAgentSkills(CancellationToken.None) ?? []; + const dtos: ISkillDto[] = skills.map(skill => ({ uri: skill.uri })); + this._proxy.$acceptSkills(dtos); + } catch (error) { + this._logService.error('[chat] Failed to push skills to extension host', error); + } + } + $unregisterAgent(handle: number): void { this._agents.deleteAndDispose(handle); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a5dfeb3c1d6..37251c6f672 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1677,6 +1677,30 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatDebug'); return extHostChatDebug.registerChatDebugLogProvider(provider); }, + get customAgents() { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.customAgents as readonly vscode.ChatResource[]; + }, + onDidChangeCustomAgents: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.onDidChangeCustomAgents(listener, thisArgs, disposables); + }, + get instructions() { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.instructions as readonly vscode.ChatResource[]; + }, + onDidChangeInstructions: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.onDidChangeInstructions(listener, thisArgs, disposables); + }, + get skills() { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.skills as readonly vscode.ChatResource[]; + }, + onDidChangeSkills: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.onDidChangeSkills(listener, thisArgs, disposables); + }, }; // namespace: lm diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0f5821f2662..0bd60212242 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1614,6 +1614,21 @@ export interface ExtHostChatAgentsShape2 { $setRequestTools(requestId: string, tools: UserSelectedTools): void; $setYieldRequested(requestId: string, value: boolean): void; $acceptActiveChatSession(sessionResource: UriComponents | undefined): void; + $acceptCustomAgents(agents: ICustomAgentDto[]): void; + $acceptInstructions(instructions: IInstructionDto[]): void; + $acceptSkills(skills: ISkillDto[]): void; +} + +export interface ICustomAgentDto { + uri: UriComponents; +} + +export interface IInstructionDto { + uri: UriComponents; +} + +export interface ISkillDto { + uri: UriComponents; } export interface IChatParticipantMetadata { participant: string; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 7c5acd5cf88..5edee47784f 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -27,7 +27,7 @@ import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js' import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IChatSessionContextDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js'; +import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IExtensionChatAgentMetadata, IInstructionDto, IMainContext, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js'; import { CommandsConverter, ExtHostCommands } from './extHostCommands.js'; import { ExtHostDiagnostics } from './extHostDiagnostics.js'; import { ExtHostDocuments } from './extHostDocuments.js'; @@ -487,6 +487,17 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _onDidDisposeChatSession = this._register(new Emitter()); readonly onDidDisposeChatSession = this._onDidDisposeChatSession.event; + private readonly _onDidChangeCustomAgents = this._register(new Emitter()); + readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; + private readonly _onDidChangeInstructions = this._register(new Emitter()); + readonly onDidChangeInstructions = this._onDidChangeInstructions.event; + private readonly _onDidChangeSkills = this._register(new Emitter()); + readonly onDidChangeSkills = this._onDidChangeSkills.event; + + private _customAgents: vscode.ChatResource[] = []; + private _instructions: vscode.ChatResource[] = []; + private _skills: vscode.ChatResource[] = []; + private _activeChatPanelSessionResource: URI | undefined; private readonly _onDidChangeActiveChatPanelSessionResource = this._register(new Emitter()); @@ -496,6 +507,33 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return this._activeChatPanelSessionResource; } + get customAgents(): readonly vscode.ChatResource[] { + return this._customAgents; + } + + get instructions(): readonly vscode.ChatResource[] { + return this._instructions; + } + + get skills(): readonly vscode.ChatResource[] { + return this._skills; + } + + $acceptCustomAgents(agents: ICustomAgentDto[]): void { + this._customAgents = agents.map(a => Object.freeze({ uri: URI.revive(a.uri) })); + this._onDidChangeCustomAgents.fire(); + } + + $acceptInstructions(instructions: IInstructionDto[]): void { + this._instructions = instructions.map(i => Object.freeze({ uri: URI.revive(i.uri) })); + this._onDidChangeInstructions.fire(); + } + + $acceptSkills(skills: ISkillDto[]): void { + this._skills = skills.map(s => Object.freeze({ uri: URI.revive(s.uri) })); + this._onDidChangeSkills.fire(); + } + constructor( mainContext: IMainContext, private readonly _logService: ILogService, 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 66d01e1d7af..b4de6bbc2e4 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -417,6 +417,11 @@ export interface IPromptsService extends IDisposable { */ readonly onDidChangeCustomAgents: Event; + /** + * Event that is triggered when the list of instruction files changes. + */ + readonly onDidChangeInstructions: Event; + /** * Finds all available custom agents * @param sessionResource Optional session resource to scope debug logging to a specific session. @@ -483,6 +488,11 @@ export interface IPromptsService extends IDisposable { */ findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise; + /** + * Event that is triggered when the list of skills changes. + */ + readonly onDidChangeSkills: Event; + /** * Gets detailed discovery information for a prompt type. * This includes all files found and their load/skip status with reasons. 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 75acb1fe94b..c628f10fcf7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -152,6 +152,7 @@ export class PromptsService extends Disposable implements IPromptsService { private readonly _contributedWhenKeys = new Set(); private readonly _contributedWhenClauses = new Map(); private readonly _onDidContributedWhenChange = this._register(new Emitter()); + private readonly _onDidChangeInstructions = this._register(new Emitter()); private readonly _onDidPluginPromptFilesChange = this._register(new Emitter()); private readonly _onDidPluginHooksChange = this._register(new Emitter()); private _pluginPromptFilesByType = new Map(); @@ -417,6 +418,7 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedCustomAgents.refresh(); } else if (type === PromptsType.instructions) { this.cachedFileLocations[PromptsType.instructions] = undefined; + this._onDidChangeInstructions.fire(); } else if (type === PromptsType.prompt) { this.cachedFileLocations[PromptsType.prompt] = undefined; this.cachedSlashCommands.refresh(); @@ -644,6 +646,14 @@ export class PromptsService extends Disposable implements IPromptsService { return this.cachedCustomAgents.onDidChange; } + public get onDidChangeInstructions(): Event { + return Event.any( + this.getFileLocatorEvent(PromptsType.instructions), + this._onDidContributedWhenChange.event, + this._onDidChangeInstructions.event, + ); + } + public async getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise { const sw = StopWatch.create(); const result = await this.cachedCustomAgents.get(token); 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 e01bc0089f0..131aafd3982 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 @@ -18,8 +18,8 @@ export class MockPromptsService implements IPromptsService { _serviceBrand: undefined; - private readonly _onDidChangeCustomChatModes = new Emitter(); - readonly onDidChangeCustomAgents = this._onDidChangeCustomChatModes.event; + private readonly _onDidChangeCustomAgents = new Emitter(); + readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; private readonly _onDidLogDiscovery = new Emitter(); readonly onDidLogDiscovery: Event = this._onDidLogDiscovery.event; @@ -28,7 +28,7 @@ export class MockPromptsService implements IPromptsService { setCustomModes(modes: ICustomAgent[]): void { this._customModes = modes; - this._onDidChangeCustomChatModes.fire(); + this._onDidChangeCustomAgents.fire(); } async getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise { @@ -72,4 +72,7 @@ export class MockPromptsService implements IPromptsService { getHooks(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } getInstructionFiles(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } dispose(): void { } + onDidChangeInstructions: Event = Event.None; + onDidChangePromptFiles: Event = Event.None; + onDidChangeSkills: Event = Event.None; } diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index e683d6ce600..898b0cfbe27 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -103,6 +103,39 @@ declare module 'vscode' { // #region Chat Provider Registration export namespace chat { + /** + * An event that fires when the list of {@link customAgents custom agents} changes. + */ + export const onDidChangeCustomAgents: Event; + + /** + * The list of currently available custom agents. These are `.agent.md` files + * from all sources (workspace, user, and extension-provided). + */ + export const customAgents: readonly ChatResource[]; + + /** + * An event that fires when the list of {@link instructions instructions} changes. + */ + export const onDidChangeInstructions: Event; + + /** + * The list of currently available instructions. These are `.instructions.md` files + * from all sources (workspace, user, and extension-provided). + */ + export const instructions: readonly ChatResource[]; + + /** + * An event that fires when the list of {@link skills skills} changes. + */ + export const onDidChangeSkills: Event; + + /** + * The list of currently available skills. These are `SKILL.md` files + * from all sources (workspace, user, and extension-provided). + */ + export const skills: readonly ChatResource[]; + /** * Register a provider for custom agents. * @param provider The custom agent provider. From 7d47fd19043b9c168c5b7560c95417587d14356c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:57:15 +0100 Subject: [PATCH 155/448] Sessions - enable branch name generation (#299202) --- .../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 843068b74d6..0effc9b662a 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -27,6 +27,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'files.autoSave': 'afterDelay', 'git.autofetch': true, + 'git.branchRandomName.enable': true, 'git.detectWorktrees': false, 'git.showProgress': false, From 8a03516dd47f01b0fe0124ef9054cca31b743839 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 16:04:28 +0100 Subject: [PATCH 156/448] sessions - focus chat input after attaching context (#299203) --- src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 1a649c0634e..8f78d2590da 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -245,7 +245,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { })); // Update input state when attachments or model change - this._register(this._contextAttachments.onDidChangeContext(() => this._updateDraftState())); + this._register(this._contextAttachments.onDidChangeContext(() => { + this._updateDraftState(); + this._focusEditor(); + })); this._register(autorun(reader => { this._currentLanguageModel.read(reader); this._updateDraftState(); From 75a2f31cdad3579a757044b31b1a3ad7644528bb Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 16:17:21 +0100 Subject: [PATCH 157/448] components for AI Customization shortcuts widget --- .../browser/aiCustomizationShortcutsWidget.ts | 137 +++++++++ .../customizationsToolbar.contribution.ts | 8 +- .../browser/media/customizationsToolbar.css | 253 ++++++++--------- .../sessions/browser/sessionsViewPane.ts | 121 +------- .../aiCustomizationShortcutsWidget.fixture.ts | 267 ++++++++++++++++++ 5 files changed, 542 insertions(+), 244 deletions(-) create mode 100644 src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts create mode 100644 src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts diff --git a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts new file mode 100644 index 00000000000..c4e89d70e17 --- /dev/null +++ b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import '../../../browser/media/sidebarActionButton.css'; +import './media/customizationsToolbar.css'; +import * as DOM from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { Menus } from '../../../browser/menus.js'; +import { getCustomizationTotalCount } from './customizationCounts.js'; + +const $ = DOM.$; + +const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed'; + +export interface IAICustomizationShortcutsWidgetOptions { + readonly onDidToggleCollapse?: () => void; +} + +export class AICustomizationShortcutsWidget extends Disposable { + + constructor( + container: HTMLElement, + options: IAICustomizationShortcutsWidgetOptions | undefined, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageService private readonly storageService: IStorageService, + @IPromptsService private readonly promptsService: IPromptsService, + @IMcpService private readonly mcpService: IMcpService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, + ) { + super(); + + this._render(container, options); + } + + private _render(parent: HTMLElement, options: IAICustomizationShortcutsWidgetOptions | undefined): void { + // Get initial collapsed state + const isCollapsed = this.storageService.getBoolean(CUSTOMIZATIONS_COLLAPSED_KEY, StorageScope.PROFILE, false); + + const container = DOM.append(parent, $('.ai-customization-toolbar')); + if (isCollapsed) { + container.classList.add('collapsed'); + } + + // Header (clickable to toggle) + const header = DOM.append(container, $('.ai-customization-header')); + header.classList.toggle('collapsed', isCollapsed); + + const headerButtonContainer = DOM.append(header, $('.customization-link-button-container')); + const headerButton = this._register(new Button(headerButtonContainer, { + ...defaultButtonStyles, + secondary: true, + title: false, + supportIcons: true, + buttonSecondaryBackground: 'transparent', + buttonSecondaryHoverBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryBorder: undefined, + })); + headerButton.element.classList.add('customization-link-button', 'sidebar-action-button'); + headerButton.element.setAttribute('aria-expanded', String(!isCollapsed)); + headerButton.label = localize('customizations', "CUSTOMIZATIONS"); + + const chevronContainer = DOM.append(headerButton.element, $('span.customization-link-counts')); + const chevron = DOM.append(chevronContainer, $('.ai-customization-chevron')); + const headerTotalCount = DOM.append(chevronContainer, $('span.ai-customization-header-total.hidden')); + chevron.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown)); + + // Toolbar container + const toolbarContainer = DOM.append(container, $('.ai-customization-toolbar-content.sidebar-action-list')); + + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, Menus.SidebarCustomizations, { + hiddenItemStrategy: HiddenItemStrategy.NoHide, + toolbarOptions: { primaryGroup: () => true }, + telemetrySource: 'sidebarCustomizations', + })); + + let updateCountRequestId = 0; + const updateHeaderTotalCount = async () => { + const requestId = ++updateCountRequestId; + const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService); + if (requestId !== updateCountRequestId) { + return; + } + + headerTotalCount.classList.toggle('hidden', totalCount === 0); + headerTotalCount.textContent = `${totalCount}`; + }; + + this._register(this.promptsService.onDidChangeCustomAgents(() => updateHeaderTotalCount())); + this._register(this.promptsService.onDidChangeSlashCommands(() => updateHeaderTotalCount())); + this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => updateHeaderTotalCount())); + this._register(autorun(reader => { + this.mcpService.servers.read(reader); + updateHeaderTotalCount(); + })); + this._register(autorun(reader => { + this.workspaceService.activeProjectRoot.read(reader); + updateHeaderTotalCount(); + })); + updateHeaderTotalCount(); + + // Toggle collapse on header click + const transitionListener = this._register(new MutableDisposable()); + const toggleCollapse = () => { + const collapsed = container.classList.toggle('collapsed'); + header.classList.toggle('collapsed', collapsed); + this.storageService.store(CUSTOMIZATIONS_COLLAPSED_KEY, collapsed, StorageScope.PROFILE, StorageTarget.USER); + headerButton.element.setAttribute('aria-expanded', String(!collapsed)); + chevron.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chevronRight), ...ThemeIcon.asClassNameArray(Codicon.chevronDown)); + chevron.classList.add(...ThemeIcon.asClassNameArray(collapsed ? Codicon.chevronRight : Codicon.chevronDown)); + + // Re-layout after the transition + transitionListener.value = DOM.addDisposableListener(toolbarContainer, 'transitionend', () => { + transitionListener.clear(); + options?.onDidToggleCollapse?.(); + }); + }; + + this._register(headerButton.onDidClick(() => toggleCollapse())); + } +} diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index afbbd572247..ba97537a9b8 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -34,7 +34,7 @@ import { getSourceCounts, getSourceCountsTotal } from './customizationCounts.js' import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; -interface ICustomizationItemConfig { +export interface ICustomizationItemConfig { readonly id: string; readonly label: string; readonly icon: ThemeIcon; @@ -43,7 +43,7 @@ interface ICustomizationItemConfig { readonly isMcp?: boolean; } -const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ +export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ { id: 'sessions.customization.agents', label: localize('agents', "Agents"), @@ -92,7 +92,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ * Custom ActionViewItem for each customization link in the toolbar. * Renders icon + label + source count badges, matching the sidebar footer style. */ -class CustomizationLinkViewItem extends ActionViewItem { +export class CustomizationLinkViewItem extends ActionViewItem { private readonly _viewItemDisposables: DisposableStore; private _button: Button | undefined; @@ -199,7 +199,7 @@ class CustomizationLinkViewItem extends ActionViewItem { // --- Register actions and view items --- // -class CustomizationsToolbarContribution extends Disposable implements IWorkbenchContribution { +export class CustomizationsToolbarContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.sessionsCustomizationsToolbar'; diff --git a/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css b/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css index d671775dbd5..bc08fc25eb5 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css +++ b/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css @@ -2,132 +2,129 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -.agent-sessions-viewpane { - - /* AI Customization section - pinned to bottom */ - .ai-customization-toolbar { - display: flex; - flex-direction: column; - flex-shrink: 0; - border-top: 1px solid var(--vscode-widget-border); - padding: 6px; - } - - /* Make the toolbar, action bar, and items fill full width and stack vertically */ - .ai-customization-toolbar .ai-customization-toolbar-content .monaco-toolbar, - .ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar { - width: 100%; - } - - .ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar .actions-container { - display: flex; - flex-direction: column; - width: 100%; - } - - .ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar .action-item { - width: 100%; - max-width: 100%; - } - - .ai-customization-toolbar .customization-link-widget { - width: 100%; - } - - /* Customization header - clickable for collapse */ - .ai-customization-toolbar .ai-customization-header { - display: flex; - align-items: center; - -webkit-user-select: none; - user-select: none; - } - - .ai-customization-toolbar .ai-customization-header:not(.collapsed) { - margin-bottom: 4px; - } - - .ai-customization-toolbar .ai-customization-chevron { - flex-shrink: 0; - opacity: 0; - } - - .ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:hover .ai-customization-chevron, - .ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:focus-within .ai-customization-chevron, - .ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-chevron, - .ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-chevron { - opacity: 0.7; - } - - .ai-customization-toolbar .ai-customization-header-total { - display: none; - opacity: 0.7; - font-size: 11px; - line-height: 1; - } - - .ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:not(:hover):not(:focus-within) .ai-customization-header-total:not(.hidden) { - display: inline; - } - - .ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-header-total, - .ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-header-total, - .ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button .ai-customization-header-total { - display: none; - } - - /* Button container - fills available space */ - .ai-customization-toolbar .customization-link-button-container { - overflow: hidden; - min-width: 0; - flex: 1; - } - - /* Button needs relative positioning for counts overlay */ - .ai-customization-toolbar .customization-link-button { - position: relative; - } - - /* Counts - floating right inside the button */ - .ai-customization-toolbar .customization-link-counts { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - display: flex; - align-items: center; - gap: 6px; - } - - .ai-customization-toolbar .customization-link-counts.hidden { - display: none; - } - - .ai-customization-toolbar .source-count-badge { - display: flex; - align-items: center; - gap: 2px; - } - - .ai-customization-toolbar .source-count-icon { - font-size: 12px; - opacity: 0.6; - } - - .ai-customization-toolbar .source-count-num { - font-size: 11px; - color: var(--vscode-descriptionForeground); - opacity: 0.8; - } - - /* Collapsed state */ - .ai-customization-toolbar .ai-customization-toolbar-content { - max-height: 500px; - overflow: hidden; - transition: max-height 0.2s ease-out; - } - - .ai-customization-toolbar.collapsed .ai-customization-toolbar-content { - max-height: 0; - } +/* AI Customization section - pinned to bottom */ +.ai-customization-toolbar { + display: flex; + flex-direction: column; + flex-shrink: 0; + border-top: 1px solid var(--vscode-widget-border); + padding: 6px; +} + +/* Make the toolbar, action bar, and items fill full width and stack vertically */ +.ai-customization-toolbar .ai-customization-toolbar-content .monaco-toolbar, +.ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar { + width: 100%; +} + +.ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar .actions-container { + display: flex; + flex-direction: column; + width: 100%; +} + +.ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar .action-item { + width: 100%; + max-width: 100%; +} + +.ai-customization-toolbar .customization-link-widget { + width: 100%; +} + +/* Customization header - clickable for collapse */ +.ai-customization-toolbar .ai-customization-header { + display: flex; + align-items: center; + -webkit-user-select: none; + user-select: none; +} + +.ai-customization-toolbar .ai-customization-header:not(.collapsed) { + margin-bottom: 4px; +} + +.ai-customization-toolbar .ai-customization-chevron { + flex-shrink: 0; + opacity: 0; +} + +.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:hover .ai-customization-chevron, +.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:focus-within .ai-customization-chevron, +.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-chevron, +.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-chevron { + opacity: 0.7; +} + +.ai-customization-toolbar .ai-customization-header-total { + display: none; + opacity: 0.7; + font-size: 11px; + line-height: 1; +} + +.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:not(:hover):not(:focus-within) .ai-customization-header-total:not(.hidden) { + display: inline; +} + +.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-header-total, +.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-header-total, +.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button .ai-customization-header-total { + display: none; +} + +/* Button container - fills available space */ +.ai-customization-toolbar .customization-link-button-container { + overflow: hidden; + min-width: 0; + flex: 1; +} + +/* Button needs relative positioning for counts overlay */ +.ai-customization-toolbar .customization-link-button { + position: relative; +} + +/* Counts - floating right inside the button */ +.ai-customization-toolbar .customization-link-counts { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + gap: 6px; +} + +.ai-customization-toolbar .customization-link-counts.hidden { + display: none; +} + +.ai-customization-toolbar .source-count-badge { + display: flex; + align-items: center; + gap: 2px; +} + +.ai-customization-toolbar .source-count-icon { + font-size: 12px; + opacity: 0.6; +} + +.ai-customization-toolbar .source-count-num { + font-size: 11px; + color: var(--vscode-descriptionForeground); + opacity: 0.8; +} + +/* Collapsed state */ +.ai-customization-toolbar .ai-customization-toolbar-content { + max-height: 500px; + overflow: hidden; + transition: max-height 0.2s ease-out; + padding-bottom: 2px; +} + +.ai-customization-toolbar.collapsed .ai-customization-toolbar-content { + max-height: 0; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 41865aac2fb..fbec4187720 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -3,15 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import '../../../browser/media/sidebarActionButton.css'; -import './media/customizationsToolbar.css'; import './media/sessionsViewPane.css'; import * as DOM from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { MutableDisposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorsVisibleContext } from '../../../../workbench/common/contextkeys.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -29,9 +25,6 @@ import { localize, localize2 } from '../../../../nls.js'; import { AgentSessionsControl } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsControl.js'; import { AgentSessionsFilter, AgentSessionsGrouping } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; -import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; -import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { ISessionsManagementService, IsNewChatSessionContext } from './sessionsManagementService.js'; import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -40,26 +33,19 @@ import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ACTION_ID_NEW_CHAT } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { Menus } from '../../../browser/menus.js'; -import { getCustomizationTotalCount } from './customizationCounts.js'; +import { AICustomizationShortcutsWidget } from './aiCustomizationShortcutsWidget.js'; import { IHostService } from '../../../../workbench/services/host/browser/host.js'; const $ = DOM.$; export const SessionsViewId = 'agentic.workbench.view.sessionsView'; const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu'); -const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed'; - export class AgenticSessionsViewPane extends ViewPane { private viewPaneContainer: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; sessionsControl: AgentSessionsControl | undefined; - private aiCustomizationContainer: HTMLElement | undefined; constructor( options: IViewPaneOptions, @@ -73,13 +59,8 @@ export class AgenticSessionsViewPane extends ViewPane { @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IStorageService private readonly storageService: IStorageService, - @IPromptsService private readonly promptsService: IPromptsService, - @IMcpService private readonly mcpService: IMcpService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IHostService private readonly hostService: IHostService, - @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); } @@ -180,8 +161,14 @@ export class AgenticSessionsViewPane extends ViewPane { })); // AI Customization toolbar (bottom, fixed height) - this.aiCustomizationContainer = DOM.append(sessionsContainer, $('div')); - this.createAICustomizationShortcuts(this.aiCustomizationContainer); + this._register(this.instantiationService.createInstance(AICustomizationShortcutsWidget, sessionsContainer, { + onDidToggleCollapse: () => { + if (this.viewPaneContainer) { + const { offsetHeight, offsetWidth } = this.viewPaneContainer; + this.layoutBody(offsetHeight, offsetWidth); + } + }, + })); } private restoreLastSelectedSession(): void { @@ -191,96 +178,6 @@ export class AgenticSessionsViewPane extends ViewPane { } } - private createAICustomizationShortcuts(container: HTMLElement): void { - // Get initial collapsed state - const isCollapsed = this.storageService.getBoolean(CUSTOMIZATIONS_COLLAPSED_KEY, StorageScope.PROFILE, false); - - container.classList.add('ai-customization-toolbar'); - if (isCollapsed) { - container.classList.add('collapsed'); - } - - // Header (clickable to toggle) - const header = DOM.append(container, $('.ai-customization-header')); - header.classList.toggle('collapsed', isCollapsed); - - const headerButtonContainer = DOM.append(header, $('.customization-link-button-container')); - const headerButton = this._register(new Button(headerButtonContainer, { - ...defaultButtonStyles, - secondary: true, - title: false, - supportIcons: true, - buttonSecondaryBackground: 'transparent', - buttonSecondaryHoverBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryBorder: undefined, - })); - headerButton.element.classList.add('customization-link-button', 'sidebar-action-button'); - headerButton.element.setAttribute('aria-expanded', String(!isCollapsed)); - headerButton.label = localize('customizations', "CUSTOMIZATIONS"); - - const chevronContainer = DOM.append(headerButton.element, $('span.customization-link-counts')); - const chevron = DOM.append(chevronContainer, $('.ai-customization-chevron')); - const headerTotalCount = DOM.append(chevronContainer, $('span.ai-customization-header-total.hidden')); - chevron.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown)); - - // Toolbar container - const toolbarContainer = DOM.append(container, $('.ai-customization-toolbar-content.sidebar-action-list')); - - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, Menus.SidebarCustomizations, { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { primaryGroup: () => true }, - telemetrySource: 'sidebarCustomizations', - })); - - let updateCountRequestId = 0; - const updateHeaderTotalCount = async () => { - const requestId = ++updateCountRequestId; - const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService); - if (requestId !== updateCountRequestId) { - return; - } - - headerTotalCount.classList.toggle('hidden', totalCount === 0); - headerTotalCount.textContent = `${totalCount}`; - }; - - this._register(this.promptsService.onDidChangeCustomAgents(() => updateHeaderTotalCount())); - this._register(this.promptsService.onDidChangeSlashCommands(() => updateHeaderTotalCount())); - this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => updateHeaderTotalCount())); - this._register(autorun(reader => { - this.mcpService.servers.read(reader); - updateHeaderTotalCount(); - })); - this._register(autorun(reader => { - this.workspaceService.activeProjectRoot.read(reader); - updateHeaderTotalCount(); - })); - updateHeaderTotalCount(); - - // Toggle collapse on header click - const transitionListener = this._register(new MutableDisposable()); - const toggleCollapse = () => { - const collapsed = container.classList.toggle('collapsed'); - header.classList.toggle('collapsed', collapsed); - this.storageService.store(CUSTOMIZATIONS_COLLAPSED_KEY, collapsed, StorageScope.PROFILE, StorageTarget.USER); - headerButton.element.setAttribute('aria-expanded', String(!collapsed)); - chevron.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chevronRight), ...ThemeIcon.asClassNameArray(Codicon.chevronDown)); - chevron.classList.add(...ThemeIcon.asClassNameArray(collapsed ? Codicon.chevronRight : Codicon.chevronDown)); - - // Re-layout after the transition so sessions control gets the right height - transitionListener.value = DOM.addDisposableListener(toolbarContainer, 'transitionend', () => { - transitionListener.clear(); - if (this.viewPaneContainer) { - const { offsetHeight, offsetWidth } = this.viewPaneContainer; - this.layoutBody(offsetHeight, offsetWidth); - } - }); - }; - - this._register(headerButton.onDidClick(() => toggleCollapse())); - } - private getSessionHoverPosition(): HoverPosition { const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); const sideBarPosition = this.layoutService.getSideBarPosition(); diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts new file mode 100644 index 00000000000..71d5b630d11 --- /dev/null +++ b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { toAction } from '../../../../../base/common/actions.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { IActionViewItemFactory, IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; +import { IMenu, IMenuActionOptions, IMenuService, isIMenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; +import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IWorkspace, IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IPromptsService, PromptsStorage } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; +import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js'; +import { IMcpServer, IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js'; +import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; +import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem } from '../../browser/customizationsToolbar.contribution.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../browser/sessionsManagementService.js'; +import { Menus } from '../../../../browser/menus.js'; + +// Ensure color registrations are loaded +import '../../../../common/theme.js'; +import '../../../../../platform/theme/common/colors/inputColors.js'; + +// ============================================================================ +// One-time menu item registration (module-level). +// MenuRegistry.appendMenuItem does not throw on duplicates, unlike registerAction2 +// which registers global commands and throws on the second call. +// ============================================================================ + +const menuRegistrations = new DisposableStore(); +for (const [index, config] of CUSTOMIZATION_ITEMS.entries()) { + menuRegistrations.add(MenuRegistry.appendMenuItem(Menus.SidebarCustomizations, { + command: { id: config.id, title: config.label }, + group: 'navigation', + order: index + 1, + })); +} + +// ============================================================================ +// FixtureMenuService — reads from MenuRegistry without context-key filtering +// (MockContextKeyService.contextMatchesRules always returns false, which hides +// every item when using the real MenuService.) +// ============================================================================ + +class FixtureMenuService implements IMenuService { + declare readonly _serviceBrand: undefined; + + createMenu(id: MenuId): IMenu { + return { + onDidChange: Event.None, + dispose: () => { }, + getActions: () => { + const items = MenuRegistry.getMenuItems(id).filter(isIMenuItem); + items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + const actions = items.map(item => { + const title = typeof item.command.title === 'string' ? item.command.title : item.command.title.value; + return toAction({ id: item.command.id, label: title, run: () => { } }); + }); + return actions.length ? [['navigation', actions as unknown as (MenuItemAction | SubmenuItemAction)[]]] : []; + }, + }; + } + + getMenuActions(_id: MenuId, _contextKeyService: unknown, _options?: IMenuActionOptions) { return []; } + getMenuContexts() { return new Set(); } + resetHiddenStates() { } +} + +// ============================================================================ +// Minimal IActionViewItemService that supports register/lookUp +// ============================================================================ + +class FixtureActionViewItemService implements IActionViewItemService { + declare _serviceBrand: undefined; + + private readonly _providers = new Map(); + private readonly _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + register(menu: MenuId, commandId: string | MenuId, provider: IActionViewItemFactory): { dispose(): void } { + const key = `${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`; + this._providers.set(key, provider); + return { dispose: () => { this._providers.delete(key); } }; + } + + lookUp(menu: MenuId, commandId: string | MenuId): IActionViewItemFactory | undefined { + const key = `${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`; + return this._providers.get(key); + } +} + +// ============================================================================ +// Mock helpers +// ============================================================================ + +const defaultFilter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension], +}; + +function createMockPromptsService(): IPromptsService { + return createMockPromptsServiceWithCounts(); +} + +interface ICustomizationCounts { + readonly agents?: number; + readonly skills?: number; + readonly instructions?: number; + readonly prompts?: number; + readonly hooks?: number; +} + +function createMockPromptsServiceWithCounts(counts?: ICustomizationCounts): IPromptsService { + const fakeUri = (prefix: string, i: number) => URI.parse(`file:///mock/${prefix}-${i}.md`); + const fakeItem = (prefix: string, i: number) => ({ uri: fakeUri(prefix, i), storage: PromptsStorage.local }); + + const agents = Array.from({ length: counts?.agents ?? 0 }, (_, i) => ({ + uri: fakeUri('agent', i), + source: { storage: PromptsStorage.local }, + })); + const skills = Array.from({ length: counts?.skills ?? 0 }, (_, i) => fakeItem('skill', i)); + const prompts = Array.from({ length: counts?.prompts ?? 0 }, (_, i) => ({ + promptPath: { uri: fakeUri('prompt', i), storage: PromptsStorage.local, type: PromptsType.prompt }, + })); + const instructions = Array.from({ length: counts?.instructions ?? 0 }, (_, i) => fakeItem('instructions', i)); + const hooks = Array.from({ length: counts?.hooks ?? 0 }, (_, i) => fakeItem('hook', i)); + + return new class extends mock() { + override readonly onDidChangeCustomAgents = Event.None; + override readonly onDidChangeSlashCommands = Event.None; + override async getCustomAgents() { return agents as never[]; } + override async findAgentSkills() { return skills as never[]; } + override async getPromptSlashCommands() { return prompts as never[]; } + override async listPromptFiles(type: PromptsType) { + return (type === PromptsType.hook ? hooks : instructions) as never[]; + } + override async listAgentInstructions() { return [] as never[]; } + }(); +} + +function createMockMcpService(serverCount: number = 0): IMcpService { + const MockServer = mock(); + const servers = observableValue('mockMcpServers', Array.from({ length: serverCount }, () => new MockServer())); + return new class extends mock() { + override readonly servers = servers; + }(); +} + +function createMockWorkspaceService(): IAICustomizationWorkspaceService { + const activeProjectRoot = observableValue('mockActiveProjectRoot', undefined); + return new class extends mock() { + override readonly activeProjectRoot = activeProjectRoot; + override getActiveProjectRoot() { return undefined; } + override getStorageSourceFilter() { return defaultFilter; } + }(); +} + +function createMockWorkspaceContextService(): IWorkspaceContextService { + return new class extends mock() { + override readonly onDidChangeWorkspaceFolders = Event.None; + override getWorkspace(): IWorkspace { return { id: 'test', folders: [] }; } + }(); +} + +// ============================================================================ +// Render helper +// ============================================================================ + +function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: number; collapsed?: boolean; counts?: ICustomizationCounts }): void { + ctx.container.style.width = '300px'; + ctx.container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + + const actionViewItemService = new FixtureActionViewItemService(); + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + // Register overrides BEFORE registerWorkbenchServices so they take priority + reg.defineInstance(IMenuService, new FixtureMenuService()); + reg.defineInstance(IActionViewItemService, actionViewItemService); + registerWorkbenchServices(reg); + // Services needed by AICustomizationShortcutsWidget + reg.defineInstance(IPromptsService, options?.counts ? createMockPromptsServiceWithCounts(options.counts) : createMockPromptsService()); + reg.defineInstance(IMcpService, createMockMcpService(options?.mcpServerCount ?? 0)); + reg.defineInstance(IAICustomizationWorkspaceService, createMockWorkspaceService()); + reg.defineInstance(IWorkspaceContextService, createMockWorkspaceContextService()); + // Additional services needed by CustomizationLinkViewItem + reg.defineInstance(ILanguageModelsService, new class extends mock() { + override readonly onDidChangeLanguageModels = Event.None; + }()); + reg.defineInstance(ISessionsManagementService, new class extends mock() { + override readonly activeSession = observableValue('activeSession', undefined); + }()); + reg.defineInstance(IFileService, new class extends mock() { + override readonly onDidFilesChange = Event.None; + }()); + }, + }); + + // Register view item factories from the real CustomizationLinkViewItem (per-render, instance-scoped) + for (const config of CUSTOMIZATION_ITEMS) { + ctx.disposableStore.add(actionViewItemService.register(Menus.SidebarCustomizations, config.id, (action, options) => { + return instantiationService.createInstance(CustomizationLinkViewItem, action, options, config); + })); + } + + // Override storage to set initial collapsed state + if (options?.collapsed) { + const storageService = instantiationService.get(IStorageService); + instantiationService.set(IStorageService, new class extends mock() { + override getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean) { + if (key === 'agentSessions.customizationsCollapsed') { + return true; + } + return storageService.getBoolean(key, scope, fallbackValue!); + } + override store() { } + }()); + } + + // Create the widget (uses FixtureMenuService → reads MenuRegistry items registered above) + ctx.disposableStore.add( + instantiationService.createInstance(AICustomizationShortcutsWidget, ctx.container, undefined) + ); +} + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ path: 'sessions/' }, { + + Expanded: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderWidget(ctx), + }), + + Collapsed: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderWidget(ctx, { collapsed: true }), + }), + + WithMcpServers: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderWidget(ctx, { mcpServerCount: 3 }), + }), + + CollapsedWithMcpServers: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderWidget(ctx, { mcpServerCount: 3, collapsed: true }), + }), + + WithCounts: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderWidget(ctx, { + mcpServerCount: 2, + counts: { agents: 2, skills: 30, instructions: 16, prompts: 17, hooks: 4 }, + }), + }), +}); From a4e35e0d6992b16f5eaac751457641cd9bc4099b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 07:20:21 -0800 Subject: [PATCH 158/448] chat: add support for agent plugin sources (#299081) * chat: add support for agent plugin sources - Adds support for agent plugins to reference sources as specified in PLUGIN_SOURCES.md, enabling installation from GitHub, npm, pip, and other package registries - Integrates source parsing and validation into the plugin installation service and repository service - Adds comprehensive test coverage for plugin source handling and installation from various sources - Creates PLUGIN_SOURCES.md documentation describing how to specify plugin source configurations (Commit message generated by Copilot) * comments * windows fixes and fault handling * fix tests --- extensions/git/src/commands.ts | 14 +- extensions/git/src/git.ts | 11 +- .../agentPluginEditor/agentPluginEditor.ts | 7 +- .../agentPluginEditor/agentPluginItems.ts | 3 +- .../browser/agentPluginRepositoryService.ts | 180 +++++- .../contrib/chat/browser/agentPluginsView.ts | 7 +- .../chat/browser/pluginInstallService.ts | 270 +++++++- .../plugins/agentPluginRepositoryService.ts | 24 +- .../chat/common/plugins/agentPluginService.ts | 2 + .../common/plugins/agentPluginServiceImpl.ts | 1 + .../plugins/pluginMarketplaceService.ts | 280 +++++++- .../agentPluginRepositoryService.test.ts | 48 +- .../plugins/pluginInstallService.test.ts | 611 ++++++++++++++++++ .../plugins/pluginMarketplaceService.test.ts | 152 ++++- .../service/promptsService.test.ts | 3 +- .../common/discovery/pluginMcpDiscovery.ts | 3 +- 16 files changed, 1571 insertions(+), 45 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 64898026821..d3eae80b5e1 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1040,19 +1040,29 @@ export class CommandCenter { } @command('_git.cloneRepository') - async cloneRepository(url: string, parentPath: string): Promise { + async cloneRepository(url: string, localPath: string, ref?: string): Promise { const opts = { location: ProgressLocation.Notification, title: l10n.t('Cloning git repository "{0}"...', url), cancellable: true }; + const parentPath = path.dirname(localPath); + const targetName = path.basename(localPath); + await window.withProgress( opts, - (progress, token) => this.model.git.clone(url, { parentPath, progress }, token) + (progress, token) => this.model.git.clone(url, { parentPath, targetName, progress, ref }, token) ); } + @command('_git.checkout') + async checkoutRepository(repositoryPath: string, treeish: string, detached?: boolean): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + await repo.checkout(treeish, [], detached ? { detached: true } : {}); + } + @command('_git.pull') async pullRepository(repositoryPath: string): Promise { const dotGit = await this.git.getRepositoryDotGit(repositoryPath); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 5f7d1100f70..1bd20a42c54 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -378,6 +378,7 @@ const STASH_FORMAT = '%H%n%P%n%gd%n%gs%n%at%n%ct'; export interface ICloneOptions { readonly parentPath: string; + readonly targetName?: string; readonly progress: Progress<{ increment: number }>; readonly recursive?: boolean; readonly ref?: string; @@ -433,14 +434,16 @@ export class Git { } async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise { - const baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository'; + const baseFolderName = options.targetName || decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository'; let folderName = baseFolderName; let folderPath = path.join(options.parentPath, folderName); let count = 1; - while (count < 20 && await new Promise(c => exists(folderPath, c))) { - folderName = `${baseFolderName}-${count++}`; - folderPath = path.join(options.parentPath, folderName); + if (!options.targetName) { + while (count < 20 && await new Promise(c => exists(folderPath, c))) { + folderName = `${baseFolderName}-${count++}`; + folderPath = path.join(options.parentPath, folderName); + } } await mkdirp(options.parentPath); diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts index df8124325ee..bca23294bf6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts @@ -12,7 +12,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas, matchesScheme } from '../../../../../base/common/network.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { basename, dirname, joinPath } from '../../../../../base/common/resources.js'; +import { dirname, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { TokenizationRegistry } from '../../../../../editor/common/languages.js'; @@ -202,6 +202,7 @@ export class AgentPluginEditor extends EditorPane { description: item.description, version: '', source: item.source, + sourceDescriptor: item.sourceDescriptor, marketplace: item.marketplace, marketplaceReference: item.marketplaceReference, marketplaceType: item.marketplaceType, @@ -222,6 +223,7 @@ export class AgentPluginEditor extends EditorPane { name: item.name, description: mp.description, source: mp.source, + sourceDescriptor: mp.sourceDescriptor, marketplace: mp.marketplace, marketplaceReference: mp.marketplaceReference, marketplaceType: mp.marketplaceType, @@ -267,7 +269,7 @@ export class AgentPluginEditor extends EditorPane { } private installedPluginToItem(plugin: IAgentPlugin): IInstalledPluginItem { - const name = basename(plugin.uri); + const name = plugin.label; const description = plugin.fromMarketplace?.description ?? this.labelService.getUriLabel(dirname(plugin.uri), { relative: true }); const marketplace = plugin.fromMarketplace?.marketplace; return { kind: AgentPluginItemKind.Installed, name, description, marketplace, plugin }; @@ -517,6 +519,7 @@ class InstallPluginEditorAction extends Action { description: this.item.description, version: '', source: this.item.source, + sourceDescriptor: this.item.sourceDescriptor, marketplace: this.item.marketplace, marketplaceReference: this.item.marketplaceReference, marketplaceType: this.item.marketplaceType, diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts index 20ec5ed4009..9f1b8f8e97c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../base/common/uri.js'; import type { IAgentPlugin } from '../../common/plugins/agentPluginService.js'; -import type { IMarketplaceReference, MarketplaceType } from '../../common/plugins/pluginMarketplaceService.js'; +import type { IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType } from '../../common/plugins/pluginMarketplaceService.js'; export const enum AgentPluginItemKind { Installed = 'installed', @@ -25,6 +25,7 @@ export interface IMarketplacePluginItem { readonly name: string; readonly description: string; readonly source: string; + readonly sourceDescriptor: IPluginSourceDescriptor; readonly marketplace: string; readonly marketplaceReference: IMarketplaceReference; readonly marketplaceType: MarketplaceType; diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts index 7b2244345e8..41abb16b8f8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts @@ -18,7 +18,7 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import type { Dto } from '../../../services/extensions/common/proxyIdentifier.js'; import { IAgentPluginRepositoryService, IEnsureRepositoryOptions, IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js'; -import { IMarketplacePlugin, IMarketplaceReference, MarketplaceReferenceKind, MarketplaceType } from '../common/plugins/pluginMarketplaceService.js'; +import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceReferenceKind, MarketplaceType, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; const MARKETPLACE_INDEX_STORAGE_KEY = 'chat.plugins.marketplaces.index.v1'; @@ -176,7 +176,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi this._storageService.store(MARKETPLACE_INDEX_STORAGE_KEY, JSON.stringify(serialized), StorageScope.APPLICATION, StorageTarget.MACHINE); } - private async _cloneRepository(repoDir: URI, cloneUrl: string, progressTitle: string, failureLabel: string): Promise { + private async _cloneRepository(repoDir: URI, cloneUrl: string, progressTitle: string, failureLabel: string, ref?: string): Promise { try { await this._progressService.withProgress( { @@ -186,7 +186,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi }, async () => { await this._fileService.createFolder(dirname(repoDir)); - await this._commandService.executeCommand('_git.cloneRepository', cloneUrl, dirname(repoDir).fsPath); + await this._commandService.executeCommand('_git.cloneRepository', cloneUrl, repoDir.fsPath, ref); } ); } catch (err) { @@ -212,4 +212,178 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi } return pluginDir; } + + getPluginSourceInstallUri(sourceDescriptor: IPluginSourceDescriptor): URI { + switch (sourceDescriptor.kind) { + case PluginSourceKind.RelativePath: + throw new Error('Use getPluginInstallUri() for relative-path sources'); + case PluginSourceKind.GitHub: { + const [owner, repo] = sourceDescriptor.repo.split('/'); + return joinPath(this._cacheRoot, 'github.com', owner, repo, ...this._getSourceRevisionCacheSuffix(sourceDescriptor)); + } + case PluginSourceKind.GitUrl: { + const segments = this._gitUrlCacheSegments(sourceDescriptor.url, sourceDescriptor.ref, sourceDescriptor.sha); + return joinPath(this._cacheRoot, ...segments); + } + case PluginSourceKind.Npm: + return joinPath(this._cacheRoot, 'npm', sanitizePackageName(sourceDescriptor.package), 'node_modules', sourceDescriptor.package); + case PluginSourceKind.Pip: + return joinPath(this._cacheRoot, 'pip', sanitizePackageName(sourceDescriptor.package)); + } + } + + async ensurePluginSource(plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise { + const descriptor = plugin.sourceDescriptor; + switch (descriptor.kind) { + case PluginSourceKind.RelativePath: + return this.ensureRepository(plugin.marketplaceReference, options); + case PluginSourceKind.GitHub: { + const cloneUrl = `https://github.com/${descriptor.repo}.git`; + const repoDir = this.getPluginSourceInstallUri(descriptor); + const repoExists = await this._fileService.exists(repoDir); + if (repoExists) { + await this._checkoutPluginSourceRevision(repoDir, descriptor, options?.failureLabel ?? descriptor.repo); + return repoDir; + } + const progressTitle = options?.progressTitle ?? localize('cloningPluginSource', "Cloning plugin source '{0}'...", descriptor.repo); + const failureLabel = options?.failureLabel ?? descriptor.repo; + await this._cloneRepository(repoDir, cloneUrl, progressTitle, failureLabel, descriptor.ref); + await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); + return repoDir; + } + case PluginSourceKind.GitUrl: { + const repoDir = this.getPluginSourceInstallUri(descriptor); + const repoExists = await this._fileService.exists(repoDir); + if (repoExists) { + await this._checkoutPluginSourceRevision(repoDir, descriptor, options?.failureLabel ?? descriptor.url); + return repoDir; + } + const progressTitle = options?.progressTitle ?? localize('cloningPluginSourceUrl', "Cloning plugin source '{0}'...", descriptor.url); + const failureLabel = options?.failureLabel ?? descriptor.url; + await this._cloneRepository(repoDir, descriptor.url, progressTitle, failureLabel, descriptor.ref); + await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); + return repoDir; + } + case PluginSourceKind.Npm: { + // npm/pip install directories are managed by the install service. + // Return the expected install URI without performing installation. + return joinPath(this._cacheRoot, 'npm', sanitizePackageName(descriptor.package)); + } + case PluginSourceKind.Pip: { + return joinPath(this._cacheRoot, 'pip', sanitizePackageName(descriptor.package)); + } + } + } + + async updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { + const descriptor = plugin.sourceDescriptor; + if (descriptor.kind !== PluginSourceKind.GitHub && descriptor.kind !== PluginSourceKind.GitUrl) { + return; + } + + const repoDir = this.getPluginSourceInstallUri(descriptor); + const repoExists = await this._fileService.exists(repoDir); + if (!repoExists) { + this._logService.warn(`[AgentPluginRepositoryService] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`); + return; + } + + const updateLabel = options?.pluginName ?? plugin.name; + const failureLabel = options?.failureLabel ?? updateLabel; + + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel), + cancellable: false, + }, + async () => { + await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); + if (descriptor.sha) { + await this._commandService.executeCommand('git.fetch', repoDir.fsPath); + } else { + await this._commandService.executeCommand('_git.pull', repoDir.fsPath); + } + await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); + } + ); + } catch (err) { + this._logService.error(`[AgentPluginRepositoryService] Failed to update plugin source ${updateLabel}:`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), + actions: { + primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { + this._commandService.executeCommand('git.showOutput'); + })], + }, + }); + } + } + + private _gitUrlCacheSegments(url: string, ref?: string, sha?: string): string[] { + try { + const parsed = URI.parse(url); + const authority = (parsed.authority || 'unknown').replace(/[\\/:*?"<>|]/g, '_').toLowerCase(); + const pathPart = parsed.path.replace(/^\/+/, '').replace(/\.git$/i, '').replace(/\/+$/g, ''); + const segments = pathPart.split('/').map(s => s.replace(/[\\/:*?"<>|]/g, '_')); + return [authority, ...segments, ...this._getSourceRevisionCacheSuffix(ref, sha)]; + } catch { + return ['git', url.replace(/[\\/:*?"<>|]/g, '_'), ...this._getSourceRevisionCacheSuffix(ref, sha)]; + } + } + + private _getSourceRevisionCacheSuffix(descriptorOrRef: IPluginSourceDescriptor | string | undefined, sha?: string): string[] { + if (typeof descriptorOrRef === 'object' && descriptorOrRef) { + if (descriptorOrRef.kind === PluginSourceKind.GitHub || descriptorOrRef.kind === PluginSourceKind.GitUrl) { + return this._getSourceRevisionCacheSuffix(descriptorOrRef.ref, descriptorOrRef.sha); + } + return []; + } + + const ref = descriptorOrRef; + if (sha) { + return [`sha_${sanitizePackageName(sha)}`]; + } + if (ref) { + return [`ref_${sanitizePackageName(ref)}`]; + } + return []; + } + + private async _checkoutPluginSourceRevision(repoDir: URI, descriptor: IPluginSourceDescriptor, failureLabel: string): Promise { + if (descriptor.kind !== PluginSourceKind.GitHub && descriptor.kind !== PluginSourceKind.GitUrl) { + return; + } + + if (!descriptor.sha && !descriptor.ref) { + return; + } + + try { + if (descriptor.sha) { + await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, descriptor.sha, true); + return; + } + + await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, descriptor.ref); + } catch (err) { + this._logService.error(`[AgentPluginRepositoryService] Failed to checkout plugin source revision for ${failureLabel}:`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('checkoutPluginSourceFailed', "Failed to checkout plugin '{0}' to requested revision: {1}", failureLabel, err?.message ?? String(err)), + actions: { + primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { + this._commandService.executeCommand('git.showOutput'); + })], + }, + }); + throw err; + } + } +} + +function sanitizePackageName(name: string): string { + return name.replace(/[\\/:*?"<>|]/g, '_'); } diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts index 129cbad928f..0033b0888ef 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts @@ -17,7 +17,7 @@ import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, isDispos import { ThemeIcon } from '../../../../base/common/themables.js'; import { autorun } from '../../../../base/common/observable.js'; import { IPagedModel, PagedModel } from '../../../../base/common/paging.js'; -import { basename, dirname, joinPath } from '../../../../base/common/resources.js'; +import { dirname, joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -56,7 +56,7 @@ export const InstalledAgentPluginsViewId = 'workbench.views.agentPlugins.install //#region Item model function installedPluginToItem(plugin: IAgentPlugin, labelService: ILabelService): IInstalledPluginItem { - const name = basename(plugin.uri); + const name = plugin.label; const description = plugin.fromMarketplace?.description ?? labelService.getUriLabel(dirname(plugin.uri), { relative: true }); const marketplace = plugin.fromMarketplace?.marketplace; return { kind: AgentPluginItemKind.Installed, name, description, marketplace, plugin }; @@ -68,6 +68,7 @@ function marketplacePluginToItem(plugin: IMarketplacePlugin): IMarketplacePlugin name: plugin.name, description: plugin.description, source: plugin.source, + sourceDescriptor: plugin.sourceDescriptor, marketplace: plugin.marketplace, marketplaceReference: plugin.marketplaceReference, marketplaceType: plugin.marketplaceType, @@ -95,6 +96,7 @@ class InstallPluginAction extends Action { description: this.item.description, version: '', source: this.item.source, + sourceDescriptor: this.item.sourceDescriptor, marketplace: this.item.marketplace, marketplaceReference: this.item.marketplaceReference, marketplaceType: this.item.marketplaceType, @@ -518,6 +520,7 @@ export class AgentPluginsListView extends AbstractExtensionsListView { + switch (plugin.sourceDescriptor.kind) { + case PluginSourceKind.RelativePath: + return this._installRelativePathPlugin(plugin); + case PluginSourceKind.GitHub: + case PluginSourceKind.GitUrl: + return this._installGitPlugin(plugin); + case PluginSourceKind.Npm: + return this._installNpmPlugin(plugin, plugin.sourceDescriptor); + case PluginSourceKind.Pip: + return this._installPipPlugin(plugin, plugin.sourceDescriptor); + } + } + + async updatePlugin(plugin: IMarketplacePlugin): Promise { + switch (plugin.sourceDescriptor.kind) { + case PluginSourceKind.RelativePath: + return this._pluginRepositoryService.pullRepository(plugin.marketplaceReference, { + pluginName: plugin.name, + failureLabel: plugin.name, + marketplaceType: plugin.marketplaceType, + }); + case PluginSourceKind.GitHub: + case PluginSourceKind.GitUrl: + return this._pluginRepositoryService.updatePluginSource(plugin, { + pluginName: plugin.name, + failureLabel: plugin.name, + marketplaceType: plugin.marketplaceType, + }); + case PluginSourceKind.Npm: + return this._installNpmPlugin(plugin, plugin.sourceDescriptor); + case PluginSourceKind.Pip: + return this._installPipPlugin(plugin, plugin.sourceDescriptor); + } + } + + getPluginInstallUri(plugin: IMarketplacePlugin): URI { + if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { + return this._pluginRepositoryService.getPluginInstallUri(plugin); + } + return this._pluginRepositoryService.getPluginSourceInstallUri(plugin.sourceDescriptor); + } + + // --- Relative-path source (existing git-based flow) ----------------------- + + private async _installRelativePathPlugin(plugin: IMarketplacePlugin): Promise { try { await this._pluginRepositoryService.ensureRepository(plugin.marketplaceReference, { progressTitle: localize('installingPlugin', "Installing plugin '{0}'...", plugin.name), @@ -55,15 +112,212 @@ export class PluginInstallService implements IPluginInstallService { this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); } - async updatePlugin(plugin: IMarketplacePlugin): Promise { - return this._pluginRepositoryService.pullRepository(plugin.marketplaceReference, { - pluginName: plugin.name, - failureLabel: plugin.name, - marketplaceType: plugin.marketplaceType, + // --- GitHub / Git URL source (independent clone) -------------------------- + + private async _installGitPlugin(plugin: IMarketplacePlugin): Promise { + let pluginDir: URI; + try { + pluginDir = await this._pluginRepositoryService.ensurePluginSource(plugin, { + progressTitle: localize('installingPlugin', "Installing plugin '{0}'...", plugin.name), + failureLabel: plugin.name, + marketplaceType: plugin.marketplaceType, + }); + } catch { + return; + } + + const pluginExists = await this._fileService.exists(pluginDir); + if (!pluginExists) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pluginSourceNotFound', "Plugin source '{0}' not found after cloning.", getPluginSourceLabel(plugin.sourceDescriptor)), + }); + return; + } + + this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); + } + + // --- npm source ----------------------------------------------------------- + + private async _installNpmPlugin(plugin: IMarketplacePlugin, source: INpmPluginSource): Promise { + const packageSpec = source.version ? `${source.package}@${source.version}` : source.package; + const installDir = await this._pluginRepositoryService.ensurePluginSource(plugin); + const args = ['npm', 'install', '--prefix', installDir.fsPath, packageSpec]; + if (source.registry) { + args.push('--registry', source.registry); + } + const command = this._formatShellCommand(args); + + const confirmed = await this._confirmTerminalCommand(plugin.name, command); + if (!confirmed) { + return; + } + + const { success, terminal } = await this._runTerminalCommand( + command, + localize('installingNpmPlugin', "Installing npm plugin '{0}'...", plugin.name), + ); + if (!success) { + return; + } + + const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(source); + const pluginExists = await this._fileService.exists(pluginDir); + if (!pluginExists) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('npmPluginNotFound', "npm package '{0}' was not found after installation.", source.package), + }); + return; + } + + terminal?.dispose(); + this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); + } + + // --- pip source ----------------------------------------------------------- + + private async _installPipPlugin(plugin: IMarketplacePlugin, source: IPipPluginSource): Promise { + const packageSpec = source.version ? `${source.package}==${source.version}` : source.package; + const installDir = await this._pluginRepositoryService.ensurePluginSource(plugin); + const args = ['pip', 'install', '--target', installDir.fsPath, packageSpec]; + if (source.registry) { + args.push('--index-url', source.registry); + } + const command = this._formatShellCommand(args); + + const confirmed = await this._confirmTerminalCommand(plugin.name, command); + if (!confirmed) { + return; + } + + const { success, terminal } = await this._runTerminalCommand( + command, + localize('installingPipPlugin', "Installing pip plugin '{0}'...", plugin.name), + ); + if (!success) { + return; + } + + const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(source); + const pluginExists = await this._fileService.exists(pluginDir); + if (!pluginExists) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pipPluginNotFound', "pip package '{0}' was not found after installation.", source.package), + }); + return; + } + + terminal?.dispose(); + this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); + } + + // --- Helpers -------------------------------------------------------------- + + private async _confirmTerminalCommand(pluginName: string, command: string): Promise { + const { confirmed } = await this._dialogService.confirm({ + type: 'question', + message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName), + detail: localize('confirmPluginInstallDetail', "This will run the following command in a terminal:\n\n{0}", command), + primaryButton: localize({ key: 'confirmInstall', comment: ['&& denotes a mnemonic'] }, "&&Install"), + }); + return confirmed; + } + + private async _runTerminalCommand(command: string, progressTitle: string) { + let terminal: ITerminalInstance | undefined; + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: progressTitle, + cancellable: false, + }, + async () => { + terminal = await this._terminalService.createTerminal({ + config: { + name: localize('pluginInstallTerminal', "Plugin Install"), + forceShellIntegration: true, + isTransient: true, + isFeatureTerminal: true, + }, + }); + + await terminal.processReady; + this._terminalService.setActiveInstance(terminal); + + const commandResultPromise = this._waitForTerminalCommandCompletion(terminal); + await terminal.runCommand(command, true); + const exitCode = await commandResultPromise; + if (exitCode !== 0) { + throw new Error(localize('terminalCommandExitCode', "Command exited with code {0}", exitCode)); + } + } + ); + return { success: true, terminal }; + } catch (err) { + this._logService.error('[PluginInstallService] Terminal command failed:', err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('terminalCommandFailed', "Plugin installation command failed: {0}", err?.message ?? String(err)), + }); + return { success: false, terminal }; + } + } + + private _waitForTerminalCommandCompletion(terminal: ITerminalInstance): Promise { + return new Promise(resolve => { + const disposables = new DisposableStore(); + let isResolved = false; + + const resolveAndDispose = (exitCode: number | undefined): void => { + if (isResolved) { + return; + } + isResolved = true; + disposables.dispose(); + resolve(exitCode); + }; + + const attachCommandFinishedListener = (): void => { + const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection); + if (!commandDetection) { + return; + } + + disposables.add(commandDetection.onCommandFinished((command: ITerminalCommand) => { + resolveAndDispose(command.exitCode ?? 0); + })); + }; + + attachCommandFinishedListener(); + disposables.add(terminal.capabilities.onDidAddCommandDetectionCapability(() => attachCommandFinishedListener())); + + const timeoutHandle: CancelablePromise = timeout(120_000); + disposables.add(toDisposable(() => timeoutHandle.cancel())); + void timeoutHandle.then(() => { + if (isResolved) { + return; + } + this._logService.warn('[PluginInstallService] Terminal command completion timed out'); + resolveAndDispose(undefined); + }); }); } - getPluginInstallUri(plugin: IMarketplacePlugin): URI { - return this._pluginRepositoryService.getPluginInstallUri(plugin); + private _formatShellCommand(args: readonly string[]): string { + const [command, ...rest] = args; + return [command, ...rest.map(arg => this._shellEscapeArg(arg))].join(' '); + } + + private _shellEscapeArg(value: string): string { + if (isWindows) { + // PowerShell: use double quotes, escape backticks, dollar signs, and double quotes + return `"${value.replace(/[`$"]/g, '`$&')}"`; + } + // POSIX shells: use single quotes, escape by ending quote, adding escaped quote, reopening + return `'${value.replace(/'/g, `'\\''`)}'`; } } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts index 3f9a9bffda5..cfb76de1c1d 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IMarketplacePlugin, IMarketplaceReference, MarketplaceType } from './pluginMarketplaceService.js'; +import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType } from './pluginMarketplaceService.js'; export const IAgentPluginRepositoryService = createDecorator('agentPluginRepositoryService'); @@ -61,4 +61,26 @@ export interface IAgentPluginRepositoryService { * Pulls latest changes for a cloned marketplace repository. */ pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise; + + /** + * Returns the local install URI for a plugin based on its + * {@link IPluginSourceDescriptor}. For non-relative-path sources + * (github, url, npm, pip), this computes a cache location independent + * of the marketplace repository. + */ + getPluginSourceInstallUri(sourceDescriptor: IPluginSourceDescriptor): URI; + + /** + * Ensures the plugin source is available locally. For github/url sources + * this clones the repository into the cache. For npm/pip sources this is + * a no-op (installation via terminal is handled by the install service). + */ + ensurePluginSource(plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise; + + /** + * Updates a plugin source that is stored outside the marketplace repository. + * For github/url sources this pulls latest changes and reapplies pinned + * ref/sha checkout. For npm/pip sources this is a no-op. + */ + updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index bbc65030621..4efdce7078e 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -44,6 +44,8 @@ export interface IAgentPluginMcpServerDefinition { export interface IAgentPlugin { readonly uri: URI; + /** Human-readable display name for the plugin. */ + readonly label: string; readonly enabled: IObservable; setEnabled(enabled: boolean): void; /** Removes this plugin from its discovery source (config or installed storage). */ diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index bfd8c768e5e..722ff6da576 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -416,6 +416,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements const plugin: PluginEntry = { uri, + label: fromMarketplace?.name ?? basename(uri), enabled, setEnabled: setEnabledCallback, remove: removeCallback, diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index 11a8db54823..dbd6e9b7b17 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -45,12 +45,64 @@ export interface IMarketplaceReference { readonly localRepositoryUri?: URI; } +export const enum PluginSourceKind { + RelativePath = 'relativePath', + GitHub = 'github', + GitUrl = 'url', + Npm = 'npm', + Pip = 'pip', +} + +export interface IRelativePathPluginSource { + readonly kind: PluginSourceKind.RelativePath; + /** Resolved relative path within the marketplace repository. */ + readonly path: string; +} + +export interface IGitHubPluginSource { + readonly kind: PluginSourceKind.GitHub; + readonly repo: string; + readonly ref?: string; + readonly sha?: string; +} + +export interface IGitUrlPluginSource { + readonly kind: PluginSourceKind.GitUrl; + /** Full git repository URL (must end with .git). */ + readonly url: string; + readonly ref?: string; + readonly sha?: string; +} + +export interface INpmPluginSource { + readonly kind: PluginSourceKind.Npm; + readonly package: string; + readonly version?: string; + readonly registry?: string; +} + +export interface IPipPluginSource { + readonly kind: PluginSourceKind.Pip; + readonly package: string; + readonly version?: string; + readonly registry?: string; +} + +export type IPluginSourceDescriptor = + | IRelativePathPluginSource + | IGitHubPluginSource + | IGitUrlPluginSource + | INpmPluginSource + | IPipPluginSource; + export interface IMarketplacePlugin { readonly name: string; readonly description: string; readonly version: string; - /** Subdirectory within the repository where the plugin lives. */ + /** Subdirectory within the repository where the plugin lives (for relative-path sources). */ readonly source: string; + /** Structured source descriptor indicating how the plugin should be fetched/installed. */ + readonly sourceDescriptor: IPluginSourceDescriptor; /** Marketplace label shown in UI and plugin provenance. */ readonly marketplace: string; /** Canonical reference for clone/update/install location resolution. */ @@ -60,6 +112,18 @@ export interface IMarketplacePlugin { readonly readmeUri?: URI; } +/** Raw JSON shape of a remote plugin source object in marketplace.json. */ +interface IJsonPluginSource { + readonly source: string; + readonly repo?: string; + readonly url?: string; + readonly package?: string; + readonly ref?: string; + readonly sha?: string; + readonly version?: string; + readonly registry?: string; +} + interface IMarketplaceJson { readonly metadata?: { readonly pluginRoot?: string; @@ -68,7 +132,7 @@ interface IMarketplaceJson { readonly name?: string; readonly description?: string; readonly version?: string; - readonly source?: string; + readonly source?: string | IJsonPluginSource; }[]; } @@ -118,6 +182,23 @@ interface IStoredInstalledPlugin { readonly enabled: boolean; } +/** + * Ensures that an {@link IMarketplacePlugin} loaded from storage has a + * {@link IMarketplacePlugin.sourceDescriptor sourceDescriptor}. Plugins + * persisted before the sourceDescriptor field was introduced will only + * have the legacy `source` string — this function synthesises a + * {@link PluginSourceKind.RelativePath} descriptor from it. + */ +function ensureSourceDescriptor(plugin: IMarketplacePlugin): IMarketplacePlugin { + if (plugin.sourceDescriptor) { + return plugin; + } + return { + ...plugin, + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: plugin.source }, + }; +} + const installedPluginsMemento = observableMemento({ defaultValue: [], key: 'chat.plugins.installed.v1', @@ -151,7 +232,12 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke installedPluginsMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, _storageService) ); - this.installedPlugins = this._installedPluginsStore.map(s => revive(s)); + this.installedPlugins = this._installedPluginsStore.map(s => + (revive(s) as readonly IMarketplaceInstalledPlugin[]).map(e => ({ + ...e, + plugin: ensureSourceDescriptor(e.plugin), + })) + ); this.onDidChangeMarketplaces = Event.filter( _configurationService.onDidChangeConfiguration, @@ -213,21 +299,27 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke continue; } const plugins = json.plugins - .filter((p): p is { name: string; description?: string; version?: string; source?: string } => + .filter((p): p is { name: string; description?: string; version?: string; source?: string | IJsonPluginSource } => typeof p.name === 'string' && !!p.name ) .flatMap(p => { - const source = resolvePluginSource(json.metadata?.pluginRoot, p.source ?? ''); - if (source === undefined) { - this._logService.warn(`[PluginMarketplaceService] Skipping plugin '${p.name}' in ${repo}: invalid source path '${p.source ?? ''}' with pluginRoot '${json.metadata?.pluginRoot ?? ''}'`); + const sourceDescriptor = parsePluginSource(p.source, json.metadata?.pluginRoot, { + pluginName: p.name, + logService: this._logService, + logPrefix: `[PluginMarketplaceService]`, + }); + if (!sourceDescriptor) { return []; } + const source = sourceDescriptor.kind === PluginSourceKind.RelativePath ? sourceDescriptor.path : ''; + return [{ name: p.name, description: p.description ?? '', version: p.version ?? '', source, + sourceDescriptor, marketplace: reference.displayLabel, marketplaceReference: reference, marketplaceType: def.type, @@ -293,7 +385,7 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke continue; } - const plugins = entry.plugins.map(plugin => ({ + const plugins = entry.plugins.map(plugin => ensureSourceDescriptor({ ...plugin, marketplace: reference.displayLabel, marketplaceReference: reference, @@ -344,9 +436,11 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void { const current = this.installedPlugins.get(); if (current.some(e => isEqual(e.pluginUri, pluginUri))) { - return; + // Still update to trigger watchers to re-check, something might have happened that we want to know about + this._installedPluginsStore.set([...current], undefined); + } else { + this._installedPluginsStore.set([...current, { pluginUri, plugin, enabled: true }], undefined); } - this._installedPluginsStore.set([...current, { pluginUri, plugin, enabled: true }], undefined); } removeInstalledPlugin(pluginUri: URI): void { @@ -391,21 +485,27 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke } return json.plugins - .filter((p): p is { name: string; description?: string; version?: string; source?: string } => + .filter((p): p is { name: string; description?: string; version?: string; source?: string | IJsonPluginSource } => typeof p.name === 'string' && !!p.name ) .flatMap(p => { - const source = resolvePluginSource(json.metadata?.pluginRoot, p.source ?? ''); - if (source === undefined) { - this._logService.warn(`[PluginMarketplaceService] Skipping plugin '${p.name}' in ${reference.rawValue}: invalid source path '${p.source ?? ''}' with pluginRoot '${json.metadata?.pluginRoot ?? ''}'`); + const sourceDescriptor = parsePluginSource(p.source, json.metadata?.pluginRoot, { + pluginName: p.name, + logService: this._logService, + logPrefix: `[PluginMarketplaceService]`, + }); + if (!sourceDescriptor) { return []; } + const source = sourceDescriptor.kind === PluginSourceKind.RelativePath ? sourceDescriptor.path : ''; + return [{ name: p.name, description: p.description ?? '', version: p.version ?? '', source, + sourceDescriptor, marketplace: reference.displayLabel, marketplaceReference: reference, marketplaceType: def.type, @@ -597,6 +697,158 @@ function resolvePluginSource(pluginRoot: string | undefined, source: string): st return relativePath(repoRoot, resolvedUri) ?? undefined; } +/** + * Parse a raw `source` field from marketplace.json into a structured + * {@link IPluginSourceDescriptor}. Accepts either a relative-path string + * or a JSON object with a `source` discriminant indicating the kind. + */ +export function parsePluginSource( + rawSource: string | IJsonPluginSource | undefined, + pluginRoot: string | undefined, + logContext: { pluginName: string; logService: ILogService; logPrefix: string }, +): IPluginSourceDescriptor | undefined { + if (rawSource === undefined || rawSource === null) { + // Treat missing source the same as empty string → pluginRoot or repo root. + const resolved = resolvePluginSource(pluginRoot, ''); + if (resolved === undefined) { + return undefined; + } + return { kind: PluginSourceKind.RelativePath, path: resolved }; + } + + // String source → legacy relative-path behaviour. + if (typeof rawSource === 'string') { + const resolved = resolvePluginSource(pluginRoot, rawSource); + if (resolved === undefined) { + return undefined; + } + return { kind: PluginSourceKind.RelativePath, path: resolved }; + } + + // Object source → discriminated by `rawSource.source`. + if (typeof rawSource !== 'object' || typeof rawSource.source !== 'string') { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': source object is missing a 'source' discriminant`); + return undefined; + } + + switch (rawSource.source) { + case 'github': { + if (typeof rawSource.repo !== 'string' || !rawSource.repo) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': github source is missing required 'repo' field`); + return undefined; + } + if (!isValidGitHubRepo(rawSource.repo)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': github source repo must be in 'owner/repo' format`); + return undefined; + } + if (!isOptionalString(rawSource.ref)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': github source 'ref' must be a string when provided`); + return undefined; + } + if (!isOptionalGitSha(rawSource.sha)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': github source 'sha' must be a full 40-character commit hash when provided`); + return undefined; + } + return { + kind: PluginSourceKind.GitHub, + repo: rawSource.repo, + ref: rawSource.ref, + sha: rawSource.sha, + }; + } + case 'url': { + if (typeof rawSource.url !== 'string' || !rawSource.url) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': url source is missing required 'url' field`); + return undefined; + } + if (!rawSource.url.toLowerCase().endsWith('.git')) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': url source must end with '.git'`); + return undefined; + } + if (!isOptionalString(rawSource.ref)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': url source 'ref' must be a string when provided`); + return undefined; + } + if (!isOptionalGitSha(rawSource.sha)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': url source 'sha' must be a full 40-character commit hash when provided`); + return undefined; + } + return { + kind: PluginSourceKind.GitUrl, + url: rawSource.url, + ref: rawSource.ref, + sha: rawSource.sha, + }; + } + case 'npm': { + if (typeof rawSource.package !== 'string' || !rawSource.package) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': npm source is missing required 'package' field`); + return undefined; + } + if (!isOptionalString(rawSource.version) || !isOptionalString(rawSource.registry)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': npm source 'version' and 'registry' must be strings when provided`); + return undefined; + } + return { + kind: PluginSourceKind.Npm, + package: rawSource.package, + version: rawSource.version, + registry: rawSource.registry, + }; + } + case 'pip': { + if (typeof rawSource.package !== 'string' || !rawSource.package) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': pip source is missing required 'package' field`); + return undefined; + } + if (!isOptionalString(rawSource.version) || !isOptionalString(rawSource.registry)) { + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': pip source 'version' and 'registry' must be strings when provided`); + return undefined; + } + return { + kind: PluginSourceKind.Pip, + package: rawSource.package, + version: rawSource.version, + registry: rawSource.registry, + }; + } + default: + logContext.logService.warn(`${logContext.logPrefix} Skipping plugin '${logContext.pluginName}': unknown source kind '${rawSource.source}'`); + return undefined; + } +} + +function isOptionalString(value: unknown): value is string | undefined { + return value === undefined || typeof value === 'string'; +} + +function isOptionalGitSha(value: unknown): value is string | undefined { + return value === undefined || (typeof value === 'string' && /^[0-9a-fA-F]{40}$/.test(value)); +} + +function isValidGitHubRepo(repo: string): boolean { + return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo); +} + +/** + * Returns a human-readable label for a plugin source descriptor, + * suitable for error messages and UI display. + */ +export function getPluginSourceLabel(descriptor: IPluginSourceDescriptor): string { + switch (descriptor.kind) { + case PluginSourceKind.RelativePath: + return descriptor.path || '.'; + case PluginSourceKind.GitHub: + return descriptor.repo; + case PluginSourceKind.GitUrl: + return descriptor.url; + case PluginSourceKind.Npm: + return descriptor.version ? `${descriptor.package}@${descriptor.version}` : descriptor.package; + case PluginSourceKind.Pip: + return descriptor.version ? `${descriptor.package}==${descriptor.version}` : descriptor.package; + } +} + function getMarketplaceReadmeUri(repo: string, source: string): URI { const normalizedSource = source.trim().replace(/^\.?\/+|\/+$/g, ''); const readmePath = normalizedSource ? `${normalizedSource}/README.md` : 'README.md'; diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts index d5b0366678f..0aad10fc122 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts @@ -15,7 +15,7 @@ import { INotificationService } from '../../../../../../platform/notification/co import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { AgentPluginRepositoryService } from '../../../browser/agentPluginRepositoryService.js'; -import { IMarketplacePlugin, MarketplaceType, parseMarketplaceReference } from '../../../common/plugins/pluginMarketplaceService.js'; +import { IMarketplacePlugin, MarketplaceType, parseMarketplaceReference, PluginSourceKind } from '../../../common/plugins/pluginMarketplaceService.js'; suite('AgentPluginRepositoryService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -32,6 +32,7 @@ suite('AgentPluginRepositoryService', () => { description: '', version: '', source, + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: source }, marketplace: marketplaceReference.displayLabel, marketplaceReference, marketplaceType: MarketplaceType.Copilot, @@ -40,7 +41,7 @@ suite('AgentPluginRepositoryService', () => { function createService( onExists?: (resource: URI) => Promise, - onExecuteCommand?: (id: string) => void, + onExecuteCommand?: (id: string, ...args: unknown[]) => void, ): AgentPluginRepositoryService { const instantiationService = store.add(new TestInstantiationService()); @@ -53,8 +54,8 @@ suite('AgentPluginRepositoryService', () => { } as unknown as IProgressService; instantiationService.stub(ICommandService, { - executeCommand: async (id: string) => { - onExecuteCommand?.(id); + executeCommand: async (id: string, ...args: unknown[]) => { + onExecuteCommand?.(id, ...args); return undefined; }, } as unknown as ICommandService); @@ -170,4 +171,43 @@ suite('AgentPluginRepositoryService', () => { assert.strictEqual(uri.path, '/tmp/marketplace-repo'); assert.strictEqual(commandInvocationCount, 0); }); + + test('builds revision-aware install URI for github plugin sources', () => { + const service = createService(); + const uri = service.getPluginSourceInstallUri({ + kind: PluginSourceKind.GitHub, + repo: 'owner/repo', + ref: 'release/v1', + }); + + assert.strictEqual(uri.path, '/cache/agentPlugins/github.com/owner/repo/ref_release_v1'); + }); + + test('updates git plugin source by pulling and checking out requested revision', async () => { + const commands: string[] = []; + const service = createService(async () => true, (id: string) => { + commands.push(id); + }); + + await service.updatePluginSource({ + name: 'my-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { + kind: PluginSourceKind.GitHub, + repo: 'owner/repo', + sha: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0', + }, + marketplace: 'owner/repo', + marketplaceReference: parseMarketplaceReference('owner/repo')!, + marketplaceType: MarketplaceType.Copilot, + }, { + pluginName: 'my-plugin', + failureLabel: 'my-plugin', + marketplaceType: MarketplaceType.Copilot, + }); + + assert.deepStrictEqual(commands, ['git.openRepository', 'git.fetch', '_git.checkout']); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts new file mode 100644 index 00000000000..7a55baa369a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts @@ -0,0 +1,611 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { INotificationService } from '../../../../../../platform/notification/common/notification.js'; +import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; +import { ITerminalService } from '../../../../terminal/browser/terminal.js'; +import { PluginInstallService } from '../../../browser/pluginInstallService.js'; +import { IAgentPluginRepositoryService, IEnsureRepositoryOptions, IPullRepositoryOptions } from '../../../common/plugins/agentPluginRepositoryService.js'; +import { IMarketplacePlugin, IMarketplaceReference, IPluginMarketplaceService, IPluginSourceDescriptor, MarketplaceType, parseMarketplaceReference, PluginSourceKind } from '../../../common/plugins/pluginMarketplaceService.js'; + +suite('PluginInstallService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + // --- Factory helpers ------------------------------------------------------- + + function makeMarketplaceRef(marketplace: string): IMarketplaceReference { + const ref = parseMarketplaceReference(marketplace); + assert.ok(ref); + return ref!; + } + + function createPlugin(overrides: Partial & { sourceDescriptor: IPluginSourceDescriptor }): IMarketplacePlugin { + return { + name: overrides.name ?? 'test-plugin', + description: overrides.description ?? '', + version: overrides.version ?? '', + source: overrides.source ?? '', + sourceDescriptor: overrides.sourceDescriptor, + marketplace: overrides.marketplace ?? 'microsoft/vscode', + marketplaceReference: overrides.marketplaceReference ?? makeMarketplaceRef('microsoft/vscode'), + marketplaceType: overrides.marketplaceType ?? MarketplaceType.Copilot, + readmeUri: overrides.readmeUri, + }; + } + + // --- Mock tracking types --------------------------------------------------- + + interface MockState { + notifications: { severity: number; message: string }[]; + addedPlugins: { uri: string; plugin: IMarketplacePlugin }[]; + dialogConfirmResult: boolean; + fileExistsResult: boolean | ((uri: URI) => Promise); + ensureRepositoryResult: URI; + ensurePluginSourceResult: URI; + /** Plugin source install URI, per kind */ + pluginSourceInstallUris: Map; + /** The commands that were sent to the terminal */ + terminalCommands: string[]; + /** Simulated exit code from terminal */ + terminalExitCode: number; + /** Whether the terminal resolves the command completion at all */ + terminalCompletes: boolean; + pullRepositoryCalls: { marketplace: IMarketplaceReference; options?: IPullRepositoryOptions }[]; + updatePluginSourceCalls: { plugin: IMarketplacePlugin; options?: IPullRepositoryOptions }[]; + } + + function createDefaults(): MockState { + return { + notifications: [], + addedPlugins: [], + dialogConfirmResult: true, + fileExistsResult: true, + ensureRepositoryResult: URI.file('/cache/agentPlugins/github.com/microsoft/vscode'), + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-package'), + pluginSourceInstallUris: new Map(), + terminalCommands: [], + terminalExitCode: 0, + terminalCompletes: true, + pullRepositoryCalls: [], + updatePluginSourceCalls: [], + }; + } + + function createService(stateOverrides?: Partial): { service: PluginInstallService; state: MockState } { + const state: MockState = { ...createDefaults(), ...stateOverrides }; + const instantiationService = store.add(new TestInstantiationService()); + + // IFileService + instantiationService.stub(IFileService, { + exists: async (resource: URI) => { + if (typeof state.fileExistsResult === 'function') { + return state.fileExistsResult(resource); + } + return state.fileExistsResult; + }, + } as unknown as IFileService); + + // INotificationService + instantiationService.stub(INotificationService, { + notify: (notification: { severity: number; message: string }) => { + state.notifications.push({ severity: notification.severity, message: notification.message }); + return undefined; + }, + } as unknown as INotificationService); + + // IDialogService + instantiationService.stub(IDialogService, { + confirm: async () => ({ confirmed: state.dialogConfirmResult }), + } as unknown as IDialogService); + + // ITerminalService — the mock coordinates runCommand and onCommandFinished + // so the command ID matches, just like a real terminal would. + instantiationService.stub(ITerminalService, { + createTerminal: async () => { + let finishedCallback: ((cmd: { id: string; exitCode: number }) => void) | undefined; + return { + processReady: Promise.resolve(), + dispose: () => { }, + runCommand: (command: string, _addNewLine?: boolean) => { + state.terminalCommands.push(command); + // Simulate command completing after runCommand is called + if (finishedCallback) { + finishedCallback({ id: 'command', exitCode: state.terminalExitCode }); + } + }, + capabilities: { + get: () => state.terminalCompletes ? { + onCommandFinished: (callback: (cmd: { id: string; exitCode: number }) => void) => { + finishedCallback = callback; + return { dispose() { } }; + }, + } : undefined, + onDidAddCommandDetectionCapability: () => ({ dispose() { } }), + }, + }; + }, + setActiveInstance: () => { }, + } as unknown as ITerminalService); + + // IProgressService + instantiationService.stub(IProgressService, { + withProgress: async (_options: unknown, callback: (...args: unknown[]) => Promise) => callback(), + } as unknown as IProgressService); + + // ILogService + instantiationService.stub(ILogService, new NullLogService()); + + // IAgentPluginRepositoryService + instantiationService.stub(IAgentPluginRepositoryService, { + getPluginInstallUri: (plugin: IMarketplacePlugin) => { + return URI.joinPath(state.ensureRepositoryResult, plugin.source); + }, + getRepositoryUri: () => state.ensureRepositoryResult, + ensureRepository: async (_marketplace: IMarketplaceReference, _options?: IEnsureRepositoryOptions) => { + return state.ensureRepositoryResult; + }, + pullRepository: async (marketplace: IMarketplaceReference, options?: IPullRepositoryOptions) => { + state.pullRepositoryCalls.push({ marketplace, options }); + }, + getPluginSourceInstallUri: (descriptor: IPluginSourceDescriptor) => { + const key = descriptor.kind; + return state.pluginSourceInstallUris.get(key) ?? URI.file(`/cache/agentPlugins/${key}/default`); + }, + ensurePluginSource: async () => state.ensurePluginSourceResult, + updatePluginSource: async (plugin: IMarketplacePlugin, options?: IPullRepositoryOptions) => { + state.updatePluginSourceCalls.push({ plugin, options }); + }, + } as unknown as IAgentPluginRepositoryService); + + // IPluginMarketplaceService + instantiationService.stub(IPluginMarketplaceService, { + addInstalledPlugin: (uri: URI, plugin: IMarketplacePlugin) => { + state.addedPlugins.push({ uri: uri.toString(), plugin }); + }, + } as unknown as IPluginMarketplaceService); + + const service = instantiationService.createInstance(PluginInstallService); + return { service, state }; + } + + // ========================================================================= + // getPluginInstallUri + // ========================================================================= + + suite('getPluginInstallUri', () => { + + test('delegates to getPluginInstallUri for relative-path plugins', () => { + const { service } = createService(); + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + const uri = service.getPluginInstallUri(plugin); + assert.strictEqual(uri.path, '/cache/agentPlugins/github.com/microsoft/vscode/plugins/myPlugin'); + }); + + test('delegates to getPluginSourceInstallUri for npm plugins', () => { + const npmUri = URI.file('/cache/agentPlugins/npm/my-pkg/node_modules/my-pkg'); + const { service } = createService({ + pluginSourceInstallUris: new Map([['npm', npmUri]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + const uri = service.getPluginInstallUri(plugin); + assert.strictEqual(uri.path, npmUri.path); + }); + + test('delegates to getPluginSourceInstallUri for pip plugins', () => { + const pipUri = URI.file('/cache/agentPlugins/pip/my-pkg'); + const { service } = createService({ + pluginSourceInstallUris: new Map([['pip', pipUri]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + }); + const uri = service.getPluginInstallUri(plugin); + assert.strictEqual(uri.path, pipUri.path); + }); + + test('delegates to getPluginSourceInstallUri for github plugins', () => { + const ghUri = URI.file('/cache/agentPlugins/github.com/owner/repo'); + const { service } = createService({ + pluginSourceInstallUris: new Map([['github', ghUri]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + }); + const uri = service.getPluginInstallUri(plugin); + assert.strictEqual(uri.path, ghUri.path); + }); + }); + + // ========================================================================= + // installPlugin — relative path + // ========================================================================= + + suite('installPlugin — relative path', () => { + + test('installs a relative-path plugin when directory exists', async () => { + const { service, state } = createService(); + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 1); + assert.ok(state.addedPlugins[0].uri.includes('plugins/myPlugin')); + assert.strictEqual(state.notifications.length, 0); + }); + + test('notifies error when plugin directory does not exist', async () => { + const { service, state } = createService({ fileExistsResult: false }); + const plugin = createPlugin({ + source: 'plugins/missing', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/missing' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 0); + assert.strictEqual(state.notifications.length, 1); + assert.ok(state.notifications[0].message.includes('not found')); + }); + + test('does not install when ensureRepository throws', async () => { + const { state } = createService(); + // Override ensureRepository to throw + const instantiationService = store.add(new TestInstantiationService()); + const repoService = { + ensureRepository: async () => { throw new Error('clone failed'); }, + getPluginInstallUri: () => URI.file('/x'), + getPluginSourceInstallUri: () => URI.file('/x'), + }; + instantiationService.stub(IAgentPluginRepositoryService, repoService as unknown as IAgentPluginRepositoryService); + instantiationService.stub(IFileService, { exists: async () => true } as unknown as IFileService); + instantiationService.stub(INotificationService, { notify: (n: { severity: number; message: string }) => { state.notifications.push(n); } } as unknown as INotificationService); + instantiationService.stub(IDialogService, { confirm: async () => ({ confirmed: true }) } as unknown as IDialogService); + instantiationService.stub(ITerminalService, {} as unknown as ITerminalService); + instantiationService.stub(IProgressService, { withProgress: async (_o: unknown, cb: () => Promise) => cb() } as unknown as IProgressService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IPluginMarketplaceService, { addInstalledPlugin: () => { } } as unknown as IPluginMarketplaceService); + const svc = instantiationService.createInstance(PluginInstallService); + + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + await svc.installPlugin(plugin); + + // Should return without installing or crashing + assert.strictEqual(state.addedPlugins.length, 0); + }); + }); + + // ========================================================================= + // installPlugin — GitHub / GitUrl + // ========================================================================= + + suite('installPlugin — git sources', () => { + + test('installs a GitHub plugin when source exists after clone', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/github.com/owner/repo'), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 1); + assert.strictEqual(state.notifications.length, 0); + }); + + test('installs a GitUrl plugin when source exists after clone', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/example.com/repo'), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.GitUrl, url: 'https://example.com/repo.git' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 1); + assert.strictEqual(state.notifications.length, 0); + }); + + test('notifies error when cloned directory does not exist', async () => { + const { service, state } = createService({ + fileExistsResult: false, + ensurePluginSourceResult: URI.file('/cache/agentPlugins/github.com/owner/repo'), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 0); + assert.strictEqual(state.notifications.length, 1); + assert.ok(state.notifications[0].message.includes('not found')); + }); + }); + + // ========================================================================= + // installPlugin — npm + // ========================================================================= + + suite('installPlugin — npm', () => { + + test('runs npm install and registers plugin on success', async () => { + const npmInstallUri = URI.file('/cache/agentPlugins/npm/my-pkg/node_modules/my-pkg'); + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + pluginSourceInstallUris: new Map([['npm', npmInstallUri]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('npm')); + assert.ok(state.terminalCommands[0].includes('install')); + assert.ok(state.terminalCommands[0].includes('my-pkg')); + assert.strictEqual(state.addedPlugins.length, 1); + assert.strictEqual(state.notifications.length, 0); + }); + + test('includes version in npm install command', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + pluginSourceInstallUris: new Map([['npm', URI.file('/cache/agentPlugins/npm/my-pkg/node_modules/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg', version: '1.2.3' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('my-pkg@1.2.3')); + }); + + test('includes registry in npm install command', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + pluginSourceInstallUris: new Map([['npm', URI.file('/cache/agentPlugins/npm/my-pkg/node_modules/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg', registry: 'https://custom.registry.com' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('--registry')); + assert.ok(state.terminalCommands[0].includes('https://custom.registry.com')); + }); + + test('does not install when user declines confirmation', async () => { + const { service, state } = createService({ dialogConfirmResult: false }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 0); + assert.strictEqual(state.addedPlugins.length, 0); + }); + + test('notifies error when npm package directory not found after install', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + // exists returns true for ensurePluginSource but false for the final check + fileExistsResult: false, + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 0); + assert.strictEqual(state.notifications.length, 1); + assert.ok(state.notifications[0].message.includes('not found')); + }); + + test('notifies error when terminal command fails with non-zero exit code', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + terminalExitCode: 1, + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 0); + assert.strictEqual(state.notifications.length, 1); + assert.ok(state.notifications[0].message.includes('failed')); + }); + }); + + // ========================================================================= + // installPlugin — pip + // ========================================================================= + + suite('installPlugin — pip', () => { + + test('runs pip install and registers plugin on success', async () => { + const pipInstallUri = URI.file('/cache/agentPlugins/pip/my-pkg'); + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), + pluginSourceInstallUris: new Map([['pip', pipInstallUri]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('pip')); + assert.ok(state.terminalCommands[0].includes('install')); + assert.ok(state.terminalCommands[0].includes('my-pkg')); + assert.strictEqual(state.addedPlugins.length, 1); + assert.strictEqual(state.notifications.length, 0); + }); + + test('includes version with == syntax in pip install command', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), + pluginSourceInstallUris: new Map([['pip', URI.file('/cache/agentPlugins/pip/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg', version: '2.0.0' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('my-pkg==2.0.0')); + }); + + test('includes registry with --index-url in pip install command', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), + pluginSourceInstallUris: new Map([['pip', URI.file('/cache/agentPlugins/pip/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg', registry: 'https://pypi.custom.com/simple' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('--index-url')); + assert.ok(state.terminalCommands[0].includes('https://pypi.custom.com/simple')); + }); + + test('does not install when user declines confirmation', async () => { + const { service, state } = createService({ dialogConfirmResult: false }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 0); + assert.strictEqual(state.addedPlugins.length, 0); + }); + + test('notifies error when pip package directory not found after install', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), + fileExistsResult: false, + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 0); + assert.strictEqual(state.notifications.length, 1); + assert.ok(state.notifications[0].message.includes('not found')); + }); + }); + + // ========================================================================= + // updatePlugin + // ========================================================================= + + suite('updatePlugin', () => { + + test('calls pullRepository for relative-path plugins', async () => { + const { service, state } = createService(); + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + + await service.updatePlugin(plugin); + + assert.strictEqual(state.pullRepositoryCalls.length, 1); + assert.strictEqual(state.updatePluginSourceCalls.length, 0); + }); + + test('calls updatePluginSource for GitHub plugins', async () => { + const { service, state } = createService(); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + }); + + await service.updatePlugin(plugin); + + assert.strictEqual(state.updatePluginSourceCalls.length, 1); + assert.strictEqual(state.pullRepositoryCalls.length, 0); + }); + + test('calls updatePluginSource for GitUrl plugins', async () => { + const { service, state } = createService(); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.GitUrl, url: 'https://example.com/repo.git' }, + }); + + await service.updatePlugin(plugin); + + assert.strictEqual(state.updatePluginSourceCalls.length, 1); + assert.strictEqual(state.pullRepositoryCalls.length, 0); + }); + + test('re-installs for npm plugin updates', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + pluginSourceInstallUris: new Map([['npm', URI.file('/cache/agentPlugins/npm/my-pkg/node_modules/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + + await service.updatePlugin(plugin); + + // npm update goes through the same install flow + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('npm')); + }); + + test('re-installs for pip plugin updates', async () => { + const { service, state } = createService({ + ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), + pluginSourceInstallUris: new Map([['pip', URI.file('/cache/agentPlugins/pip/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + }); + + await service.updatePlugin(plugin); + + assert.strictEqual(state.terminalCommands.length, 1); + assert.ok(state.terminalCommands[0].includes('pip')); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts index 46bb4fbcb3f..da94ecb5615 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts @@ -15,7 +15,7 @@ import { IStorageService, InMemoryStorageService } from '../../../../../../platf import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IAgentPluginRepositoryService } from '../../../common/plugins/agentPluginRepositoryService.js'; import { ChatConfiguration } from '../../../common/constants.js'; -import { MarketplaceReferenceKind, MarketplaceType, PluginMarketplaceService, parseMarketplaceReference, parseMarketplaceReferences } from '../../../common/plugins/pluginMarketplaceService.js'; +import { MarketplaceReferenceKind, MarketplaceType, PluginMarketplaceService, PluginSourceKind, getPluginSourceLabel, parseMarketplaceReference, parseMarketplaceReferences, parsePluginSource } from '../../../common/plugins/pluginMarketplaceService.js'; suite('PluginMarketplaceService', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -142,6 +142,7 @@ suite('PluginMarketplaceService - getMarketplacePluginMetadata', () => { description: 'A test plugin', version: '2.0.0', source: 'plugins/my-plugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/my-plugin' } as const, marketplace: marketplaceRef.displayLabel, marketplaceReference: marketplaceRef, marketplaceType: MarketplaceType.Copilot, @@ -165,3 +166,152 @@ suite('PluginMarketplaceService - getMarketplacePluginMetadata', () => { assert.strictEqual(result, undefined); }); }); + +suite('parsePluginSource', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const logContext = { + pluginName: 'test', + logService: new NullLogService(), + logPrefix: '[test]', + }; + + test('parses string source as RelativePath', () => { + const result = parsePluginSource('./my-plugin', undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.RelativePath, path: 'my-plugin' }); + }); + + test('parses string source with pluginRoot', () => { + const result = parsePluginSource('sub', 'plugins', logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.RelativePath, path: 'plugins/sub' }); + }); + + test('parses undefined source as RelativePath using pluginRoot', () => { + const result = parsePluginSource(undefined, 'root', logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.RelativePath, path: 'root' }); + }); + + test('parses empty string source as RelativePath using pluginRoot', () => { + const result = parsePluginSource('', 'base', logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.RelativePath, path: 'base' }); + }); + + test('returns undefined for empty source without pluginRoot', () => { + assert.strictEqual(parsePluginSource('', undefined, logContext), undefined); + }); + + test('parses github object source', () => { + const result = parsePluginSource({ source: 'github', repo: 'owner/repo' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.GitHub, repo: 'owner/repo', ref: undefined, sha: undefined }); + }); + + test('parses github object source with ref and sha', () => { + const result = parsePluginSource({ source: 'github', repo: 'owner/repo', ref: 'v2.0.0', sha: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.GitHub, repo: 'owner/repo', ref: 'v2.0.0', sha: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0' }); + }); + + test('returns undefined for github source missing repo', () => { + assert.strictEqual(parsePluginSource({ source: 'github' }, undefined, logContext), undefined); + }); + + test('returns undefined for github source with invalid repo format', () => { + assert.strictEqual(parsePluginSource({ source: 'github', repo: 'owner' }, undefined, logContext), undefined); + }); + + test('returns undefined for github source with invalid sha', () => { + assert.strictEqual(parsePluginSource({ source: 'github', repo: 'owner/repo', sha: 'abc123' }, undefined, logContext), undefined); + }); + + test('parses url object source', () => { + const result = parsePluginSource({ source: 'url', url: 'https://gitlab.com/team/plugin.git' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.GitUrl, url: 'https://gitlab.com/team/plugin.git', ref: undefined, sha: undefined }); + }); + + test('returns undefined for url source missing url field', () => { + assert.strictEqual(parsePluginSource({ source: 'url' }, undefined, logContext), undefined); + }); + + test('returns undefined for url source not ending in .git', () => { + assert.strictEqual(parsePluginSource({ source: 'url', url: 'https://gitlab.com/team/plugin' }, undefined, logContext), undefined); + }); + + test('parses npm object source', () => { + const result = parsePluginSource({ source: 'npm', package: '@acme/claude-plugin' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.Npm, package: '@acme/claude-plugin', version: undefined, registry: undefined }); + }); + + test('parses npm object source with version and registry', () => { + const result = parsePluginSource({ source: 'npm', package: '@acme/claude-plugin', version: '2.1.0', registry: 'https://npm.example.com' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.Npm, package: '@acme/claude-plugin', version: '2.1.0', registry: 'https://npm.example.com' }); + }); + + test('returns undefined for npm source missing package', () => { + assert.strictEqual(parsePluginSource({ source: 'npm' }, undefined, logContext), undefined); + }); + + test('returns undefined for npm source with non-string version', () => { + assert.strictEqual(parsePluginSource({ source: 'npm', package: '@acme/claude-plugin', version: 123 } as never, undefined, logContext), undefined); + }); + + test('parses pip object source', () => { + const result = parsePluginSource({ source: 'pip', package: 'my-plugin' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.Pip, package: 'my-plugin', version: undefined, registry: undefined }); + }); + + test('parses pip object source with version and registry', () => { + const result = parsePluginSource({ source: 'pip', package: 'my-plugin', version: '1.0.0', registry: 'https://pypi.example.com' }, undefined, logContext); + assert.deepStrictEqual(result, { kind: PluginSourceKind.Pip, package: 'my-plugin', version: '1.0.0', registry: 'https://pypi.example.com' }); + }); + + test('returns undefined for pip source missing package', () => { + assert.strictEqual(parsePluginSource({ source: 'pip' }, undefined, logContext), undefined); + }); + + test('returns undefined for pip source with non-string registry', () => { + assert.strictEqual(parsePluginSource({ source: 'pip', package: 'my-plugin', registry: 42 } as never, undefined, logContext), undefined); + }); + + test('returns undefined for unknown source kind', () => { + assert.strictEqual(parsePluginSource({ source: 'unknown' }, undefined, logContext), undefined); + }); + + test('returns undefined for object source without source discriminant', () => { + assert.strictEqual(parsePluginSource({ package: 'test' } as never, undefined, logContext), undefined); + }); +}); + +suite('getPluginSourceLabel', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('formats relative path', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.RelativePath, path: 'plugins/foo' }), 'plugins/foo'); + }); + + test('formats empty relative path', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.RelativePath, path: '' }), '.'); + }); + + test('formats github source', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.GitHub, repo: 'owner/repo' }), 'owner/repo'); + }); + + test('formats url source', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.GitUrl, url: 'https://example.com/repo.git' }), 'https://example.com/repo.git'); + }); + + test('formats npm source without version', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.Npm, package: '@acme/plugin' }), '@acme/plugin'); + }); + + test('formats npm source with version', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.Npm, package: '@acme/plugin', version: '1.0.0' }), '@acme/plugin@1.0.0'); + }); + + test('formats pip source without version', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.Pip, package: 'my-plugin' }), 'my-plugin'); + }); + + test('formats pip source with version', () => { + assert.strictEqual(getPluginSourceLabel({ kind: PluginSourceKind.Pip, package: 'my-plugin', version: '2.0' }), 'my-plugin==2.0'); + }); +}); 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 6130e821b99..7d51bb6782f 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 @@ -11,7 +11,7 @@ import { match } from '../../../../../../../base/common/glob.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { ISettableObservable, observableValue } from '../../../../../../../base/common/observable.js'; -import { relativePath } from '../../../../../../../base/common/resources.js'; +import { basename, relativePath } from '../../../../../../../base/common/resources.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; @@ -3505,6 +3505,7 @@ suite('PromptsService', () => { return { plugin: { uri: URI.file(path), + label: basename(URI.file(path)), enabled, setEnabled: () => { }, remove: () => { }, diff --git a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts index 904987c9e34..a173a33b7d2 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts @@ -8,7 +8,6 @@ import { Disposable, DisposableResourceMap } from '../../../../../base/common/li import { ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { basename } from '../../../../../base/common/resources.js'; import { isDefined } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; @@ -61,7 +60,7 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { const collectionId = `plugin.${plugin.uri}`; return this._mcpRegistry.registerCollection({ id: collectionId, - label: `${basename(plugin.uri)} (Agent Plugin)`, + label: `${plugin.label} (Agent Plugin)`, remoteAuthority: plugin.uri.scheme === Schemas.vscodeRemote ? plugin.uri.authority : null, configTarget: ConfigurationTarget.USER, scope: StorageScope.PROFILE, From b31b8d535b63d670e9c5e6caecb7f536781ae500 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 15:50:31 +0000 Subject: [PATCH 159/448] update: refine color theme styles and improve CSS for better UI consistency --- extensions/theme-2026/themes/2026-dark.json | 8 +- extensions/theme-2026/themes/2026-light.json | 4 +- extensions/theme-2026/themes/styles.css | 149 ------------------ src/vs/base/browser/ui/inputbox/inputBox.css | 1 + .../browser/viewParts/minimap/minimap.css | 4 + .../notebook/browser/media/notebook.css | 2 +- 6 files changed, 14 insertions(+), 154 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4645a08fa61..26e6241d748 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -26,7 +26,7 @@ "button.secondaryHoverBackground": "#FFFFFF10", "checkbox.background": "#242526", "checkbox.border": "#333536", - "checkbox.foreground": "#bfbfbf", + "checkbox.foreground": "#8C8C8C", "dropdown.background": "#191A1B", "dropdown.border": "#333536", "dropdown.foreground": "#bfbfbf", @@ -100,12 +100,13 @@ "commandCenter.foreground": "#bfbfbf", "commandCenter.activeForeground": "#bfbfbf", "commandCenter.background": "#191A1B", - "commandCenter.activeBackground": "#252627", + "commandCenter.activeBackground": "#FFFFFF0F", "commandCenter.border": "#2E3031", "editor.background": "#121314", "editor.foreground": "#BBBEBF", "editorStickyScroll.background": "#121314", "editorStickyScrollHover.background": "#202122", + "editorStickyScroll.border": "#2A2B2CFF", "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#BBBEBF", "editorCursor.foreground": "#BBBEBF", @@ -266,7 +267,8 @@ "charts.yellow": "#E0B97F", "charts.orange": "#CD861A", "charts.green": "#86CF86", - "charts.purple": "#AD80D7" + "charts.purple": "#AD80D7", + "inlineChat.border": "#00000000" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f5a8730719e..df59066c02a 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -28,7 +28,7 @@ "button.secondaryHoverBackground": "#F2F3F4", "checkbox.background": "#EAEAEA", "checkbox.border": "#D8D8D8", - "checkbox.foreground": "#202020", + "checkbox.foreground": "#606060", "dropdown.background": "#FFFFFF", "dropdown.border": "#D8D8D8", "dropdown.foreground": "#202020", @@ -54,6 +54,7 @@ "widget.border": "#EEEEF1", "editorStickyScroll.shadow": "#00000000", "editorStickyScrollHover.background": "#F0F0F3", + "editorStickyScroll.border": "#F0F1F2FF", "sideBarStickyScroll.shadow": "#00000000", "panelStickyScroll.shadow": "#00000000", "listFilterWidget.shadow": "#00000000", @@ -272,6 +273,7 @@ "charts.green": "#388A34", "charts.purple": "#652D90", "agentStatusIndicator.background": "#FFFFFF", + "inlineChat.border": "#00000000" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index d76e07491ed..924b770a19f 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -3,15 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -:root { - --radius-sm: 4px; - --radius-lg: 8px; -} - -.monaco-pane-view .split-view-view:first-of-type > .pane > .pane-header { - border-top: 1px solid var(--vscode-sideBarSectionHeader-border) !important; -} - /* Tab border bottom - make transparent */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { --tabs-border-bottom-color: transparent !important; @@ -53,143 +44,3 @@ color: var(--vscode-descriptionForeground) !important; border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } - -/* Input Boxes */ -.monaco-inputbox .monaco-action-bar .action-item .codicon, -.monaco-workbench .search-container .input-box, -.monaco-custom-toggle { - color: var(--vscode-icon-foreground) !important; -} - -/* Chat input toolbar icons should follow icon foreground token */ -.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon, -.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon { - color: var(--vscode-icon-foreground) !important; -} - -/* Todo List Widget - remove shadows from buttons */ -.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button, -.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button:hover, -.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button:active, -.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button, -.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button:hover, -.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button:active { - box-shadow: none; -} - -/* Link buttons and tool call buttons - remove shadows */ -.monaco-workbench .monaco-button.link-button, -.monaco-workbench .monaco-button.link-button:hover, -.monaco-workbench .monaco-button.link-button:active, -.monaco-workbench .chat-confirmation-widget-title.monaco-button, -.monaco-workbench .chat-confirmation-widget-title.monaco-button:hover, -.monaco-workbench .chat-confirmation-widget-title.monaco-button:active, -.monaco-workbench .chat-used-context-label .monaco-button, -.monaco-workbench .chat-used-context-label .monaco-button:hover, -.monaco-workbench .chat-used-context-label .monaco-button:active { - box-shadow: none; -} - -.monaco-workbench .debug-hover-widget { - color: var(--vscode-editor-foreground) !important; -} - -.monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-rows .monaco-list-row:hover:not(.highlighted):not(.selected):not(.focused) { - background-color: var(--vscode-list-hoverBackground); -} - -/* Minimap */ - -.monaco-workbench .monaco-editor .minimap canvas { - opacity: 0.85; -} - -.monaco-workbench.vs-dark .monaco-editor .minimap, -.monaco-workbench .monaco-editor .minimap-shadow-visible { - opacity: 0.85; - background-color: var(--vscode-editor-background); - left: 0; -} - -/* Minimap autohide: ensure opacity:0 overrides the 0.85 above */ -.monaco-workbench .monaco-editor .minimap:is(.minimap-autohide-mouseover, .minimap-autohide-scroll) { - opacity: 0; -} - -.monaco-workbench .monaco-editor .minimap:is(.minimap-autohide-mouseover:hover, .minimap-autohide-scroll.active) { - opacity: 0.85; -} - -/* Sticky Scroll */ -.monaco-workbench .monaco-editor .sticky-widget { - border-bottom: var(--vscode-editorWidget-border) !important; - background: transparent !important; -} - -.monaco-workbench .monaco-editor .sticky-widget > * { - background: transparent !important; -} - -.monaco-workbench.vs-dark .monaco-editor .sticky-widget { - border-bottom: none !important; -} - -.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines-scrollable { - background: var(--vscode-editor-background) !important; -} - -.monaco-editor .sticky-widget .sticky-line-content { - background: var(--vscode-editor-background) !important; -} - -.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers { - background: var(--vscode-editor-background) !important; -} - -.monaco-workbench .monaco-editor .sticky-widget .sticky-line-content:hover { - background: var(--vscode-editorStickyScrollHover-background) !important; -} - -.monaco-editor .rename-box.preview { - border: 1px solid var(--vscode-editorWidget-border); -} - -/* Notebook */ - -.notebookOverlay .monaco-list-row .cell-title-toolbar { - background-color: var(--vscode-editorWidget-background) !important; -} - -/* Inline Chat */ -.monaco-workbench .monaco-editor .inline-chat { - border: none; -} - -/* Command Center */ -.monaco-workbench .part.titlebar .command-center .agent-status-pill { - border-color: var(--vscode-input-border); -} - -.monaco-workbench .part.titlebar .command-center .agent-status-badge { - border-color: var(--vscode-input-border); -} - -.monaco-workbench.vs-dark .monaco-action-bar:not(.vertical) .agent-status-badge-section.sparkle .action-container:hover, -.monaco-workbench.vs-dark .monaco-action-bar:not(.vertical) .agent-status-badge-section.sparkle .dropdown-action-container:hover - { - background-color: var(--vscode-toolbar-hoverBackground); -} - -.monaco-workbench.vs-dark .monaco-action-bar:not(.vertical) .agent-status-badge .monaco-dropdown-with-primary:not(.disabled):hover { - background-color: var(--vscode-commandCenter-activeBackground); -} - -.monaco-workbench .unified-quick-access-tabs { - background: transparent; -} - -/* Quick Input List - use descriptionForeground color for descriptions */ -.monaco-workbench .quick-input-list .monaco-icon-label .label-description { - opacity: 1; - color: var(--vscode-descriptionForeground); -} diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index 827a19f29b4..dc5e637f6ee 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -103,4 +103,5 @@ background-repeat: no-repeat; width: 16px; height: 16px; + color: var(--vscode-icon-foreground); } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index 73814f23979..e6ced7d3dc8 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -61,3 +61,7 @@ .monaco-editor .minimap { z-index: 5; } + +.monaco-editor .minimap canvas { + opacity: 0.9; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index e0309cff01e..01010edd2af 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -385,9 +385,9 @@ .notebookOverlay .monaco-list-row .cell-title-toolbar { border-radius: var(--vscode-cornerRadius-medium); box-shadow: var(--vscode-shadow-sm); + background-color: var(--vscode-editorWidget-background); } -.notebookOverlay .monaco-list-row .cell-title-toolbar, .notebookOverlay .monaco-list-row.cell-drag-image, .notebookOverlay .cell-bottom-toolbar-container .action-item, .notebookOverlay .cell-list-top-cell-toolbar-container .action-item { From b2fcfb568f8b9877b1d5c67d1832f03eaa12edf8 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 15:59:55 +0000 Subject: [PATCH 160/448] update: remove contrastBorder and use editorWidget-border for chat overlay styles --- extensions/theme-2026/themes/2026-dark.json | 1 - extensions/theme-2026/themes/2026-light.json | 1 - .../chat/browser/chatEditing/media/chatEditingEditorOverlay.css | 2 +- .../chat/browser/chatEditing/media/chatEditorController.css | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 26e6241d748..06cafb2e978 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -10,7 +10,6 @@ "descriptionForeground": "#8C8C8C", "icon.foreground": "#8C8C8C", "focusBorder": "#3994BCB3", - "contrastBorder": "#333536", "textBlockQuote.background": "#242526", "textBlockQuote.border": "#2A2B2CFF", "textCodeBlock.background": "#242526", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index df59066c02a..1818a2c066d 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -10,7 +10,6 @@ "descriptionForeground": "#606060", "icon.foreground": "#606060", "focusBorder": "#0069CCFF", - "contrastBorder": "#F0F1F2FF", "textBlockQuote.background": "#EAEAEA", "textBlockQuote.border": "#F0F1F2FF", "textCodeBlock.background": "#EAEAEA", diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css index 0c26b35ff49..fc44d44f42f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css @@ -8,7 +8,7 @@ color: var(--vscode-foreground); background-color: var(--vscode-editorWidget-background); border-radius: 6px; - border: 1px solid var(--vscode-contrastBorder); + border: 1px solid var(--vscode-editorWidget-border); display: flex; align-items: center; justify-content: center; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css index e24c87525bf..5e5b64f1fcd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css @@ -21,7 +21,7 @@ border-radius: 6px; background-color: var(--vscode-editorWidget-background); color: var(--vscode-foreground); - border: 1px solid var(--vscode-contrastBorder); + border: 1px solid var(--vscode-editorWidget-border); overflow: hidden; } From 43b50946c60517eb8418fa8a4d64637141ca93f5 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 16:07:16 +0000 Subject: [PATCH 161/448] update: adjust tab border styles for improved theme consistency --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 4 ++-- extensions/theme-2026/themes/styles.css | 9 --------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 06cafb2e978..d5655d068ef 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -190,7 +190,6 @@ "tab.inactiveForeground": "#8C8C8C", "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", - "tab.activeBorder": "#121314", "tab.activeBorderTop": "#3994BC", "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", @@ -199,7 +198,8 @@ "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", "editorGroupHeader.tabsBackground": "#191A1B", - "editorGroupHeader.tabsBorder": "#2A2B2CFF", + "tab.activeBorder": "#00000000", + "editorGroupHeader.tabsBorder": "#00000000", "breadcrumb.foreground": "#8C8C8C", "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bfbfbf", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 1818a2c066d..e9db9b37656 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -195,7 +195,6 @@ "tab.inactiveForeground": "#606060", "tab.border": "#F0F1F2FF", "tab.lastPinnedBorder": "#F0F1F2FF", - "tab.activeBorder": "#FAFAFD", "tab.activeBorderTop": "#000000", "tab.hoverBackground": "#DADADA4f", "tab.hoverForeground": "#202020", @@ -204,7 +203,8 @@ "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", - "editorGroupHeader.tabsBorder": "#F0F1F2FF", + "tab.activeBorder": "#00000000", + "editorGroupHeader.tabsBorder": "#00000000", "breadcrumb.foreground": "#606060", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 924b770a19f..be78cde2241 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -3,15 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Tab border bottom - make transparent */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { - --tabs-border-bottom-color: transparent !important; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { - --tab-border-bottom-color: transparent !important; -} - /* Quick Input (Command Palette) */ .monaco-workbench .quick-input-list .quick-input-list-entry .quick-input-list-separator { From 055382526dad5fc03cd886509177237fc6dd2742 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:36:36 -0800 Subject: [PATCH 162/448] Browser style updates (#299221) --- .../lib/stylelint/vscode-known-variables.json | 2 + .../browserView/common/browserView.ts | 1 + .../browserView/electron-main/browserView.ts | 1 + src/vs/workbench/common/theme.ts | 5 + .../electron-browser/browserEditor.ts | 28 ++-- .../electron-browser/media/browser.css | 142 +++++++++++++----- 6 files changed, 132 insertions(+), 47 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 1ea3723af79..29a3d47d232 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -36,6 +36,7 @@ "--vscode-breadcrumb-focusForeground", "--vscode-breadcrumb-foreground", "--vscode-breadcrumbPicker-background", + "--vscode-browser-border", "--vscode-button-background", "--vscode-button-border", "--vscode-button-foreground", @@ -1009,6 +1010,7 @@ "--text-link-decoration", "--vscode-action-item-auto-timeout", "--monaco-editor-warning-decoration", + "--animation-angle", "--animation-opacity", "--chat-setup-dialog-glow-angle", "--vscode-chat-font-family", diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index 499b0ef5cb9..ec51dec6b8f 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -14,6 +14,7 @@ export interface IBrowserViewBounds { width: number; height: number; zoomFactor: number; + cornerRadius: number; } export interface IBrowserViewCaptureScreenshotOptions { diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 11f6f39b592..10fb6b62afa 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -385,6 +385,7 @@ export class BrowserView extends Disposable implements ICDPTarget { } this._view.webContents.setZoomFactor(bounds.zoomFactor); + this._view.setBorderRadius(Math.round(bounds.cornerRadius * bounds.zoomFactor)); this._view.setBounds({ x: Math.round(bounds.x * bounds.zoomFactor), y: Math.round(bounds.y * bounds.zoomFactor), diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 143a09c1bf1..7c7df50fd38 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -549,6 +549,11 @@ export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.borde export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', scrollbarShadow, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); +// < --- Browser --- > + +export const BROWSER_BORDER = registerColor('browser.border', TAB_BORDER, localize('browserBorder', "Border color for integrated browser pages.")); + + // < --- Profiles --- > export const PROFILE_BADGE_BACKGROUND = registerColor('profileBadge.background', { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 1a551c6c31c..a9983e9e468 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -336,9 +336,13 @@ export class BrowserEditor extends EditorPane { this._browserContainer.tabIndex = 0; // make focusable this._browserContainerWrapper.appendChild(this._browserContainer); + // Create additional wrapper around placeholder contents for applying border radius clipping. + const placeholderContents = $('.browser-placeholder-contents'); + this._browserContainer.appendChild(placeholderContents); + // Create placeholder screenshot (background placeholder when WebContentsView is hidden) this._placeholderScreenshot = $('.browser-placeholder-screenshot'); - this._browserContainer.appendChild(this._placeholderScreenshot); + placeholderContents.appendChild(this._placeholderScreenshot); // Create overlay pause container (hidden by default via CSS) this._overlayPauseContainer = $('.browser-overlay-paused'); @@ -348,16 +352,16 @@ export class BrowserEditor extends EditorPane { overlayPauseMessage.appendChild(this._overlayPauseHeading); overlayPauseMessage.appendChild(this._overlayPauseDetail); this._overlayPauseContainer.appendChild(overlayPauseMessage); - this._browserContainer.appendChild(this._overlayPauseContainer); + placeholderContents.appendChild(this._overlayPauseContainer); // Create error container (hidden by default) this._errorContainer = $('.browser-error-container'); this._errorContainer.style.display = 'none'; - this._browserContainer.appendChild(this._errorContainer); + placeholderContents.appendChild(this._errorContainer); // Create welcome container (shown when no URL is loaded) this._welcomeContainer = this.createWelcomeContainer(); - this._browserContainer.appendChild(this._welcomeContainer); + placeholderContents.appendChild(this._welcomeContainer); this._register(addDisposableListener(this._browserContainer, EventType.FOCUS, (event) => { // When the browser container gets focus, make sure the browser view also gets focused. @@ -399,7 +403,7 @@ export class BrowserEditor extends EditorPane { this._storageScopeContext.set(this._model.storageScope); this._devToolsOpenContext.set(this._model.isDevToolsOpen); - this._updateSharingState(); + this._updateSharingState(true); // Update find widget with new model this._findWidget.rawValue?.setModel(this._model); @@ -411,10 +415,10 @@ export class BrowserEditor extends EditorPane { // Listen for sharing state changes on the model this._inputDisposables.add(this._model.onDidChangeSharedWithAgent(() => { - this._updateSharingState(); + this._updateSharingState(false); })); this._inputDisposables.add(watchForAgentSharingContextChanges(this.contextKeyService)(() => { - this._updateSharingState(); + this._updateSharingState(false); })); // Initialize UI state and context keys from model @@ -661,11 +665,12 @@ export class BrowserEditor extends EditorPane { return this._model?.url; } - private _updateSharingState(): void { + private _updateSharingState(isInitialState: boolean): void { const sharingEnabled = this.contextKeyService.contextMatchesRules(canShareBrowserWithAgentContext); const isShared = sharingEnabled && !!this._model && this._model.sharedWithAgent; - this._browserContainerWrapper.classList.toggle('shared', isShared); + this._browserContainer.classList.toggle('animate', !isInitialState); + this._browserContainer.classList.toggle('shared', isShared); this._navigationBar.setShared(isShared); } @@ -1182,13 +1187,16 @@ export class BrowserEditor extends EditorPane { this.checkOverlays(); const containerRect = this._browserContainer.getBoundingClientRect(); + const cornerRadius = this.window.getComputedStyle(this._browserContainer).borderTopLeftRadius ?? '0'; + void this._model.layout({ windowId: this.group.windowId, x: containerRect.left, y: containerRect.top, width: containerRect.width, height: containerRect.height, - zoomFactor: getZoomFactor(this.window) + zoomFactor: getZoomFactor(this.window), + cornerRadius: parseFloat(cornerRadius) }); } } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css index 8ad449ee69d..d80361715ce 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css +++ b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css @@ -3,6 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +@property --animation-angle { + syntax: ''; + inherits: false; + initial-value: 135deg; +} + +@property --animation-opacity { + syntax: ''; + inherits: false; + initial-value: 0%; +} + +@keyframes browser-shared-border-spin { + from { + --animation-angle: 135deg; + } + to { + --animation-angle: 495deg; + } +} + .browser-root { display: flex; flex-direction: column; @@ -13,7 +34,6 @@ display: flex; align-items: center; padding: 6px 8px; - border-bottom: 1px solid var(--vscode-widget-border); background-color: var(--vscode-editor-background); flex-shrink: 0; gap: 8px; @@ -27,10 +47,13 @@ .actions-container { gap: 4px; - margin-right: 4px; } } + .browser-actions-toolbar { + margin-right: 4px; + } + .browser-url-container { flex: 1; display: flex; @@ -68,6 +91,12 @@ color: var(--vscode-descriptionForeground); white-space: nowrap; gap: 4px; + outline: none !important; + + &:focus-visible { + outline: 1px solid var(--vscode-focusBorder) !important; + outline-offset: 0px !important; + } .codicon { margin: 0; @@ -101,37 +130,7 @@ flex: 1; min-height: 0; position: relative; - z-index: 0; /* Important: creates a new stacking context for the gradient border trick */ - - &.shared { - &::before { - content: ''; - position: absolute; - top: -2px; - left: 0; - right: 0; - bottom: 0; - z-index: -2; - background: linear-gradient(135deg in lab, - color-mix(in srgb, #51a2ff 100%, transparent), - color-mix(in srgb, #4af0c0 100%, transparent), - color-mix(in srgb, #b44aff 100%, transparent) - ) !important; - pointer-events: none; - } - - &::after { - content: ''; - position: absolute; - top: 1px; - left: 3px; - right: 3px; - bottom: 3px; - z-index: -1; - background-color: var(--vscode-editor-background); - pointer-events: none; - } - } + margin-top: 1px; } .browser-container { @@ -143,12 +142,78 @@ * which would cause visible shifts when swapping between the live * view and the placeholder screenshot. */ - width: round(down, 100% - 4px, calc(1px / var(--zoom-factor, 1))); - height: round(down, 100% - 2px, calc(1px / var(--zoom-factor, 1))); + width: round(down, 100% - 8px, calc(1px / var(--zoom-factor, 1))); + height: round(down, 100% - 4px, calc(1px / var(--zoom-factor, 1))); margin: 0 auto; overflow: visible; + border-radius: var(--vscode-cornerRadius-medium); position: relative; outline: none !important; + z-index: 0; /* Important: creates a new stacking context for the gradient border trick */ + + &::before { + content: ''; + position: absolute; + --animation-angle: 135deg; + --animation-opacity: 0%; + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + z-index: -2; + border-radius: var(--vscode-cornerRadius-medium); + background: conic-gradient(from var(--animation-angle), + color-mix(in srgb, #b44aff var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #4af0c0 var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #51a2ff var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #4af0c0 var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #b44aff var(--animation-opacity), var(--vscode-browser-border, transparent)) + ); + pointer-events: none; + } + + &.shared::before { + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + --animation-angle: 495deg; + --animation-opacity: 100%; + } + + &.animate::before { + transition: top 350ms cubic-bezier(0.2, 0, 0, 1), + left 350ms cubic-bezier(0.2, 0, 0, 1), + right 350ms cubic-bezier(0.2, 0, 0, 1), + bottom 350ms cubic-bezier(0.2, 0, 0, 1), + --animation-opacity 350ms cubic-bezier(0.2, 0, 0, 1); + } + + &.shared.animate::before { + animation: browser-shared-border-spin 1500ms cubic-bezier(0, 0.2, 0, 1) 1 forwards, + browser-shared-border-spin 45s linear 1500ms infinite; + } + + &::after { + content: ''; + position: absolute; + top: 1px; + left: 1px; + right: 1px; + bottom: 1px; + z-index: -1; + border-radius: var(--vscode-cornerRadius-medium); + background-color: var(--vscode-editor-background); + pointer-events: none; + } + } + + .browser-placeholder-contents { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + border-radius: var(--vscode-cornerRadius-medium); } .browser-placeholder-screenshot { @@ -282,8 +347,9 @@ border-radius: 0; border: none; top: 0 !important; - padding: 6px 8px 6px 12px; + padding: 2px 8px 6px 12px; transition: none; + background: none !important; &:not(.visible) { display: none; @@ -291,11 +357,13 @@ .monaco-sash { width: 2px !important; - border-radius: 0; + height: calc(100% - 4px); + border-radius: var(--vscode-cornerRadius-circle); &::before { width: var(--vscode-sash-hover-size); left: calc(50% - (var(--vscode-sash-hover-size) / 2)); + border-radius: var(--vscode-cornerRadius-circle); } } From 359c7722c8e37f96bb7da1011e52e8edfbb3dd75 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 08:47:32 -0800 Subject: [PATCH 163/448] chat: show Alt key toggle for steer vs queue submit button icon (#299225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When pressing Alt in the chat input, the submit button icon now toggles to match the alternative action (steer ↔ queue), following the standard VS Code pattern. Previously the icon never changed regardless of Alt state. - Fixes the issue where users couldn't visually confirm Alt+Enter would queue instead of steer Fixes #299152 (Commit message generated by Copilot) --- .../widget/input/chatQueuePickerActionItem.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts index c03c6f69f39..51c0ad382e5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, addDisposableListener, append, EventType } from '../../../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType, ModifierKeyEmitter } from '../../../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { Action, IAction } from '../../../../../../base/common/actions.js'; @@ -42,6 +42,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { private readonly _primaryActionAction: Action; private readonly _primaryAction: ActionViewItem; private readonly _dropdown: ActionWidgetDropdownActionViewItem; + private _altKeyPressed = false; constructor( action: IAction, @@ -61,7 +62,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { this._primaryActionAction = this._register(new Action( 'chat.queuePickerPrimary', isSteerDefault ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"), - ThemeIcon.asClassName(Codicon.arrowUp), + ThemeIcon.asClassName(isSteerDefault ? Codicon.arrowRight : Codicon.add), !!contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key), () => this._runDefaultAction() )); @@ -91,21 +92,35 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { this._updatePrimaryAction(); } })); + + // Toggle icon when Alt key is pressed/released + this._register(ModifierKeyEmitter.getInstance().event(status => { + if (this._altKeyPressed !== status.altKey) { + this._altKeyPressed = status.altKey; + this._updatePrimaryAction(); + } + })); } private _isSteerDefault(): boolean { return this.configurationService.getValue(ChatConfiguration.RequestQueueingDefaultAction) === 'steer'; } - private _updatePrimaryAction(): void { + private _isEffectiveSteer(): boolean { const isSteerDefault = this._isSteerDefault(); - this._primaryActionAction.label = isSteerDefault + return this._altKeyPressed ? !isSteerDefault : isSteerDefault; + } + + private _updatePrimaryAction(): void { + const isSteer = this._isEffectiveSteer(); + this._primaryActionAction.label = isSteer ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"); + this._primaryActionAction.class = ThemeIcon.asClassName(isSteer ? Codicon.arrowRight : Codicon.add); } private _runDefaultAction(): void { - const actionId = this._isSteerDefault() + const actionId = this._isEffectiveSteer() ? ChatSteerWithMessageAction.ID : ChatQueueMessageAction.ID; this.commandService.executeCommand(actionId); From 98dc3fd3a4d1916e44c018fc30b9ee17ecfa6783 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 4 Mar 2026 11:48:18 -0500 Subject: [PATCH 164/448] Refactor model selection logic into separate file. Add tons of tests (#299210) * Refactor model selection logic into separate file. Add tons of tests * Use one get all models function * Copilot comments --- .../browser/widget/input/chatInputPart.ts | 133 +- .../widget/input/chatModelSelectionLogic.ts | 290 +++ .../input/chatModelSelectionLogic.test.ts | 1548 +++++++++++++++++ 3 files changed, 1885 insertions(+), 86 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index f4a5f39c53f..8bd16f83c31 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -89,6 +89,7 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; +import { filterModelsForSession, findDefaultModel, hasModelsTargetingSession, isModelValidForSession, mergeModelsWithCache, resolveModelFromSyncState, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, shouldRestorePersistedModel } from './chatModelSelectionLogic.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chatViewModel.js'; import { IChatAgentService } from '../../../common/participants/chatAgents.js'; @@ -625,8 +626,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.initSelectedModel(); this._register(this.languageModelsService.onDidChangeLanguageModels(() => { - const selectedModel = this._currentLanguageModel ? this.getModels().find(m => m.identifier === this._currentLanguageModel.get()?.identifier) : undefined; - if (!this.currentLanguageModel || !selectedModel) { + if (shouldResetOnModelListChange(this._currentLanguageModel.get()?.identifier, this.getModels())) { this.setCurrentLanguageModelToDefault(); } })); @@ -719,25 +719,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, true); if (persistedSelection) { - const model = this.getModels().find(m => m.identifier === persistedSelection); - if (model) { - // Only restore the model if it wasn't the default at the time of storing or it is now the default - if (!persistedAsDefault || model.metadata.isDefaultForLocation[this.location]) { - this.setCurrentLanguageModel(model); - this.checkModelSupported(); - } - } else { + const result = shouldRestorePersistedModel(persistedSelection, persistedAsDefault, this.getModels(), this.location); + if (result.shouldRestore && result.model) { + this.setCurrentLanguageModel(result.model); + this.checkModelSupported(); + } else if (!result.model) { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { const persistedModel = this.languageModelsService.lookupLanguageModel(persistedSelection); if (persistedModel) { this._waitForPersistedLanguageModel.clear(); - // Only restore the model if it wasn't the default at the time of storing or it is now the default - if (!persistedAsDefault || persistedModel.isDefaultForLocation[this.location]) { - if (persistedModel.isUserSelectable) { - this.setCurrentLanguageModel({ metadata: persistedModel, identifier: persistedSelection }); - this.checkModelSupported(); - } + const lateModel = { metadata: persistedModel, identifier: persistedSelection }; + if (shouldRestoreLateArrivingModel(persistedSelection, persistedAsDefault, lateModel, this.location)) { + this.setCurrentLanguageModel(lateModel); + this.checkModelSupported(); } } else { this.setCurrentLanguageModelToDefault(); @@ -946,14 +941,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Sync selected model - validate it belongs to the current session's model pool if (state?.selectedModel) { - const lm = this._currentLanguageModel.get(); - if (!lm || lm.identifier !== state.selectedModel.identifier) { - if (this.isModelValidForCurrentSession(state.selectedModel)) { - this.setCurrentLanguageModel(state.selectedModel); - } else { - // Model from state doesn't belong to this session's pool - use default - this.setCurrentLanguageModelToDefault(); - } + const allModels = this.getAllMergedModels(); + const sessionType = this.getCurrentSessionType(); + const syncResult = resolveModelFromSyncState(state.selectedModel, this._currentLanguageModel.get(), allModels, sessionType, { + location: this.location, + currentModeKind: this.currentModeKind, + isInlineChatV2Enabled: !!this.configurationService.getValue(InlineChatConfigKeys.EnableV2), + sessionType, + }); + if (syncResult.action === 'apply') { + this.setCurrentLanguageModel(state.selectedModel); + } else if (syncResult.action === 'default') { + this.setCurrentLanguageModelToDefault(); } } @@ -1019,7 +1018,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private checkModelSupported(): void { const lm = this._currentLanguageModel.get(); - if (lm && (!this.modelSupportedForDefaultAgent(lm) || !this.modelSupportedForInlineChat(lm) || !this.isModelValidForCurrentSession(lm))) { + const allModels = this.getAllMergedModels(); + if (shouldResetModelToDefault(lm, this.getModels(), { + location: this.location, + currentModeKind: this.currentModeKind, + isInlineChatV2Enabled: !!this.configurationService.getValue(InlineChatConfigKeys.EnableV2), + sessionType: this.getCurrentSessionType(), + }, allModels)) { this.setCurrentLanguageModelToDefault(); } } @@ -1051,56 +1056,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._syncInputStateToModel(); } - private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { - // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex - if (this.currentModeKind === ChatModeKind.Agent) { - return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata); - } - - return true; - } - - private modelSupportedForInlineChat(model: ILanguageModelChatMetadataAndIdentifier): boolean { - if (this.location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) { - return true; - } - return !!model.metadata.capabilities?.toolCalling; - } - - private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + /** + * Get all models merged from live and cache, without session/mode filtering. + * This is the canonical source for the full model pool, including cached models + * that bridge startup races when live models haven't loaded yet. + */ + private getAllMergedModels(): ILanguageModelChatMetadataAndIdentifier[] { const cachedModels = this.storageService.getObject(CachedLanguageModelsKey, StorageScope.APPLICATION, []); const liveModels = this.languageModelsService.getLanguageModelIds() .map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! })); - // Merge live models with cached models per-vendor. For vendors whose - // models have resolved, use the live data. For vendors that are still - // contributed but haven't resolved yet (startup race), keep their - // cached models. Vendors that are no longer contributed at all (e.g. - // extension uninstalled) are evicted from the cache. - let models: ILanguageModelChatMetadataAndIdentifier[]; + const contributedVendors = new Set(this.languageModelsService.getVendors().map(v => v.vendor)); + const models = mergeModelsWithCache(liveModels, cachedModels, contributedVendors); if (liveModels.length > 0) { - const liveVendors = new Set(liveModels.map(m => m.metadata.vendor)); - const contributedVendors = new Set(this.languageModelsService.getVendors().map(v => v.vendor)); - models = [ - ...liveModels, - ...cachedModels.filter(m => !liveVendors.has(m.metadata.vendor) && contributedVendors.has(m.metadata.vendor)), - ]; this.storageService.store(CachedLanguageModelsKey, models, StorageScope.APPLICATION, StorageTarget.MACHINE); - } else { - models = cachedModels; } + return models; + } + + private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + const models = this.getAllMergedModels(); models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); - const sessionType = this.getCurrentSessionType(); - if (sessionType && sessionType !== AgentSessionProviders.Local) { - // Session has a specific chat session type - show only models that target - // this session type, if any such models exist. - return models.filter(entry => entry.metadata?.targetChatSessionType === sessionType && entry.metadata?.isUserSelectable); - } - - // No session type or no targeted models - show general models (those without - // a targetChatSessionType) filtered by the standard criteria. - return models.filter(entry => !entry.metadata?.targetChatSessionType && entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry) && this.modelSupportedForInlineChat(entry)); + return filterModelsForSession(models, this.getCurrentSessionType(), this.currentModeKind, this.location, !!this.configurationService.getValue(InlineChatConfigKeys.EnableV2)); } /** @@ -1122,28 +1100,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * This is used to set the context key that controls model picker visibility. */ private hasModelsTargetingSessionType(): boolean { - const sessionType = this.getCurrentSessionType(); - if (!sessionType) { - return false; - } - return this.languageModelsService.getLanguageModelIds().some(modelId => { - const metadata = this.languageModelsService.lookupLanguageModel(modelId); - return metadata?.targetChatSessionType === sessionType; - }); + return hasModelsTargetingSession(this.getAllMergedModels(), this.getCurrentSessionType()); } - /** - * Check if a model is valid for the current session's model pool. - * If the session has targeted models, the model must target this session type. - * If no models target this session, the model must not have a targetChatSessionType. - */ private isModelValidForCurrentSession(model: ILanguageModelChatMetadataAndIdentifier): boolean { - if (this.hasModelsTargetingSessionType()) { - // Session has targeted models - model must match - return model.metadata.targetChatSessionType === this.getCurrentSessionType(); - } - // No targeted models - model must not be session-specific - return !model.metadata.targetChatSessionType; + return isModelValidForSession(model, this.getAllMergedModels(), this.getCurrentSessionType()); } /** @@ -1218,7 +1179,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private setCurrentLanguageModelToDefault() { const allModels = this.getModels(); - const defaultModel = allModels.find(m => m.metadata.isDefaultForLocation[this.location]) || allModels[0]; + const defaultModel = findDefaultModel(allModels, this.location); if (defaultModel) { this.setCurrentLanguageModel(defaultModel); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts new file mode 100644 index 00000000000..62dbca81dc3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts @@ -0,0 +1,290 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../../../common/languageModels.js'; + +/** + * Describes the context needed for model selection decisions. + */ +export interface IModelSelectionContext { + readonly location: ChatAgentLocation; + readonly currentModeKind: ChatModeKind; + readonly isInlineChatV2Enabled: boolean; + readonly sessionType: string | undefined; +} + +/** + * Filter models based on session type. + * When a session has a specific type (and it's not 'local'), only models targeting that + * session type are returned. Otherwise, general-purpose models are returned. + */ +export function filterModelsForSession( + models: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, + currentModeKind: ChatModeKind, + location: ChatAgentLocation, + isInlineChatV2Enabled: boolean, +): ILanguageModelChatMetadataAndIdentifier[] { + if (sessionType && sessionType !== 'local' && hasModelsTargetingSession(models, sessionType)) { + return models.filter(entry => + entry.metadata?.targetChatSessionType === sessionType && + entry.metadata?.isUserSelectable && + isModelSupportedForMode(entry, currentModeKind) && + isModelSupportedForInlineChat(entry, location, isInlineChatV2Enabled) + ); + } + + return models.filter(entry => + !entry.metadata?.targetChatSessionType && + entry.metadata?.isUserSelectable && + isModelSupportedForMode(entry, currentModeKind) && + isModelSupportedForInlineChat(entry, location, isInlineChatV2Enabled) + ); +} + +/** + * Check if a model is suitable for the current chat mode (e.g., agent mode requires tool calling). + */ +export function isModelSupportedForMode( + model: ILanguageModelChatMetadataAndIdentifier, + currentModeKind: ChatModeKind, +): boolean { + if (currentModeKind === ChatModeKind.Agent) { + return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata); + } + return true; +} + +/** + * Check if a model is suitable for inline chat (editor inline) usage. + */ +export function isModelSupportedForInlineChat( + model: ILanguageModelChatMetadataAndIdentifier, + location: ChatAgentLocation, + isInlineChatV2Enabled: boolean, +): boolean { + if (location !== ChatAgentLocation.EditorInline || !isInlineChatV2Enabled) { + return true; + } + return !!model.metadata.capabilities?.toolCalling; +} + +/** + * Check if any models in the pool target a specific session type. + */ +export function hasModelsTargetingSession( + allModels: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, +): boolean { + if (!sessionType) { + return false; + } + return allModels.some(m => m.metadata.targetChatSessionType === sessionType); +} + +/** + * Check if a model is valid for the current session's model pool. + * If the session has targeted models, the model must target that session type. + * If no models target this session, the model must not be session-specific. + */ +export function isModelValidForSession( + model: ILanguageModelChatMetadataAndIdentifier, + allModels: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, +): boolean { + if (hasModelsTargetingSession(allModels, sessionType)) { + return model.metadata.targetChatSessionType === sessionType; + } + return !model.metadata.targetChatSessionType; +} + +/** + * Find the default model for a given location from a list of models. + * Prefers the model marked as default for the location, falls back to the first model. + */ +export function findDefaultModel( + models: ILanguageModelChatMetadataAndIdentifier[], + location: ChatAgentLocation, +): ILanguageModelChatMetadataAndIdentifier | undefined { + return models.find(m => m.metadata.isDefaultForLocation[location]) || models[0]; +} + +/** + * Determine whether a persisted model selection should be restored. + * + * A persisted model should be restored if: + * 1. The model still exists in the available models list + * 2. Either the model wasn't the default at the time it was persisted, + * OR it is currently the default for the location + * + * This prevents scenarios where a user's explicit model choice gets overridden + * when the default model changes, while still tracking default model changes + * for users who never explicitly chose a model. + */ +export function shouldRestorePersistedModel( + persistedModelId: string, + persistedAsDefault: boolean, + availableModels: ILanguageModelChatMetadataAndIdentifier[], + location: ChatAgentLocation, +): { shouldRestore: boolean; model: ILanguageModelChatMetadataAndIdentifier | undefined } { + const model = availableModels.find(m => m.identifier === persistedModelId); + if (!model) { + return { shouldRestore: false, model: undefined }; + } + + if (!persistedAsDefault || model.metadata.isDefaultForLocation[location]) { + return { shouldRestore: true, model }; + } + + return { shouldRestore: false, model }; +} + +/** + * Determines whether the current model should be reset because it is no longer + * compatible with the current mode, session, or availability. + * + * Returns true if the model should be reset to default. + */ +export function shouldResetModelToDefault( + currentModel: ILanguageModelChatMetadataAndIdentifier | undefined, + availableModels: ILanguageModelChatMetadataAndIdentifier[], + context: IModelSelectionContext, + allModels: ILanguageModelChatMetadataAndIdentifier[], +): boolean { + if (!currentModel) { + return true; + } + + // Model is no longer in the available list + if (!availableModels.some(m => m.identifier === currentModel.identifier)) { + return true; + } + + // Model not supported for current mode + if (!isModelSupportedForMode(currentModel, context.currentModeKind)) { + return true; + } + + // Model not supported for inline chat + if (!isModelSupportedForInlineChat(currentModel, context.location, context.isInlineChatV2Enabled)) { + return true; + } + + // Model not valid for current session + if (!isModelValidForSession(currentModel, allModels, context.sessionType)) { + return true; + } + + return false; +} + +/** + * Determines whether a model from a sync state should be applied to the current view. + * + * Returns an action: + * - `'keep'` - the view already has the same model; no change needed. + * - `'apply'` - the state model is valid; the caller should switch to it. + * - `'default'` - the state model is incompatible (wrong session pool, unsupported + * mode, or missing inline-chat capability); the caller should fall + * back to the default model for the current location. + * + * @param context Optional because some callers (e.g. unit tests, or code paths + * that only care about session-pool validation) don't have a full UI context + * available. When omitted, mode and inline-chat checks are skipped and only + * session-pool membership is validated. + */ +export function resolveModelFromSyncState( + stateModel: ILanguageModelChatMetadataAndIdentifier, + currentModel: ILanguageModelChatMetadataAndIdentifier | undefined, + allModels: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, + context?: IModelSelectionContext, +): { action: 'keep' | 'apply' | 'default' } { + // Already the same model — nothing to do + if (currentModel && currentModel.identifier === stateModel.identifier) { + return { action: 'keep' }; + } + + // Validate the state model belongs to this session's model pool + if (!isModelValidForSession(stateModel, allModels, sessionType)) { + return { action: 'default' }; + } + + // When a UI context is available, also validate mode and inline-chat compatibility + if (context) { + if (!isModelSupportedForMode(stateModel, context.currentModeKind)) { + return { action: 'default' }; + } + if (!isModelSupportedForInlineChat(stateModel, context.location, context.isInlineChatV2Enabled)) { + return { action: 'default' }; + } + } + + return { action: 'apply' }; +} + +/** + * Merges live models with cached models per-vendor. + * For vendors whose models have resolved, uses live data. + * For vendors that are contributed but haven't resolved yet (startup race), keeps cached models. + * Vendors no longer contributed are evicted from cache. + */ +export function mergeModelsWithCache( + liveModels: ILanguageModelChatMetadataAndIdentifier[], + cachedModels: ILanguageModelChatMetadataAndIdentifier[], + contributedVendors: Set, +): ILanguageModelChatMetadataAndIdentifier[] { + if (liveModels.length > 0) { + const liveVendors = new Set(liveModels.map(m => m.metadata.vendor)); + return [ + ...liveModels, + ...cachedModels.filter(m => !liveVendors.has(m.metadata.vendor) && contributedVendors.has(m.metadata.vendor)), + ]; + } + return cachedModels; +} + +/** + * Determines whether the currently selected model should be reset to default + * when the language model list changes. + * + * Returns true if the model should be reset to default (i.e., the selected model + * is no longer in the available models list). + */ +export function shouldResetOnModelListChange( + currentModelId: string | undefined, + availableModels: ILanguageModelChatMetadataAndIdentifier[], +): boolean { + if (!currentModelId) { + return true; + } + return !availableModels.some(m => m.identifier === currentModelId); +} + +/** + * Determines whether a late-arriving persisted model should be restored. + * This handles the startup race where the model wasn't available during + * `initSelectedModel` but arrives later via `onDidChangeLanguageModels`. + * + * The model must pass both the persisted-default check and the `isUserSelectable` check. + */ +export function shouldRestoreLateArrivingModel( + persistedModelId: string, + persistedAsDefault: boolean, + model: ILanguageModelChatMetadataAndIdentifier, + location: ChatAgentLocation, +): boolean { + if (!model.metadata.isUserSelectable) { + return false; + } + const result = shouldRestorePersistedModel( + persistedModelId, + persistedAsDefault, + [model], + location, + ); + return result.shouldRestore; +} diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts new file mode 100644 index 00000000000..d282483d7d7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts @@ -0,0 +1,1548 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../../common/constants.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../../../../common/languageModels.js'; +import { + filterModelsForSession, + findDefaultModel, + hasModelsTargetingSession, + isModelSupportedForInlineChat, + isModelSupportedForMode, + isModelValidForSession, + mergeModelsWithCache, + resolveModelFromSyncState, + shouldResetModelToDefault, + shouldResetOnModelListChange, + shouldRestoreLateArrivingModel, + shouldRestorePersistedModel, +} from '../../../../browser/widget/input/chatModelSelectionLogic.js'; + +/** + * Test helper that composes the full startup pipeline: merge live+cache → sort → filter by session/mode. + * This mirrors what `chatInputPart.getModels()` does, but without the storage side effects. + */ +function computeAvailableModels( + liveModels: ILanguageModelChatMetadataAndIdentifier[], + cachedModels: ILanguageModelChatMetadataAndIdentifier[], + contributedVendors: Set, + sessionType: string | undefined, + currentModeKind: ChatModeKind, + location: ChatAgentLocation, + isInlineChatV2Enabled: boolean, +): ILanguageModelChatMetadataAndIdentifier[] { + const merged = mergeModelsWithCache(liveModels, cachedModels, contributedVendors); + merged.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + return filterModelsForSession(merged, sessionType, currentModeKind, location, isInlineChatV2Enabled); +} + +function createModel( + id: string, + name: string, + overrides?: Partial, +): ILanguageModelChatMetadataAndIdentifier { + return { + identifier: `copilot/${id}`, + metadata: { + extension: new ExtensionIdentifier('test.ext'), + id, + name, + vendor: 'copilot', + version: '1.0', + family: 'copilot', + maxInputTokens: 128000, + maxOutputTokens: 4096, + isDefaultForLocation: {}, + isUserSelectable: true, + modelPickerCategory: undefined, + capabilities: { toolCalling: true, agentMode: true }, + ...overrides, + } as ILanguageModelChatMetadata, + }; +} + +function createDefaultModelForLocation( + id: string, + name: string, + location: ChatAgentLocation, + overrides?: Partial, +): ILanguageModelChatMetadataAndIdentifier { + return createModel(id, name, { + isDefaultForLocation: { [location]: true }, + ...overrides, + }); +} + +function createSessionModel( + id: string, + name: string, + sessionType: string, + overrides?: Partial, +): ILanguageModelChatMetadataAndIdentifier { + return createModel(id, name, { + targetChatSessionType: sessionType, + ...overrides, + }); +} + +suite('ChatModelSelectionLogic', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('isModelSupportedForMode', () => { + + test('any model is supported in Ask mode', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Ask), true); + }); + + test('any model is supported in Edit mode', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Edit), true); + }); + + test('model with tool calling and agent mode is supported in Agent mode', () => { + const model = createModel('agent-capable', 'Agent-Capable', { + capabilities: { toolCalling: true, agentMode: true }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), true); + }); + + test('model with tool calling but agentMode=undefined is supported in Agent mode', () => { + const model = createModel('tool-only', 'Tool-Only', { + capabilities: { toolCalling: true }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), true); + }); + + test('model without tool calling is NOT supported in Agent mode', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), false); + }); + + test('model with agentMode=false is NOT supported in Agent mode', () => { + const model = createModel('no-agent', 'No-Agent', { + capabilities: { toolCalling: true, agentMode: false }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), false); + }); + + test('model with no capabilities is NOT supported in Agent mode', () => { + const model = createModel('no-caps', 'No-Caps', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), false); + }); + }); + + suite('isModelSupportedForInlineChat', () => { + + test('any model is supported when not in EditorInline location', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.Chat, true), true); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.Terminal, true), true); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.Notebook, true), true); + }); + + test('any model is supported in EditorInline when V2 is disabled', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, false), true); + }); + + test('model with tool calling is supported in EditorInline with V2', () => { + const model = createModel('tools', 'Tools', { + capabilities: { toolCalling: true }, + }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, true), true); + }); + + test('model without tool calling is NOT supported in EditorInline with V2', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, true), false); + }); + + test('model with no capabilities is NOT supported in EditorInline with V2', () => { + const model = createModel('no-caps', 'No-Caps', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, true), false); + }); + }); + + suite('filterModelsForSession', () => { + + const gpt4o = createModel('gpt-4o', 'GPT-4o'); + const claude = createModel('claude', 'Claude'); + const notSelectable = createModel('hidden', 'Hidden', { isUserSelectable: false }); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const noToolsModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + + test('returns user-selectable general models when no session type set', () => { + const result = filterModelsForSession( + [gpt4o, claude, notSelectable], + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('returns user-selectable general models for local session type', () => { + const result = filterModelsForSession( + [gpt4o, claude, notSelectable], + 'local', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('excludes models targeting a specific session type when in general session', () => { + const result = filterModelsForSession( + [gpt4o, claude, cloudModel], + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('returns only session-targeted models for a specific session type', () => { + const result = filterModelsForSession( + [gpt4o, claude, cloudModel], + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud-gpt']); + }); + + test('filters out models incompatible with Agent mode in general session', () => { + const result = filterModelsForSession( + [gpt4o, noToolsModel], + undefined, + ChatModeKind.Agent, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o']); + }); + + test('filters by mode for session-targeted models', () => { + const cloudNoTools = createSessionModel('cloud-basic', 'Cloud Basic', 'cloud', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const result = filterModelsForSession( + [gpt4o, cloudModel, cloudNoTools], + 'cloud', + ChatModeKind.Agent, + ChatAgentLocation.Chat, + false, + ); + // Session-type filtering also checks mode and inline chat support + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud-gpt']); + }); + + test('excludes non-selectable models from session-targeted results', () => { + const cloudHidden = createSessionModel('cloud-hidden', 'Cloud Hidden', 'cloud', { + isUserSelectable: false, + }); + const result = filterModelsForSession( + [cloudModel, cloudHidden], + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud-gpt']); + }); + + test('falls back to general models when no models target the session type', () => { + const result = filterModelsForSession( + [gpt4o, claude], + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('filters inline chat incompatible models in EditorInline with V2', () => { + const noToolsSelectable = createModel('no-tools-selectable', 'No-Tools-Selectable', { + capabilities: { toolCalling: false }, + }); + const result = filterModelsForSession( + [gpt4o, noToolsSelectable], + undefined, + ChatModeKind.Ask, + ChatAgentLocation.EditorInline, + true, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o']); + }); + }); + + suite('hasModelsTargetingSession', () => { + + test('returns false when session type is undefined', () => { + const models = [createModel('gpt', 'GPT')]; + assert.strictEqual(hasModelsTargetingSession(models, undefined), false); + }); + + test('returns false when no models target the session type', () => { + const models = [createModel('gpt', 'GPT')]; + assert.strictEqual(hasModelsTargetingSession(models, 'cloud'), false); + }); + + test('returns true when a model targets the session type', () => { + const models = [ + createModel('gpt', 'GPT'), + createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'), + ]; + assert.strictEqual(hasModelsTargetingSession(models, 'cloud'), true); + }); + + test('returns false for different session type', () => { + const models = [createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud')]; + assert.strictEqual(hasModelsTargetingSession(models, 'enterprise'), false); + }); + }); + + suite('isModelValidForSession', () => { + + test('general model is valid when no models target the session', () => { + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel]; + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'cloud'), true); + }); + + test('session-targeted model is NOT valid when no models target the session type in pool', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + assert.strictEqual(isModelValidForSession(sessionModel, [generalModel], undefined), false); + }); + + test('session-targeted model IS valid when pool has models targeting that session', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [createModel('gpt', 'GPT'), sessionModel]; + assert.strictEqual(isModelValidForSession(sessionModel, allModels, 'cloud'), true); + }); + + test('general model is NOT valid when pool has models targeting the session', () => { + const generalModel = createModel('gpt', 'GPT'); + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [generalModel, sessionModel]; + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'cloud'), false); + }); + + test('model targeting wrong session is NOT valid', () => { + const wrongSessionModel = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [wrongSessionModel, cloudModel]; + assert.strictEqual(isModelValidForSession(wrongSessionModel, allModels, 'cloud'), false); + }); + + test('general model is valid when session type is undefined', () => { + const generalModel = createModel('gpt', 'GPT'); + assert.strictEqual(isModelValidForSession(generalModel, [generalModel], undefined), true); + }); + }); + + suite('findDefaultModel', () => { + + test('returns model marked as default for location', () => { + const regular = createModel('gpt', 'GPT'); + const defaultModel = createDefaultModelForLocation('claude', 'Claude', ChatAgentLocation.Chat); + const result = findDefaultModel([regular, defaultModel], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 'claude'); + }); + + test('falls back to first model when no default for location', () => { + const modelA = createModel('gpt', 'GPT'); + const modelB = createModel('claude', 'Claude'); + const result = findDefaultModel([modelA, modelB], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 'gpt'); + }); + + test('returns undefined for empty models array', () => { + const result = findDefaultModel([], ChatAgentLocation.Chat); + assert.strictEqual(result, undefined); + }); + + test('returns location-specific default when multiple defaults exist', () => { + const chatDefault = createDefaultModelForLocation('chat-default', 'Chat Default', ChatAgentLocation.Chat); + const terminalDefault = createDefaultModelForLocation('terminal-default', 'Terminal Default', ChatAgentLocation.Terminal); + const result = findDefaultModel([chatDefault, terminalDefault], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 'chat-default'); + }); + + test('does not pick terminal default when looking for chat default', () => { + const terminalDefault = createDefaultModelForLocation('terminal-default', 'Terminal Default', ChatAgentLocation.Terminal); + const regular = createModel('gpt', 'GPT'); + const result = findDefaultModel([terminalDefault, regular], ChatAgentLocation.Chat); + // Falls back to first model since none is default for Chat + assert.strictEqual(result?.metadata.id, 'terminal-default'); + }); + }); + + suite('shouldRestorePersistedModel', () => { + + test('restores model that was explicitly chosen (not default)', () => { + const model = createModel('gpt', 'GPT'); + const result = shouldRestorePersistedModel('copilot/gpt', false, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + assert.strictEqual(result.model?.identifier, 'copilot/gpt'); + }); + + test('restores model that was default and is still default', () => { + const model = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt', true, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + }); + + test('does NOT restore model that was default but is no longer default', () => { + const model = createModel('gpt', 'GPT'); + const result = shouldRestorePersistedModel('copilot/gpt', true, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + assert.strictEqual(result.model?.identifier, 'copilot/gpt'); + }); + + test('does NOT restore model that no longer exists', () => { + const otherModel = createModel('claude', 'Claude'); + const result = shouldRestorePersistedModel('copilot/gpt', false, [otherModel], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + assert.strictEqual(result.model, undefined); + }); + + test('handles empty models list', () => { + const result = shouldRestorePersistedModel('copilot/gpt', false, [], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + assert.strictEqual(result.model, undefined); + }); + + test('user choice is preserved when default changes to a different model', () => { + // User explicitly chose GPT-4o, default used to be Claude, now default is something else + const gpt = createModel('gpt-4o', 'GPT-4o'); + const claude = createModel('claude', 'Claude'); + const result = shouldRestorePersistedModel('copilot/gpt-4o', false, [gpt, claude], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + assert.strictEqual(result.model?.metadata.id, 'gpt-4o'); + }); + + test('default tracking: follows new default when user never explicitly chose', () => { + // Old default was GPT-4o (persisted as default), now Claude is the default + const gpt = createModel('gpt-4o', 'GPT-4o'); + const claude = createDefaultModelForLocation('claude', 'Claude', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt-4o', true, [gpt, claude], ChatAgentLocation.Chat); + // Should NOT restore because GPT-4o is no longer default and was stored as default + assert.strictEqual(result.shouldRestore, false); + }); + }); + + suite('shouldResetModelToDefault', () => { + + const defaultContext = { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }; + + test('should reset when current model is undefined', () => { + assert.strictEqual(shouldResetModelToDefault(undefined, [], defaultContext, []), true); + }); + + test('should reset when model is no longer available', () => { + const model = createModel('gpt', 'GPT'); + assert.strictEqual(shouldResetModelToDefault(model, [], defaultContext, [model]), true); + }); + + test('should NOT reset when model is available and compatible', () => { + const model = createModel('gpt', 'GPT'); + assert.strictEqual(shouldResetModelToDefault(model, [model], defaultContext, [model]), false); + }); + + test('should reset when model is not supported for current mode', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const context = { ...defaultContext, currentModeKind: ChatModeKind.Agent }; + assert.strictEqual(shouldResetModelToDefault(model, [model], context, [model]), true); + }); + + test('should reset when model is not supported for inline chat', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + const context = { + ...defaultContext, + location: ChatAgentLocation.EditorInline, + isInlineChatV2Enabled: true, + }; + assert.strictEqual(shouldResetModelToDefault(model, [model], context, [model]), true); + }); + + test('should reset when model is not valid for session', () => { + const generalModel = createModel('gpt', 'GPT'); + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [generalModel, sessionModel]; + const context = { ...defaultContext, sessionType: 'cloud' }; + assert.strictEqual(shouldResetModelToDefault(generalModel, [generalModel], context, allModels), true); + }); + + test('should NOT reset session model in matching session', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const context = { ...defaultContext, sessionType: 'cloud' }; + assert.strictEqual(shouldResetModelToDefault(sessionModel, [sessionModel], context, [sessionModel]), false); + }); + }); + + suite('resolveModelFromSyncState', () => { + + test('keeps current model when same as state model', () => { + const model = createModel('gpt', 'GPT'); + const result = resolveModelFromSyncState(model, model, [model], undefined); + assert.strictEqual(result.action, 'keep'); + }); + + test('applies state model when different and valid', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('claude', 'Claude'); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined); + assert.strictEqual(result.action, 'apply'); + }); + + test('uses default when state model not valid for session', () => { + const current = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const stateModel = createModel('gpt', 'GPT'); // general model, not valid for cloud session + const allModels = [current, stateModel]; + const result = resolveModelFromSyncState(stateModel, current, allModels, 'cloud'); + assert.strictEqual(result.action, 'default'); + }); + + test('applies when current model is undefined', () => { + const stateModel = createModel('gpt', 'GPT'); + const result = resolveModelFromSyncState(stateModel, undefined, [stateModel], undefined); + assert.strictEqual(result.action, 'apply'); + }); + + test('applies session model when valid for matching session', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, sessionModel]; + const result = resolveModelFromSyncState(sessionModel, generalModel, allModels, 'cloud'); + assert.strictEqual(result.action, 'apply'); + }); + + test('returns default when state model does not support current mode', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }); + assert.strictEqual(result.action, 'default'); + }); + + test('returns default when state model does not support inline chat V2', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined, { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: true, + sessionType: undefined, + }); + assert.strictEqual(result.action, 'default'); + }); + + test('applies when state model supports current mode with context', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('agent-model', 'Agent Model', { + capabilities: { toolCalling: true, agentMode: true }, + }); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }); + assert.strictEqual(result.action, 'apply'); + }); + }); + + suite('mergeModelsWithCache', () => { + + test('uses live models when available', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedModel = createModel('cached-gpt', 'Cached GPT'); + const result = mergeModelsWithCache([liveModel], [cachedModel], new Set(['copilot'])); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'gpt'); + }); + + test('falls back to cached models when no live models', () => { + const cachedModel = createModel('cached-gpt', 'Cached GPT'); + const result = mergeModelsWithCache([], [cachedModel], new Set(['copilot'])); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'cached-gpt'); + }); + + test('merges cached models from vendors not yet resolved', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedOtherVendor = createModel('other-model', 'Other Model', { vendor: 'other-vendor' }); + const result = mergeModelsWithCache( + [liveModel], + [cachedOtherVendor], + new Set(['copilot', 'other-vendor']), + ); + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result.map(m => m.metadata.id).sort(), ['gpt', 'other-model']); + }); + + test('evicts cached models from vendors no longer contributed', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedRemovedVendor = createModel('removed-model', 'Removed Model', { vendor: 'removed-vendor' }); + const result = mergeModelsWithCache( + [liveModel], + [cachedRemovedVendor], + new Set(['copilot']), // removed-vendor is NOT contributed + ); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'gpt'); + }); + + test('does not duplicate models from same vendor', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedSameVendor = createModel('cached-gpt', 'Cached GPT'); + const result = mergeModelsWithCache( + [liveModel], + [cachedSameVendor], + new Set(['copilot']), + ); + // Both are vendor 'copilot', live vendor takes priority + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'gpt'); + }); + + test('handles empty cache and empty live models', () => { + const result = mergeModelsWithCache([], [], new Set()); + assert.deepStrictEqual(result, []); + }); + + test('handles multiple vendors with partial resolution', () => { + const liveA = createModel('a-model', 'A Model', { vendor: 'vendor-a' }); + const cachedB = createModel('b-model', 'B Model', { vendor: 'vendor-b' }); + const cachedC = createModel('c-model', 'C Model', { vendor: 'vendor-c' }); + const result = mergeModelsWithCache( + [liveA], + [cachedB, cachedC], + new Set(['vendor-a', 'vendor-b']), // vendor-c not contributed + ); + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result.map(m => m.metadata.vendor).sort(), ['vendor-a', 'vendor-b']); + }); + }); + + suite('model switching scenarios', () => { + + test('switching from Ask to Agent mode should reset model without tool support', () => { + const noToolsModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const toolModel = createModel('tool-model', 'Tool Model'); + const allModels = [noToolsModel, toolModel]; + + // In Ask mode, model is fine + assert.strictEqual( + shouldResetModelToDefault(noToolsModel, allModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), + false, + ); + + // After switching to Agent mode, model should be reset + assert.strictEqual( + shouldResetModelToDefault(noToolsModel, allModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), + true, + ); + }); + + test('switching sessions should reject model from wrong session pool', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // Cloud model is valid in cloud session + assert.strictEqual( + isModelValidForSession(cloudModel, allModels, 'cloud'), + true, + ); + + // Cloud model is NOT valid in general session (no session type) + assert.strictEqual( + isModelValidForSession(cloudModel, allModels, undefined), + false, + ); + + // General model is NOT valid in cloud session (when cloud models exist) + assert.strictEqual( + isModelValidForSession(generalModel, allModels, 'cloud'), + false, + ); + + // General model IS valid in general session + assert.strictEqual( + isModelValidForSession(generalModel, allModels, undefined), + true, + ); + }); + + test('model removal should trigger reset', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Initially both available, GPT is selected + assert.strictEqual( + shouldResetModelToDefault(gpt, [gpt, claude], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, [gpt, claude]), + false, + ); + + // GPT is removed from available models + assert.strictEqual( + shouldResetModelToDefault(gpt, [claude], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, [claude]), + true, + ); + }); + + test('syncing model from state respects session boundaries', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // State has a cloud model, but we are in a general session + const result = resolveModelFromSyncState(cloudModel, generalModel, allModels, undefined); + assert.strictEqual(result.action, 'default'); + }); + + test('syncing model from state applies model when switching to matching session', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // State has a cloud model and we are in a cloud session + const result = resolveModelFromSyncState(cloudModel, generalModel, allModels, 'cloud'); + assert.strictEqual(result.action, 'apply'); + }); + + test('persisted model selection survives when model is still default', () => { + const model = createDefaultModelForLocation('gpt-4o', 'GPT-4o', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt-4o', true, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + }); + + test('persisted model selection does NOT restore when a new default is assigned', () => { + // GPT-4o was the old default (persisted as default=true), but it's no longer default + const gpt4o = createModel('gpt-4o', 'GPT-4o'); + const newDefault = createDefaultModelForLocation('claude', 'Claude', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt-4o', true, [gpt4o, newDefault], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + }); + + test('user explicit model choice persists even when default changes', () => { + // User explicitly picked Claude (persistedAsDefault=false), default was GPT-4o + // Now default switches to something else — Claude should still be restored + const claude = createModel('claude', 'Claude'); + const newDefault = createDefaultModelForLocation('new-model', 'New Model', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/claude', false, [claude, newDefault], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + assert.strictEqual(result.model?.metadata.id, 'claude'); + }); + + test('combining mode switch + session switch validates correctly', () => { + const cloudToolModel = createSessionModel('cloud-tool', 'Cloud Tool', 'cloud', { + capabilities: { toolCalling: true, agentMode: true }, + }); + const cloudNoToolModel = createSessionModel('cloud-basic', 'Cloud Basic', 'cloud', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const allCloudModels = [cloudToolModel, cloudNoToolModel]; + + // In cloud session, Agent mode — tool model is valid + assert.strictEqual( + shouldResetModelToDefault(cloudToolModel, allCloudModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allCloudModels), + false, + ); + + // The no-tool model should be reset in Agent mode + // Both filterModelsForSession and shouldResetModelToDefault enforce mode support + assert.strictEqual( + shouldResetModelToDefault(cloudNoToolModel, allCloudModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allCloudModels), + true, + ); + }); + }); + + suite('onDidChangeLanguageModels race conditions', () => { + + test('model temporarily removed then re-added loses user choice', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Step 1: User has GPT selected, both models available + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [gpt, claude]), false); + + // Step 2: Extension reloads, GPT temporarily disappears from model list + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [claude]), true); + // → ChatInputPart resets to default (Claude) + + // Step 3: GPT comes back — but the handler just checks if current is still valid. + // By now the current is Claude (from step 2), so it stays. + assert.strictEqual(shouldResetOnModelListChange('copilot/claude', [gpt, claude]), false); + // → User's original GPT choice is lost! This is the "random switch" bug pattern. + }); + + test('model stays when model list refreshes with it still present', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Model list refreshes but GPT is still there + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [gpt, claude]), false); + }); + + test('reset when current model identifier is undefined', () => { + const gpt = createModel('gpt', 'GPT'); + assert.strictEqual(shouldResetOnModelListChange(undefined, [gpt]), true); + }); + + test('reset when models list is empty', () => { + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', []), true); + }); + + test('cache bridges the gap when live models temporarily unavailable', () => { + const cachedGpt = createModel('gpt', 'GPT'); + const cachedClaude = createModel('claude', 'Claude'); + + // Step 1: Extension unloaded, no live models. Cache fills the gap. + const merged = mergeModelsWithCache([], [cachedGpt, cachedClaude], new Set(['copilot'])); + assert.strictEqual(merged.length, 2); + + // Selected model is still found in the cached list + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', merged), false); + }); + + test('cache kept even for uncontributed vendors when no live models exist', () => { + const cachedGpt = createModel('gpt', 'GPT'); + + // When liveModels is empty, mergeModelsWithCache returns ALL cached + // because it can't distinguish "startup not ready" from "vendor removed" + const merged = mergeModelsWithCache([], [cachedGpt], new Set()); + assert.strictEqual(merged.length, 1); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', merged), false); + }); + + test('cache evicted for uncontributed vendor once live models arrive', () => { + const cachedGpt = createModel('gpt', 'GPT'); + const liveOther = createModel('other', 'Other', { vendor: 'other-vendor' }); + + // Once live models exist, the vendor filter kicks in + const merged = mergeModelsWithCache([liveOther], [cachedGpt], new Set(['other-vendor'])); + assert.strictEqual(merged.length, 1); + assert.strictEqual(merged[0].metadata.id, 'other'); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', merged), true); + }); + }); + + suite('late-arriving model restoration', () => { + + test('restores explicitly-chosen model that arrives late', () => { + const model = createModel('gpt', 'GPT'); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', false, model, ChatAgentLocation.Chat), + true, + ); + }); + + test('restores model that was default and is still default for location', () => { + const model = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Chat); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Chat), + true, + ); + }); + + test('does NOT restore model that was default but is no longer default', () => { + const model = createModel('gpt', 'GPT'); // not default for any location + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Chat), + false, + ); + }); + + test('does NOT restore model that is not user-selectable', () => { + const model = createModel('internal', 'Internal', { isUserSelectable: false }); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/internal', false, model, ChatAgentLocation.Chat), + false, + ); + }); + + test('does NOT restore model with isUserSelectable=undefined (treated as falsy)', () => { + const model = createModel('undef-sel', 'Undef-Sel', { isUserSelectable: undefined }); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/undef-sel', false, model, ChatAgentLocation.Chat), + false, + ); + }); + + test('restores model arriving late at a different location where it is default', () => { + const model = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Terminal); + // User is in Terminal — model is default there + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Terminal), + true, + ); + // But not in Chat + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Chat), + false, + ); + }); + }); + + suite('full startup pipeline (computeAvailableModels)', () => { + + test('startup with only cached models returns filtered cache', () => { + const cached = createModel('gpt', 'GPT'); + const result = computeAvailableModels( + [], // no live models yet + [cached], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt']); + }); + + test('startup with cached models from removed vendor still returns them (no live to compare)', () => { + const cached = createModel('gpt', 'GPT'); + // When liveModels is empty, mergeModelsWithCache returns ALL cached + // because it cannot tell startup-delay from vendor removal + const result = computeAvailableModels( + [], // no live models + [cached], + new Set(), // vendor no longer contributed + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt']); + }); + + test('live models supersede cached models from same vendor', () => { + const live = createModel('gpt-new', 'GPT New'); + const cached = createModel('gpt-old', 'GPT Old'); + const result = computeAvailableModels( + [live], + [cached], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-new']); + }); + + test('partial vendor resolution keeps unresolved vendors from cache', () => { + const liveA = createModel('a-model', 'A Model', { vendor: 'vendor-a' }); + const cachedB = createModel('b-model', 'B Model', { vendor: 'vendor-b' }); + const result = computeAvailableModels( + [liveA], + [cachedB], + new Set(['vendor-a', 'vendor-b']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id).sort(), ['a-model', 'b-model']); + }); + + test('results are sorted alphabetically by name', () => { + const modelC = createModel('c', 'Charlie'); + const modelA = createModel('a', 'Alpha'); + const modelB = createModel('b', 'Bravo'); + const result = computeAvailableModels( + [modelC, modelA, modelB], + [], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.name), ['Alpha', 'Bravo', 'Charlie']); + }); + + test('session-targeted models excluded from general session startup', () => { + const general = createModel('gpt', 'GPT'); + const cloudOnly = createSessionModel('cloud', 'Cloud', 'cloud'); + const result = computeAvailableModels( + [general, cloudOnly], + [], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt']); + }); + + test('only session-targeted models returned for cloud session startup', () => { + const general = createModel('gpt', 'GPT'); + const cloudOnly = createSessionModel('cloud', 'Cloud', 'cloud'); + const result = computeAvailableModels( + [general, cloudOnly], + [], + new Set(['copilot']), + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud']); + }); + + test('agent mode filters non-tool models during startup', () => { + const toolModel = createModel('tool', 'Tool Model'); + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const result = computeAvailableModels( + [toolModel, noToolModel], + [], + new Set(['copilot']), + undefined, + ChatModeKind.Agent, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['tool']); + }); + }); + + suite('_syncFromModel edge cases', () => { + + test('sync state with undefined selectedModel keeps current', () => { + const current = createModel('gpt', 'GPT'); + // When state has no selectedModel, _syncFromModel skips the model sync + // (the code checks `if (state?.selectedModel)`) + // This means the current model stays — test that resolveModelFromSyncState + // correctly identifies "keep" for same model + const result = resolveModelFromSyncState(current, current, [current], undefined); + assert.strictEqual(result.action, 'keep'); + }); + + test('sync state model from different session does not apply', () => { + // Scenario: User is in session A with cloud model, switches to session B (general) + // Session B's state still has the cloud model reference + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + const result = resolveModelFromSyncState(cloudModel, generalModel, allModels, undefined); + assert.strictEqual(result.action, 'default'); + }); + + test('sync state with model matching different session type falls back to default', () => { + const enterpriseModel = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [cloudModel, enterpriseModel]; + + // State has enterprise model, but we're in cloud session + const result = resolveModelFromSyncState(enterpriseModel, cloudModel, allModels, 'cloud'); + assert.strictEqual(result.action, 'default'); + }); + + test('sync identical model reference returns keep', () => { + const model = createModel('gpt', 'GPT'); + // Same object reference + const result = resolveModelFromSyncState(model, model, [model], undefined); + assert.strictEqual(result.action, 'keep'); + }); + + test('sync same identifier but different object returns keep', () => { + const model1 = createModel('gpt', 'GPT'); + const model2 = createModel('gpt', 'GPT'); + // Different objects, same identifier + const result = resolveModelFromSyncState(model1, model2, [model1, model2], undefined); + assert.strictEqual(result.action, 'keep'); + }); + }); + + suite('checkModelSupported interaction patterns', () => { + + const askContext = { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }; + + const agentContext = { + ...askContext, + currentModeKind: ChatModeKind.Agent, + }; + + test('initSelectedModel → checkModelSupported: restored model passes Agent check', () => { + const agentModel = createModel('agent-model', 'Agent Model', { + capabilities: { toolCalling: true, agentMode: true }, + }); + + // 1. shouldRestorePersistedModel says "restore" + const restoreResult = shouldRestorePersistedModel('copilot/agent-model', false, [agentModel], ChatAgentLocation.Chat); + assert.strictEqual(restoreResult.shouldRestore, true); + + // 2. Immediately after, checkModelSupported runs with Agent mode + assert.strictEqual(shouldResetModelToDefault(agentModel, [agentModel], agentContext, [agentModel]), false); + }); + + test('initSelectedModel → checkModelSupported: restored model FAILS Agent check', () => { + const askOnlyModel = createModel('ask-only', 'Ask Only', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const agentModel = createModel('agent-model', 'Agent Model'); + + // 1. shouldRestorePersistedModel says "restore" + const restoreResult = shouldRestorePersistedModel('copilot/ask-only', false, [askOnlyModel, agentModel], ChatAgentLocation.Chat); + assert.strictEqual(restoreResult.shouldRestore, true); + + // 2. checkModelSupported runs with Agent mode → should reset + assert.strictEqual(shouldResetModelToDefault(askOnlyModel, [askOnlyModel, agentModel], agentContext, [askOnlyModel, agentModel]), true); + + // 3. findDefaultModel picks replacement from models filtered for Agent mode + const agentCompatibleModels = filterModelsForSession( + [askOnlyModel, agentModel], undefined, ChatModeKind.Agent, ChatAgentLocation.Chat, false + ); + const defaultModel = findDefaultModel(agentCompatibleModels, ChatAgentLocation.Chat); + assert.strictEqual(defaultModel?.metadata.id, 'agent-model'); + }); + + test('mode switch triggers checkModelSupported which resets incompatible model', () => { + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: { toolCalling: false }, + }); + const toolModel = createModel('tool', 'Tool'); + + // In Ask mode: fine + assert.strictEqual(shouldResetModelToDefault(noToolModel, [noToolModel, toolModel], askContext, [noToolModel, toolModel]), false); + + // Switch to Agent mode: not fine + assert.strictEqual(shouldResetModelToDefault(noToolModel, [noToolModel, toolModel], agentContext, [noToolModel, toolModel]), true); + }); + + test('double reset is idempotent', () => { + const defaultModel = createDefaultModelForLocation('default', 'Default', ChatAgentLocation.Chat); + const otherModel = createModel('other', 'Other'); + const allModels = [defaultModel, otherModel]; + + // First reset: picks default + const result1 = findDefaultModel(allModels, ChatAgentLocation.Chat); + assert.strictEqual(result1?.metadata.id, 'default'); + + // "Second reset" — same call, same result + const result2 = findDefaultModel(allModels, ChatAgentLocation.Chat); + assert.strictEqual(result2?.metadata.id, 'default'); + + // Default model continues to pass validation + assert.strictEqual(shouldResetModelToDefault(result1!, allModels, askContext, allModels), false); + }); + }); + + suite('multiple session types and cross-contamination', () => { + + test('model from session A rejected in session B', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const enterpriseModel = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel, enterpriseModel]; + + // Cloud model not valid in enterprise session + assert.strictEqual(isModelValidForSession(cloudModel, allModels, 'enterprise'), false); + // Enterprise model not valid in cloud session + assert.strictEqual(isModelValidForSession(enterpriseModel, allModels, 'cloud'), false); + // General model not valid when session-targeted models exist + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'cloud'), false); + }); + + test('general model is valid when session type has no targeted models', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // 'enterprise' session has no targeted models + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'enterprise'), true); + }); + + test('filterModelsForSession isolates session types correctly', () => { + const general = createModel('gpt', 'GPT'); + const cloud = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const enterprise = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const allModels = [general, cloud, enterprise]; + + const cloudFiltered = filterModelsForSession(allModels, 'cloud', ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.deepStrictEqual(cloudFiltered.map(m => m.metadata.id), ['cloud-gpt']); + + const entFiltered = filterModelsForSession(allModels, 'enterprise', ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.deepStrictEqual(entFiltered.map(m => m.metadata.id), ['ent-gpt']); + + const generalFiltered = filterModelsForSession(allModels, undefined, ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.deepStrictEqual(generalFiltered.map(m => m.metadata.id), ['gpt']); + }); + + test('switching from cloud to general session resets cloud model', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // In cloud session, cloud model is valid + assert.strictEqual(shouldResetModelToDefault(cloudModel, [cloudModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allModels), false); + + // Switch to general session — cloud model should be reset + assert.strictEqual(shouldResetModelToDefault(cloudModel, [generalModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), true); + }); + }); + + suite('mode with forced model (mode.model property)', () => { + + test('mode forces model — simulating switchModelByQualifiedName success', () => { + const gpt = createModel('gpt-4o', 'GPT-4o'); + const claude = createModel('claude', 'Claude'); + const allModels = [gpt, claude]; + + // The autorun calls switchModelByQualifiedName which checks ILanguageModelChatMetadata.matchesQualifiedName + // Simulate: mode wants "GPT-4o (copilot)" + const qualifiedName = 'GPT-4o (copilot)'; + const match = allModels.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedName, m.metadata)); + assert.strictEqual(match?.metadata.id, 'gpt-4o'); + }); + + test('mode forces model — copilot vendor shorthand works', () => { + const gpt = createModel('gpt-4o', 'GPT-4o'); + // For copilot vendor, just the name works + const match = [gpt].find(m => ILanguageModelChatMetadata.matchesQualifiedName('GPT-4o', m.metadata)); + assert.strictEqual(match?.metadata.id, 'gpt-4o'); + }); + + test('mode forces model — nonexistent model gracefully misses', () => { + const gpt = createModel('gpt-4o', 'GPT-4o'); + const match = [gpt].find(m => ILanguageModelChatMetadata.matchesQualifiedName('NonExistent (copilot)', m.metadata)); + assert.strictEqual(match, undefined); + }); + + test('mode forces model that is then checked for support', () => { + // Mode forces a model, then checkModelSupported runs + const forcedModel = createModel('forced', 'Forced', { + capabilities: { toolCalling: false, agentMode: false }, + }); + + // Mode forced this model but we're in Agent mode — should be reset + assert.strictEqual(shouldResetModelToDefault(forcedModel, [forcedModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, [forcedModel]), true); + }); + }); + + suite('EditorInline + mode combined scenarios', () => { + + test('EditorInline + Agent + V2 requires both agentMode and toolCalling', () => { + const partialModel = createModel('partial', 'Partial', { + capabilities: { toolCalling: true, agentMode: false }, + }); + // Fails Agent mode check + assert.strictEqual(isModelSupportedForMode(partialModel, ChatModeKind.Agent), false); + // Passes inline chat check (has toolCalling) + assert.strictEqual(isModelSupportedForInlineChat(partialModel, ChatAgentLocation.EditorInline, true), true); + + // Combined: should reset because Agent mode fails + assert.strictEqual(shouldResetModelToDefault(partialModel, [partialModel], { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: true, + sessionType: undefined, + }, [partialModel]), true); + }); + + test('EditorInline + Ask + V2 only requires toolCalling', () => { + const toolModel = createModel('tool', 'Tool'); + assert.strictEqual(shouldResetModelToDefault(toolModel, [toolModel], { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: true, + sessionType: undefined, + }, [toolModel]), false); + }); + + test('EditorInline + Ask + V2 rejects model without toolCalling', () => { + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: {}, + }); + assert.strictEqual(shouldResetModelToDefault(noToolModel, [noToolModel], { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: true, + sessionType: undefined, + }, [noToolModel]), true); + }); + }); + + suite('findDefaultModel edge cases', () => { + + test('when all models are session-targeted and none is default, first model wins', () => { + const m1 = createSessionModel('s1', 'Session 1', 'cloud'); + const m2 = createSessionModel('s2', 'Session 2', 'cloud'); + const result = findDefaultModel([m1, m2], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 's1'); + }); + + test('default for one location does not leak to another', () => { + const chatDefault = createDefaultModelForLocation('chat-def', 'Chat Default', ChatAgentLocation.Chat); + const noDefault = createModel('no-def', 'No Default'); + + // For Chat: chatDefault wins + assert.strictEqual(findDefaultModel([noDefault, chatDefault], ChatAgentLocation.Chat)?.metadata.id, 'chat-def'); + // For Terminal: no model is default, so first model wins + assert.strictEqual(findDefaultModel([noDefault, chatDefault], ChatAgentLocation.Terminal)?.metadata.id, 'no-def'); + }); + }); + + suite('realistic multi-step race simulations', () => { + + test('startup: cached model → live models arrive → user choice preserved', () => { + const cachedGpt = createModel('gpt', 'GPT'); + const cachedClaude = createModel('claude', 'Claude'); + + // Step 1: Startup with only cache. User had GPT selected. + const cachedModels = computeAvailableModels( + [], + [cachedGpt, cachedClaude], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + // GPT is in the cached list + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', cachedModels), false); + + // Step 2: Live models arrive (same models) + const liveModels = computeAvailableModels( + [cachedGpt, cachedClaude], + [cachedGpt, cachedClaude], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + // GPT still in the list — no reset needed + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', liveModels), false); + }); + + test('startup: no cache → models arrive late → persisted choice restored', () => { + // Step 1: No models available at all + const emptyModels = computeAvailableModels([], [], new Set(['copilot']), undefined, ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.strictEqual(emptyModels.length, 0); + + // initSelectedModel: model not found, enters _waitForPersistedLanguageModel path + const restoreResult = shouldRestorePersistedModel('copilot/gpt', false, emptyModels, ChatAgentLocation.Chat); + assert.strictEqual(restoreResult.shouldRestore, false); + assert.strictEqual(restoreResult.model, undefined); + + // Step 2: Models arrive via onDidChangeLanguageModels + const arrivedModel = createModel('gpt', 'GPT'); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', false, arrivedModel, ChatAgentLocation.Chat), + true, + ); + }); + + test('extension reload: selected model flickers out then back', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Step 1: GPT is selected + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [gpt, claude]), false); + + // Step 2: Extension reloads, copilot vendor has no live models + // But cache bridges the gap + const duringReload = mergeModelsWithCache([], [gpt, claude], new Set(['copilot'])); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', duringReload), false); + + // Step 3: Extension finishes loading, live models back + const afterReload = mergeModelsWithCache([gpt, claude], [gpt, claude], new Set(['copilot'])); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', afterReload), false); + }); + + test('extension reload without cache: model lost', () => { + const gpt = createModel('gpt', 'GPT'); + + // Step 1: GPT selected, no cache + // Step 2: Extension reloads with no models and no cache + const duringReload = mergeModelsWithCache([], [], new Set(['copilot'])); + assert.strictEqual(duringReload.length, 0); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', duringReload), true); + // → Model is lost, reset to default + + // Step 3: Models come back but user's choice is already gone + const afterReload = mergeModelsWithCache([gpt], [], new Set(['copilot'])); + assert.strictEqual(afterReload.length, 1); + // User's selection was already reset to something else + // This is expected behavior — cache is the mitigation + }); + + test('session switch race: mode + session change together', () => { + const generalDefault = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Chat); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud', { + capabilities: { toolCalling: true, agentMode: true }, + }); + const allModels = [generalDefault, cloudModel]; + + // User is in general session with GPT in Agent mode + assert.strictEqual(shouldResetModelToDefault(generalDefault, [generalDefault], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), false); + + // Switch to cloud session — general model should be reset + assert.strictEqual(shouldResetModelToDefault(generalDefault, [cloudModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allModels), true); + + // The default for cloud session should be the cloud model + const cloudDefault = findDefaultModel([cloudModel], ChatAgentLocation.Chat); + assert.strictEqual(cloudDefault?.metadata.id, 'cloud-gpt'); + }); + + test('rapid mode changes: ask → agent → ask preserves compatible model', () => { + const model = createModel('gpt', 'GPT'); // Compatible with all modes + const allModels = [model]; + + // Ask mode: fine + assert.strictEqual(shouldResetModelToDefault(model, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + + // → Agent mode: model has toolCalling, still fine + assert.strictEqual(shouldResetModelToDefault(model, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + + // → Back to Ask: still fine + assert.strictEqual(shouldResetModelToDefault(model, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + }); + + test('rapid mode changes: ask → agent resets incompatible, then agent → ask does not restore', () => { + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: { toolCalling: false }, + }); + const toolModel = createDefaultModelForLocation('tool', 'Tool', ChatAgentLocation.Chat); + const allModels = [noToolModel, toolModel]; + + // Ask mode with noToolModel: fine + assert.strictEqual(shouldResetModelToDefault(noToolModel, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + + // → Agent mode: noToolModel fails, reset picks default (toolModel) + assert.strictEqual(shouldResetModelToDefault(noToolModel, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), true); + const defaultAfterReset = findDefaultModel(allModels, ChatAgentLocation.Chat); + assert.strictEqual(defaultAfterReset?.metadata.id, 'tool'); + + // → Back to Ask: toolModel is fine in Ask mode, stays as toolModel + // The original noToolModel is NOT restored — this is expected and matches ChatInputPart behavior + assert.strictEqual(shouldResetModelToDefault(toolModel, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + }); + }); +}); From defff987fafbff3d3bfece398496196f3baab4d4 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 4 Mar 2026 17:02:26 +0000 Subject: [PATCH 165/448] update: remove unused CSS file and clean up theme-related styles for better maintainability Co-authored-by: Copilot --- extensions/theme-2026/package.json | 10 +---- extensions/theme-2026/themes/styles.css | 37 ------------------- .../quickinput/browser/media/quickInput.css | 2 + .../browser/media/settingsEditor2.css | 1 + .../preferences/browser/settingsEditor2.ts | 5 +-- 5 files changed, 5 insertions(+), 50 deletions(-) delete mode 100644 extensions/theme-2026/themes/styles.css diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json index 305cc066c89..8360afdac5c 100644 --- a/extensions/theme-2026/package.json +++ b/extensions/theme-2026/package.json @@ -8,9 +8,6 @@ "engines": { "vscode": "^1.85.0" }, - "enabledApiProposals": [ - "css" - ], "categories": [ "Themes" ], @@ -28,11 +25,6 @@ "uiTheme": "vs-dark", "path": "./themes/2026-dark.json" } - ], - "css": [ - { - "path": "./themes/styles.css" - } - ] + ] } } diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css deleted file mode 100644 index be78cde2241..00000000000 --- a/extensions/theme-2026/themes/styles.css +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* Quick Input (Command Palette) */ - -.monaco-workbench .quick-input-list .quick-input-list-entry .quick-input-list-separator { - height: 16px; - margin-top: 2px; - display: flex; - align-items: center; - font-size: 11px; - padding: 0 4px; - border-radius: var(--vscode-cornerRadius-small) !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 .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 !important; - padding: 0; -} - -/* Settings */ -.monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { - border-radius: var(--radius-sm); - background: transparent !important; - color: var(--vscode-descriptionForeground) !important; - border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; -} diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 01831b024b9..aa48f0b90f2 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -308,6 +308,8 @@ .quick-input-list .quick-input-list-entry .quick-input-list-separator { margin-right: 4px; + font-size: var(--vscode-bodyFontSize-xSmall); + color: var(--vscode-descriptionForeground); /* separate from keybindings or actions */ } diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index e8ab65c8a5a..b88b3073a9d 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -62,6 +62,7 @@ .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { margin-right: 3px; padding-bottom: 3px; + color: var(--vscode-descriptionForeground); } .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget:empty { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 19d87307110..76b4bbb9c16 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -44,7 +44,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles, defaultToggleStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { asCssVariable, asCssVariableWithDefault, badgeBackground, badgeForeground, contrastBorder, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js'; +import { asCssVariable, editorForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IUserDataSyncEnablementService, IUserDataSyncService, SyncStatus } from '../../../../platform/userDataSync/common/userDataSync.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; @@ -801,9 +801,6 @@ export class SettingsEditor2 extends EditorPane { this.controlsElement = DOM.append(this.searchContainer, DOM.$('.search-container-widgets')); this.countElement = DOM.append(this.controlsElement, DOM.$('.settings-count-widget.monaco-count-badge.long')); - this.countElement.style.backgroundColor = asCssVariable(badgeBackground); - this.countElement.style.color = asCssVariable(badgeForeground); - this.countElement.style.border = `1px solid ${asCssVariableWithDefault(contrastBorder, asCssVariable(inputBackground))}`; this.searchInputActionBar = this._register(new ActionBar(this.controlsElement, { actionViewItemProvider: (action, options) => { From 4c1cc1582a579003a19dce9eed0630626a463336 Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 5 Mar 2026 02:31:07 +0900 Subject: [PATCH 166/448] fix: add version folder to visualelements manifest icon path (#299239) --- build/gulpfile.vscode.ts | 1 + resources/win32/VisualElementsManifest.xml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index b499fd720ff..ac4ee9cec7d 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -599,6 +599,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d } result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) + .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder ? `${versionedResourcesFolder}\\` : '')) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) diff --git a/resources/win32/VisualElementsManifest.xml b/resources/win32/VisualElementsManifest.xml index 40efd0a396e..5ad1283cd7e 100644 --- a/resources/win32/VisualElementsManifest.xml +++ b/resources/win32/VisualElementsManifest.xml @@ -2,8 +2,8 @@ From 450351e6198bcdb27caf173234f04c1672440890 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:53:39 -0800 Subject: [PATCH 167/448] Revert "Polish question carousel (#298377)" (#299096) This reverts commit 2f76a2d97226f50eb529669084382b8276ba92ba. --- extensions/theme-2026/themes/2026-dark.json | 2 +- extensions/theme-2026/themes/styles.css | 1 + .../browser/actions/chatExecuteActions.ts | 20 +- .../chat/browser/actions/chatQueueActions.ts | 7 +- .../chatQuestionCarouselPart.ts | 532 ++++++------------ .../media/chatQuestionCarousel.css | 318 +++++------ .../tools/builtinTools/askQuestionsTool.ts | 23 +- .../chatQuestionCarouselPart.test.ts | 96 ++-- .../builtinTools/askQuestionsTool.test.ts | 18 +- 9 files changed, 403 insertions(+), 614 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4645a08fa61..b7652bcfb0d 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -54,7 +54,7 @@ "badge.background": "#3994BCF0", "badge.foreground": "#FFFFFF", "progressBar.background": "#878889", - "list.activeSelectionBackground": "#262728", + "list.activeSelectionBackground": "#3994BC26", "list.activeSelectionForeground": "#ededed", "list.inactiveSelectionBackground": "#2C2D2E", "list.inactiveSelectionForeground": "#ededed", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index d76e07491ed..255a10a0778 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -46,6 +46,7 @@ padding: 0; } + /* Settings */ .monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { border-radius: var(--radius-sm); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4d7b9f7fa8f..29fb309c4db 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -185,8 +185,15 @@ const requestInProgressOrPendingToolCall = ContextKeyExpr.or( ChatContextKeys.Editing.hasToolConfirmation, ChatContextKeys.Editing.hasQuestionCarousel, ); +const requestInProgressWithoutInput = ContextKeyExpr.and( + ChatContextKeys.requestInProgress, + ChatContextKeys.inputHasText.negate(), +); +const pendingToolCall = ContextKeyExpr.or( + ChatContextKeys.Editing.hasToolConfirmation, + ChatContextKeys.Editing.hasQuestionCarousel, +); const whenNotInProgress = ChatContextKeys.requestInProgress.negate(); -const whenNoRequestOrPendingToolCall = requestInProgressOrPendingToolCall!.negate(); export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; @@ -195,7 +202,7 @@ export class ChatSubmitAction extends SubmitAction { const menuCondition = ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask); const precondition = ContextKeyExpr.and( ChatContextKeys.inputHasText, - whenNoRequestOrPendingToolCall, + whenNotInProgress, ChatContextKeys.chatSessionOptionsValid, ); @@ -224,7 +231,7 @@ export class ChatSubmitAction extends SubmitAction { id: MenuId.ChatExecute, order: 4, when: ContextKeyExpr.and( - whenNoRequestOrPendingToolCall, + whenNotInProgress, menuCondition, ChatContextKeys.withinEditSessionDiff.negate(), ), @@ -240,7 +247,7 @@ export class ChatSubmitAction extends SubmitAction { order: 4, when: ContextKeyExpr.and( ContextKeyExpr.or(ctxHasEditorModification.negate(), ChatContextKeys.inputHasText), - whenNoRequestOrPendingToolCall, + whenNotInProgress, ChatContextKeys.requestInProgress.negate(), menuCondition ), @@ -657,7 +664,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { constructor() { const notInProgressOrEditing = ContextKeyExpr.and( - ContextKeyExpr.or(whenNoRequestOrPendingToolCall, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), + ContextKeyExpr.or(whenNotInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.QueueOrSteer) ); @@ -839,8 +846,7 @@ export class CancelAction extends Action2 { menu: [{ id: MenuId.ChatExecute, when: ContextKeyExpr.and( - requestInProgressOrPendingToolCall, - ChatContextKeys.inputHasText.negate(), + ContextKeyExpr.or(requestInProgressWithoutInput, pendingToolCall), ChatContextKeys.remoteJobCreating.negate(), ChatContextKeys.currentlyEditing.negate(), ), diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index 701177fb30d..6606748d653 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -18,12 +18,7 @@ import { IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; const queuingActionsPresent = ContextKeyExpr.and( - ContextKeyExpr.or( - ChatContextKeys.requestInProgress, - ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer), - ChatContextKeys.Editing.hasQuestionCarousel, - ChatContextKeys.Editing.hasToolConfirmation, - ), + ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent), ); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index c22a7d3391b..47ba2a1fa5d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -10,7 +10,6 @@ import { Emitter, Event } from '../../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { isMacintosh } from '../../../../../../base/common/platform.js'; import { hasKey } from '../../../../../../base/common/types.js'; import { localize } from '../../../../../../nls.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; @@ -29,10 +28,13 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; import './media/chatQuestionCarousel.css'; +const PREVIOUS_QUESTION_ACTION_ID = 'workbench.action.chat.previousQuestion'; +const NEXT_QUESTION_ACTION_ID = 'workbench.action.chat.nextQuestion'; export interface IChatQuestionCarouselOptions { onSubmit: (answers: Map | undefined) => void; shouldAutoFocus?: boolean; @@ -46,15 +48,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private _currentIndex = 0; private readonly _answers = new Map(); - private readonly _explicitlyAnsweredQuestionIds = new Set(); private _questionContainer: HTMLElement | undefined; private _closeButtonContainer: HTMLElement | undefined; - private _tabBar: HTMLElement | undefined; - private _tabItems: HTMLElement[] = []; - private readonly _questionTabIndicators = new Map(); - private _reviewIndex = -1; private _footerRow: HTMLElement | undefined; + private _stepIndicator: HTMLElement | undefined; + private _navigationButtons: HTMLElement | undefined; + private _prevButton: Button | undefined; + private _nextButton: Button | undefined; + private readonly _nextButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable()); + private _submitButton: Button | undefined; + private readonly _submitButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable()); private _skipAllButton: Button | undefined; private _isSkipped = false; @@ -82,6 +86,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent @IHoverService private readonly _hoverService: IHoverService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); @@ -133,10 +138,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._interactiveUIStore.value = interactiveStore; // Question container - const questionPanelId = `question-panel-${this.carousel.questions[0]?.id ?? 'default'}`; this._questionContainer = dom.$('.chat-question-carousel-content'); - this._questionContainer.setAttribute('role', 'tabpanel'); - this._questionContainer.id = questionPanelId; this.domNode.append(this._questionContainer); // Close/skip button (X) - placed in header row, only shown when allowSkip is true @@ -151,75 +153,49 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._skipAllButton = skipAllButton; } - const isSingleQuestion = this.carousel.questions.length === 1; + // Footer row with step indicator and navigation buttons + this._footerRow = dom.$('.chat-question-footer-row'); - if (!isSingleQuestion) { - this._reviewIndex = this.carousel.questions.length; + // Step indicator (e.g., "2/4") on the left + this._stepIndicator = dom.$('.chat-question-step-indicator'); + this._footerRow.appendChild(this._stepIndicator); - // Multi-question: Create tab bar with question tabs and Review tab - this._tabBar = dom.$('.chat-question-tab-bar'); - const tabList = dom.$('.chat-question-tabs'); - tabList.setAttribute('role', 'tablist'); - tabList.setAttribute('aria-label', localize('chat.questionCarousel.tabBarLabel', 'Questions')); - this._tabBar.appendChild(tabList); + // Navigation controls (< >) - placed in footer row + this._navigationButtons = dom.$('.chat-question-carousel-nav'); + this._navigationButtons.setAttribute('role', 'navigation'); + this._navigationButtons.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); - this.carousel.questions.forEach((question, index) => { - const tab = dom.$('.chat-question-tab'); - tab.setAttribute('role', 'tab'); - tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false'); - tab.tabIndex = index === 0 ? 0 : -1; - tab.id = `question-tab-${question.id}-${index}`; - tab.setAttribute('aria-controls', questionPanelId); + // Group prev/next buttons together + const arrowsContainer = dom.$('.chat-question-nav-arrows'); - const displayTitle = this.getQuestionText(question.title); - const tabIndicator = dom.$('.chat-question-tab-indicator.codicon'); - const tabLabel = dom.$('span.chat-question-tab-label'); - tabLabel.textContent = displayTitle; - tab.append(tabIndicator, tabLabel); - tab.setAttribute('aria-label', displayTitle); - this._questionTabIndicators.set(question.id, tabIndicator); + const previousLabel = localize('previous', 'Previous'); + const previousLabelWithKeybinding = this.getLabelWithKeybinding(previousLabel, PREVIOUS_QUESTION_ACTION_ID); + const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev'); + prevButton.label = `$(${Codicon.chevronLeft.id})`; + prevButton.element.setAttribute('aria-label', previousLabelWithKeybinding); + interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabelWithKeybinding })); + this._prevButton = prevButton; - interactiveStore.add(dom.addDisposableListener(tab, dom.EventType.CLICK, () => { - this.saveCurrentAnswer(); - this._currentIndex = index; - this.renderCurrentQuestion(true); - tab.focus(); - })); + const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + nextButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-next'); + nextButton.label = `$(${Codicon.chevronRight.id})`; + this._nextButton = nextButton; - tabList.appendChild(tab); - this._tabItems.push(tab); - }); + const submitButton = interactiveStore.add(new Button(this._navigationButtons, { ...defaultButtonStyles })); + submitButton.element.classList.add('chat-question-submit-button'); + submitButton.label = localize('submit', 'Submit'); + this._submitButton = submitButton; - // Review tab - const reviewTab = dom.$('.chat-question-tab.no-icon'); - reviewTab.setAttribute('role', 'tab'); - reviewTab.setAttribute('aria-selected', 'false'); - reviewTab.tabIndex = -1; - reviewTab.id = 'question-tab-review'; - reviewTab.setAttribute('aria-controls', questionPanelId); - const reviewLabel = localize('chat.questionCarousel.review', 'Review'); - reviewTab.textContent = reviewLabel; - reviewTab.setAttribute('aria-label', reviewLabel); - interactiveStore.add(dom.addDisposableListener(reviewTab, dom.EventType.CLICK, () => { - this.saveCurrentAnswer(); - this._currentIndex = this._reviewIndex; - this.renderCurrentQuestion(true); - reviewTab.focus(); - })); - tabList.appendChild(reviewTab); - this._tabItems.push(reviewTab); + this._navigationButtons.appendChild(arrowsContainer); + this._footerRow.appendChild(this._navigationButtons); + this.domNode.append(this._footerRow); - // Controls container for close button only - if (this._closeButtonContainer) { - const controlsContainer = dom.$('.chat-question-tab-controls'); - controlsContainer.appendChild(this._closeButtonContainer); - this._tabBar.appendChild(controlsContainer); - } - - this.domNode.insertBefore(this._tabBar, this._questionContainer!); - } // Register event listeners + interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1))); + interactiveStore.add(nextButton.onDidClick(() => this.navigate(1))); + interactiveStore.add(submitButton.onDidClick(() => this.submit())); if (this._skipAllButton) { interactiveStore.add(this._skipAllButton.onDidClick(() => this.ignore())); } @@ -231,37 +207,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent e.preventDefault(); e.stopPropagation(); this.ignore(); - } else if (!isSingleQuestion && (event.keyCode === KeyCode.RightArrow || event.keyCode === KeyCode.LeftArrow)) { - // Arrow L/R navigates tabs from anywhere in the carousel, - // except when focus is in a text input or textarea (where arrows move cursor) - const target = e.target as HTMLElement; - const isTextInput = target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'; - const isTextarea = target.tagName === 'TEXTAREA'; - if (!isTextInput && !isTextarea) { - e.preventDefault(); - e.stopPropagation(); - const totalTabs = this._tabItems.length; // includes Review tab - if (event.keyCode === KeyCode.RightArrow) { - if (this._currentIndex < totalTabs - 1) { - this.saveCurrentAnswer(); - this._currentIndex++; - this.renderCurrentQuestion(true); - this._tabItems[this._currentIndex]?.focus(); - } - } else { - if (this._currentIndex > 0) { - this.saveCurrentAnswer(); - this._currentIndex--; - this.renderCurrentQuestion(true); - this._tabItems[this._currentIndex]?.focus(); - } - } - } - } else if (event.keyCode === KeyCode.Enter && (event.metaKey || event.ctrlKey)) { - // Cmd/Ctrl+Enter submits immediately from anywhere - e.preventDefault(); - e.stopPropagation(); - this.submit(); } else if (event.keyCode === KeyCode.Enter && !event.shiftKey) { // Handle Enter key for text inputs and freeform textareas, not radio/checkbox or buttons // Buttons have their own Enter/Space handling via Button class @@ -287,9 +232,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private saveCurrentAnswer(): void { const currentQuestion = this.carousel.questions[this._currentIndex]; - if (!currentQuestion) { - return; // Review tab or out of bounds - } const answer = this.getCurrentAnswer(); if (answer !== undefined) { this._answers.set(currentQuestion.id, answer); @@ -329,24 +271,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private handleNextOrSubmit(): void { this.saveCurrentAnswer(); - const currentQuestion = this.carousel.questions[this._currentIndex]; - if (currentQuestion && this.getCurrentAnswer() !== undefined) { - this._explicitlyAnsweredQuestionIds.add(currentQuestion.id); - this.updateQuestionTabIndicators(); - } if (this._currentIndex < this.carousel.questions.length - 1) { // Move to next question this._currentIndex++; this.persistDraftState(); this.renderCurrentQuestion(true); - } else if (this.carousel.questions.length > 1) { - // Multi-question: navigate to Review tab - this._currentIndex = this._reviewIndex; - this.renderCurrentQuestion(true); - this._tabItems[this._currentIndex]?.focus(); } else { - // Single question: submit directly + // Submit this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -357,10 +289,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private submit(): void { this.saveCurrentAnswer(); - const currentQuestion = this.carousel.questions[this._currentIndex]; - if (currentQuestion) { - this._explicitlyAnsweredQuestionIds.add(currentQuestion.id); - } this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -411,18 +339,20 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._singleSelectItems.clear(); this._multiSelectCheckboxes.clear(); this._freeformTextareas.clear(); + this._nextButtonHover.value = undefined; + this._submitButtonHover.value = undefined; // Clear references to disposed elements + this._prevButton = undefined; + this._nextButton = undefined; + this._submitButton = undefined; this._skipAllButton = undefined; this._questionContainer = undefined; + this._navigationButtons = undefined; this._closeButtonContainer = undefined; - this._tabBar = undefined; - this._tabItems = []; - this._questionTabIndicators.clear(); - this._reviewIndex = -1; this._footerRow = undefined; + this._stepIndicator = undefined; this._inputScrollable = undefined; - this._explicitlyAnsweredQuestionIds.clear(); } private layoutInputScrollable(inputScrollable: DomScrollableElement): void { @@ -621,7 +551,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } private renderCurrentQuestion(focusContainerForScreenReader: boolean = false): void { - if (!this._questionContainer) { + if (!this._questionContainer || !this._prevButton || !this._nextButton || !this._submitButton) { return; } @@ -636,102 +566,60 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._multiSelectCheckboxes.clear(); this._freeformTextareas.clear(); - // Remove footer if it exists from a previous Review render - if (this._footerRow) { - this._footerRow.remove(); - this._footerRow = undefined; - } - // Clear previous content dom.clearNode(this._questionContainer); - const isSingleQuestion = this.carousel.questions.length === 1; - const isReview = !isSingleQuestion && this._currentIndex === this._reviewIndex; - - // Update tab bar active state for multi-question carousels - if (!isSingleQuestion) { - this._tabItems.forEach((tab, index) => { - const isActive = index === this._currentIndex; - tab.classList.toggle('active', isActive); - tab.setAttribute('aria-selected', String(isActive)); - tab.tabIndex = isActive ? 0 : -1; - }); - // Link the panel to the active tab for screen readers - const activeTab = this._tabItems[this._currentIndex]; - if (activeTab) { - this._questionContainer.setAttribute('aria-labelledby', activeTab.id); - } - this.updateQuestionTabIndicators(); - } - - if (isReview) { - this.renderReviewPanel(questionRenderStore); - } else { - this.renderQuestionPanel(questionRenderStore, isSingleQuestion); - } - - // Update aria-label to reflect the current question - this._updateAriaLabel(); - - // In screen reader mode, focus the container and announce the question - if (focusContainerForScreenReader && this._accessibilityService.isScreenReaderOptimized()) { - this._focusContainerAndAnnounce(); - } - - this._onDidChangeHeight.fire(); - } - - /** - * Renders a question panel (title, message, input) inside the question container. - */ - private renderQuestionPanel(questionRenderStore: DisposableStore, isSingleQuestion: boolean): void { const question = this.carousel.questions[this._currentIndex]; - if (!question || !this._questionContainer) { + if (!question) { return; } - // Render question header row with title and close button (single question only) - if (isSingleQuestion) { - const headerRow = dom.$('.chat-question-header-row'); - const titleRow = dom.$('.chat-question-title-row'); + // Render question header row with title and close button + const headerRow = dom.$('.chat-question-header-row'); + const titleRow = dom.$('.chat-question-title-row'); - if (question.title) { - const title = dom.$('.chat-question-title'); - const questionText = question.title; - const messageContent = this.getQuestionText(questionText); + // Render question title (short header) in the header bar as plain text + if (question.title) { + const title = dom.$('.chat-question-title'); + const questionText = question.title; + const messageContent = this.getQuestionText(questionText); - title.setAttribute('aria-label', messageContent); + title.setAttribute('aria-label', messageContent); - if (question.message !== undefined) { - const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); - const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); - title.appendChild(renderedTitle.element); + if (question.message !== undefined) { + const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); + const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); + title.appendChild(renderedTitle.element); + } else { + // Check for subtitle in parentheses at the end + const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); + if (parenMatch) { + // Main title (bold) + const mainTitle = dom.$('span.chat-question-title-main'); + mainTitle.textContent = parenMatch[1]; + title.appendChild(mainTitle); + + // Subtitle in parentheses (normal weight) + const subtitle = dom.$('span.chat-question-title-subtitle'); + subtitle.textContent = ' ' + parenMatch[2]; + title.appendChild(subtitle); } else { - const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); - if (parenMatch) { - const mainTitle = dom.$('span.chat-question-title-main'); - mainTitle.textContent = parenMatch[1]; - title.appendChild(mainTitle); - - const subtitle = dom.$('span.chat-question-title-subtitle'); - subtitle.textContent = ' ' + parenMatch[2]; - title.appendChild(subtitle); - } else { - title.textContent = messageContent; - } + title.textContent = messageContent; } - titleRow.appendChild(title); } - - if (this._closeButtonContainer) { - titleRow.appendChild(this._closeButtonContainer); - } - - headerRow.appendChild(titleRow); - this._questionContainer.appendChild(headerRow); + titleRow.appendChild(title); } - // Render full question text below the header row + // Add close button to header row (if allowSkip is enabled) + if (this._closeButtonContainer) { + titleRow.appendChild(this._closeButtonContainer); + } + + headerRow.appendChild(titleRow); + + this._questionContainer.appendChild(headerRow); + + // Render full question text below the header row (supports multi-line and markdown) if (question.message) { const messageEl = dom.$('.chat-question-message'); if (isMarkdownString(question.message)) { @@ -743,6 +631,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._questionContainer.appendChild(messageEl); } + const isSingleQuestion = this.carousel.questions.length === 1; + // Update step indicator in footer + if (this._stepIndicator) { + this._stepIndicator.textContent = `${this._currentIndex + 1}/${this.carousel.questions.length}`; + this._stepIndicator.style.display = isSingleQuestion ? 'none' : ''; + } + // Render input based on question type const inputContainer = dom.$('.chat-question-input-container'); this.renderInput(inputContainer, question); @@ -768,75 +663,45 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollable.setScrollPosition({ scrollTop: 0, scrollLeft: 0 }); inputScrollable.scanDomNode(); })); + + // Update navigation button states (prevButton and nextButton are guaranteed non-null from guard above) + this._prevButton!.enabled = this._currentIndex > 0; + this._prevButton!.element.style.display = isSingleQuestion ? 'none' : ''; + + // Keep navigation arrows stable and disable next on the last question + const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1; + const submitLabel = localize('submit', 'Submit'); + const nextLabel = localize('next', 'Next'); + const nextLabelWithKeybinding = this.getLabelWithKeybinding(nextLabel, NEXT_QUESTION_ACTION_ID); + this._nextButton!.label = `$(${Codicon.chevronRight.id})`; + this._nextButton!.enabled = !isLastQuestion; + this._nextButton!.element.setAttribute('aria-label', nextLabelWithKeybinding); + this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabelWithKeybinding }); + + this._submitButton!.enabled = isLastQuestion; + this._submitButton!.element.style.display = isLastQuestion ? '' : 'none'; + this._submitButton!.element.setAttribute('aria-label', submitLabel); + this._submitButtonHover.value = isLastQuestion + ? this._hoverService.setupDelayedHover(this._submitButton!.element, { content: submitLabel }) + : undefined; + + // Update aria-label to reflect the current question + this._updateAriaLabel(); + + // In screen reader mode, focus the container and announce the question + // This must happen after all render calls to avoid focus being stolen + if (focusContainerForScreenReader && this._accessibilityService.isScreenReaderOptimized()) { + this._focusContainerAndAnnounce(); + } + + this._onDidChangeHeight.fire(); } - /** - * Renders the review panel with a summary of all answers and a submit footer. - */ - private renderReviewPanel(questionRenderStore: DisposableStore): void { - if (!this._questionContainer) { - return; - } - - // Render inline review summary. - // If no explicit answers exist yet, show a single empty-state label. - // If some explicit answers exist, show all questions and mark missing ones as not answered yet. - const summaryContainer = dom.$('.chat-question-carousel-summary'); - const answeredCount = this.carousel.questions.filter(q => this._explicitlyAnsweredQuestionIds.has(q.id)).length; - - if (answeredCount === 0) { - const emptyLabel = dom.$('div.chat-question-summary-empty'); - emptyLabel.textContent = localize('chat.questionCarousel.noQuestionsAnsweredYet', 'No questions answered yet'); - summaryContainer.appendChild(emptyLabel); - this._questionContainer.appendChild(summaryContainer); - } else { - for (const question of this.carousel.questions) { - const summaryItem = dom.$('.chat-question-summary-item'); - - const questionRow = dom.$('div.chat-question-summary-label'); - const questionText = question.message ?? question.title; - let labelText = typeof questionText === 'string' ? questionText : questionText.value; - labelText = labelText.replace(/[:\s]+$/, ''); - questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); - summaryItem.appendChild(questionRow); - - const hasExplicitAnswer = this._explicitlyAnsweredQuestionIds.has(question.id); - const answer = this._answers.get(question.id); - - if (hasExplicitAnswer && answer !== undefined) { - const formattedAnswer = this.formatAnswerForSummary(question, answer); - const answerRow = dom.$('div.chat-question-summary-answer'); - answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); - summaryItem.appendChild(answerRow); - } else { - const unanswered = dom.$('div.chat-question-summary-unanswered'); - unanswered.textContent = localize('chat.questionCarousel.notAnsweredYet', 'Not answered yet'); - summaryItem.appendChild(unanswered); - } - - summaryContainer.appendChild(summaryItem); - } - - this._questionContainer.appendChild(summaryContainer); - } - - // Footer with Submit/Cancel appears only once at least one question is answered. - if (answeredCount > 0) { - this._footerRow = dom.$('.chat-question-footer-row'); - - const hint = dom.$('span.chat-question-submit-hint'); - hint.textContent = isMacintosh - ? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit') - : localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit'); - this._footerRow.appendChild(hint); - - const submitButton = questionRenderStore.add(new Button(this._footerRow, { ...defaultButtonStyles })); - submitButton.element.classList.add('chat-question-submit-button'); - submitButton.label = localize('submit', 'Submit'); - questionRenderStore.add(submitButton.onDidClick(() => this.submit())); - - this.domNode.append(this._footerRow); - } + private getLabelWithKeybinding(label: string, actionId: string): string { + const keybindingLabel = this._keybindingService.lookupKeybinding(actionId, this._contextKeyService)?.getLabel(); + return keybindingLabel + ? localize('chat.questionCarousel.labelWithKeybinding', '{0} ({1})', label, keybindingLabel) + : label; } private renderInput(container: HTMLElement, question: IChatQuestion): void { @@ -925,7 +790,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const listItems: HTMLElement[] = []; const indicators: HTMLElement[] = []; - const updateSelection = (newIndex: number, isUserInitiated: boolean = false) => { + const updateSelection = (newIndex: number) => { // Update visual state listItems.forEach((item, i) => { const isSelected = i === newIndex; @@ -944,9 +809,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (data) { data.selectedIndex = newIndex; } - if (isUserInitiated) { - this.updateQuestionTabIndicators(); - } this.saveCurrentAnswer(); }; @@ -975,12 +837,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { - const titleSpan = dom.$('div.chat-question-list-label-title'); + const titleSpan = dom.$('span.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); - const descSpan = dom.$('div.chat-question-list-label-desc'); - descSpan.textContent = option.label.substring(separatorIndex + 3); + const descSpan = dom.$('span.chat-question-list-label-desc'); + descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { label.textContent = option.label; @@ -996,7 +858,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._inputBoxes.add(dom.addDisposableListener(listItem, dom.EventType.CLICK, (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - updateSelection(index, true); + updateSelection(index); const freeform = this._freeformTextareas.get(question.id); if (freeform) { freeform.value = ''; @@ -1042,17 +904,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // clear when we start typing in freeform this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { if (freeformTextarea.value.length > 0) { - updateSelection(-1, true); - } - })); - - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.UpArrow && freeformTextarea.selectionStart === 0 && freeformTextarea.selectionEnd === 0 && listItems.length) { - e.preventDefault(); - const lastIndex = listItems.length - 1; - updateSelection(lastIndex, true); - listItems[lastIndex].focus(); + updateSelection(-1); + } else { + this.saveCurrentAnswer(); } })); @@ -1071,11 +925,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (event.keyCode === KeyCode.DownArrow) { e.preventDefault(); - if (data.selectedIndex >= listItems.length - 1) { - updateSelection(-1); - freeformTextarea.focus(); - return; - } newIndex = Math.min(data.selectedIndex + 1, listItems.length - 1); } else if (event.keyCode === KeyCode.UpArrow) { e.preventDefault(); @@ -1091,17 +940,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const numberIndex = event.keyCode - KeyCode.Digit1; if (numberIndex < listItems.length) { e.preventDefault(); - updateSelection(numberIndex, true); + updateSelection(numberIndex); } else if (numberIndex === listItems.length) { e.preventDefault(); - updateSelection(-1, true); + updateSelection(-1); freeformTextarea.focus(); } return; } if (newIndex !== data.selectedIndex && newIndex >= 0) { - updateSelection(newIndex, true); + updateSelection(newIndex); } })); @@ -1188,12 +1037,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { - const titleSpan = dom.$('div.chat-question-list-label-title'); + const titleSpan = dom.$('span.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); - const descSpan = dom.$('div.chat-question-list-label-desc'); - descSpan.textContent = option.label.substring(separatorIndex + 3); + const descSpan = dom.$('span.chat-question-list-label-desc'); + descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { label.textContent = option.label; @@ -1211,7 +1060,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._inputBoxes.add(checkbox.onChange(() => { listItem.classList.toggle('checked', checkbox.checked); listItem.setAttribute('aria-selected', String(checkbox.checked)); - this.updateQuestionTabIndicators(); this.saveCurrentAnswer(); })); @@ -1259,18 +1107,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const autoResize = this.setupTextareaAutoResize(freeformTextarea); this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => this.saveCurrentAnswer())); - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.UpArrow && freeformTextarea.selectionStart === 0 && freeformTextarea.selectionEnd === 0 && listItems.length) { - e.preventDefault(); - focusedIndex = listItems.length - 1; - listItems[focusedIndex].focus(); - } - })); - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { - this.updateQuestionTabIndicators(); - })); - freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); @@ -1286,10 +1122,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (event.keyCode === KeyCode.DownArrow) { e.preventDefault(); - if (focusedIndex >= listItems.length - 1) { - freeformTextarea.focus(); - return; - } focusedIndex = Math.min(focusedIndex + 1, listItems.length - 1); listItems[focusedIndex].focus(); } else if (event.keyCode === KeyCode.UpArrow) { @@ -1358,20 +1190,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (data && data.selectedIndex >= 0) { selectedValue = question.options?.[data.selectedIndex]?.value; } - - // For single-select: freeform takes priority over selection. - const freeformTextarea = this._freeformTextareas.get(question.id); - const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; - if (freeformValue) { - return { selectedValue: undefined, freeformValue }; - } - - // Find default option if nothing selected and no freeform text (defaultValue is the option id) + // Find default option if nothing selected (defaultValue is the option id) if (selectedValue === undefined && typeof question.defaultValue === 'string') { const defaultOption = question.options?.find(opt => opt.id === question.defaultValue); selectedValue = defaultOption?.value; } + // For single-select: if freeform is provided, use ONLY freeform (ignore selection) + const freeformTextarea = this._freeformTextareas.get(question.id); + const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; + if (freeformValue) { + // Freeform takes priority - ignore selectedValue + return { selectedValue: undefined, freeformValue }; + } if (selectedValue !== undefined) { return { selectedValue, freeformValue: undefined }; } @@ -1442,19 +1273,35 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const summaryItem = dom.$('.chat-question-summary-item'); - // Question row with Q: prefix - const questionRow = dom.$('div.chat-question-summary-label'); + // Category label (use same text as shown in question UI: message ?? title) + const questionLabel = dom.$('span.chat-question-summary-label'); const questionText = question.message ?? question.title; let labelText = typeof questionText === 'string' ? questionText : questionText.value; + // Remove trailing colons and whitespace to avoid double colons (CSS adds ': ') labelText = labelText.replace(/[:\s]+$/, ''); - questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); - summaryItem.appendChild(questionRow); + questionLabel.textContent = labelText; + summaryItem.appendChild(questionLabel); - // Answer row with A: prefix + // Format answer with title and description parts const formattedAnswer = this.formatAnswerForSummary(question, answer); - const answerRow = dom.$('div.chat-question-summary-answer'); - answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); - summaryItem.appendChild(answerRow); + const separatorIndex = formattedAnswer.indexOf(' - '); + + if (separatorIndex !== -1) { + // Answer title (bold) + const answerTitle = dom.$('span.chat-question-summary-answer-title'); + answerTitle.textContent = formattedAnswer.substring(0, separatorIndex); + summaryItem.appendChild(answerTitle); + + // Answer description (normal) + const answerDesc = dom.$('span.chat-question-summary-answer-desc'); + answerDesc.textContent = ' - ' + formattedAnswer.substring(separatorIndex + 3); + summaryItem.appendChild(answerDesc); + } else { + // Just the answer value (bold) + const answerValue = dom.$('span.chat-question-summary-answer-title'); + answerValue.textContent = formattedAnswer; + summaryItem.appendChild(answerValue); + } summaryContainer.appendChild(summaryItem); } @@ -1513,21 +1360,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return renderAsPlaintext(md); } - - - private updateQuestionTabIndicators(): void { - for (const question of this.carousel.questions) { - const indicator = this._questionTabIndicators.get(question.id); - if (!indicator) { - continue; - } - - const hasExplicitAnswer = this._explicitlyAnsweredQuestionIds.has(question.id); - indicator.classList.toggle('codicon-check', hasExplicitAnswer); - indicator.classList.toggle('codicon-circle-filled', !hasExplicitAnswer); - } - } - hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { // does not have same content when it is not skipped and is active and we stop the response if (!this._isSkipped && !this.carousel.isUsed && isResponseVM(element) && element.isComplete) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index ae28fb9038e..3dd44e42731 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -14,19 +14,15 @@ .interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-container { margin: 0; border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-chat-list-background); - border-radius: var(--vscode-cornerRadius-large); -} - -.interactive-session .interactive-input-part.compact > .chat-question-carousel-widget-container .chat-question-carousel-container { - border-radius: var(--vscode-cornerRadius-small); + background-color: var(--vscode-editor-background); + border-radius: 4px; } /* general questions styling */ .interactive-session .chat-question-carousel-container { margin: 8px 0; border: 1px solid var(--vscode-chat-requestBorder); - border-radius: var(--vscode-cornerRadius-large); + border-radius: 4px; display: flex; flex-direction: column; overflow: hidden; @@ -61,14 +57,16 @@ flex-direction: column; flex: 1; min-height: 0; - background: var(--vscode-chat-list-background); + background: var(--vscode-chat-requestBackground); + padding: 8px 16px 10px 16px; overflow: hidden; .chat-question-header-row { display: flex; flex-direction: column; flex-shrink: 0; - background: var(--vscode-chat-list-background); + background: var(--vscode-chat-requestBackground); + padding: 0 16px 10px 16px; overflow: hidden; .chat-question-title-row { @@ -77,8 +75,6 @@ align-items: center; gap: 8px; min-width: 0; - padding: 4px 8px 4px 16px; - border-bottom: 1px solid var(--vscode-chat-requestBorder); } .chat-question-title { @@ -89,6 +85,13 @@ font-weight: 500; font-size: var(--vscode-chat-font-size-body-s); margin: 0; + padding-top: 4px; + padding-bottom: 4px; + margin-left: -16px; + margin-right: -16px; + padding-left: 16px; + padding-right: 16px; + border-bottom: 1px solid var(--vscode-chat-requestBorder); .rendered-markdown { a { @@ -123,37 +126,37 @@ width: 22px; height: 22px; padding: 0; - border: none !important; - box-shadow: none !important; + border: none; background: transparent !important; - color: var(--vscode-icon-foreground) !important; + color: var(--vscode-foreground) !important; } .monaco-button.chat-question-close:hover:not(.disabled) { background: var(--vscode-toolbar-hoverBackground) !important; } } - } - .chat-question-message { - flex-shrink: 0; - font-size: var(--vscode-chat-font-size-body-s); - line-height: 1.4; - word-wrap: break-word; - overflow-wrap: break-word; - padding: 16px; - .rendered-markdown { - a { - color: var(--vscode-textLink-foreground); - } + .chat-question-message { + flex-shrink: 0; + padding-top: 8px; + font-size: var(--vscode-chat-font-size-body-s); + word-wrap: break-word; + overflow-wrap: break-word; + line-height: 1.4; - a:hover, - a:active { - color: var(--vscode-textLink-activeForeground); - } + .rendered-markdown { + a { + color: var(--vscode-textLink-foreground); + } - p { - margin: 0; + a:hover, + a:active { + color: var(--vscode-textLink-activeForeground); + } + + p { + margin: 0; + } } } } @@ -176,26 +179,37 @@ } /* some hackiness to get the focus looking right */ - .chat-question-list-item:focus, - .chat-question-list-item:focus-visible, + .chat-question-list-item:focus:not(.selected), .chat-question-list:focus { outline: none; } + .chat-question-list:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; + } + + .chat-question-list:focus-within .chat-question-list-item.selected { + outline-width: 1px; + outline-style: solid; + outline-offset: -1px; + outline-color: var(--vscode-focusBorder); + } + .chat-question-list { display: flex; flex-direction: column; + gap: 3px; outline: none; - margin: 0 8px; - padding: 0 0 4px 0; + padding: 4px 0; .chat-question-list-item { display: flex; align-items: flex-start; - gap: 12px; - padding: 8px 8px 8px 12px; + gap: 8px; + padding: 3px 8px; cursor: pointer; - border-radius: var(--vscode-cornerRadius-medium); + border-radius: 3px; user-select: none; .chat-question-list-indicator { @@ -206,7 +220,6 @@ justify-content: center; flex-shrink: 0; margin-left: auto; - margin-top: 2px; } .chat-question-list-indicator.codicon-check { @@ -219,13 +232,11 @@ flex: 1; word-wrap: break-word; overflow-wrap: break-word; - display: flex; - flex-direction: column; + padding-top: 2px; } .chat-question-list-label-title { - font-weight: 500; - line-height: 1.4; + font-weight: 600; } .chat-question-list-label-desc { @@ -257,7 +268,11 @@ } .chat-question-list-number { + background-color: transparent; color: var(--vscode-list-activeSelectionForeground); + border-color: var(--vscode-list-activeSelectionForeground); + border-bottom-color: var(--vscode-list-activeSelectionForeground); + box-shadow: none; } } @@ -276,11 +291,11 @@ } .chat-question-freeform { + margin-left: 8px; display: flex; flex-direction: row; align-items: center; - margin: 0px 8px 0 20px; - gap: 12px; + gap: 8px; .chat-question-freeform-number { height: fit-content; @@ -296,11 +311,11 @@ width: 100%; min-height: 24px; max-height: 200px; - padding: 0; - border: none; + padding: 3px 8px; + border: 1px solid var(--vscode-input-border, var(--vscode-chat-requestBorder)); background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); - border-radius: var(--vscode-cornerRadius-medium); + border-radius: 4px; resize: none; font-family: var(--vscode-chat-font-family, inherit); font-size: var(--vscode-chat-font-size-body-s); @@ -310,26 +325,35 @@ } .chat-question-freeform-textarea:focus { - outline: none; + outline: 1px solid var(--vscode-focusBorder); + border-color: var(--vscode-focusBorder); } .chat-question-freeform-textarea::placeholder { color: var(--vscode-input-placeholderForeground); } - &:focus-within .chat-question-freeform-number { - color: var(--vscode-list-activeSelectionForeground); - } - } /* todo: change to use keybinding service so we don't have to recreate this */ .chat-question-list-number, .chat-question-freeform-number { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 14px; + padding: 0px 4px; + border-style: solid; + border-width: 1px; + border-radius: 3px; font-size: 11px; - color: var(--vscode-descriptionForeground); + font-weight: normal; + background-color: var(--vscode-keybindingLabel-background); + color: var(--vscode-keybindingLabel-foreground); + border-color: var(--vscode-keybindingLabel-border); + border-bottom-color: var(--vscode-keybindingLabel-bottomBorder); + box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow); flex-shrink: 0; - line-height: 1rem; } } @@ -343,67 +367,21 @@ overscroll-behavior: contain; } -/* tab bar for multi-question carousels */ -.interactive-session .chat-question-carousel-container .chat-question-tab-bar { +/* footer with step indicator and nav buttons */ +.interactive-session .chat-question-carousel-container .chat-question-footer-row { display: flex; + justify-content: space-between; align-items: center; - gap: 2px; - padding: 4px 8px 4px 4px; - border-bottom: 1px solid var(--vscode-chat-requestBorder); - background: var(--vscode-chat-list-background); + padding: 4px 16px; + border-top: 1px solid var(--vscode-chat-requestBorder); + background: var(--vscode-chat-requestBackground); - .chat-question-tabs { - display: flex; - align-items: center; - gap: 2px; - flex: 1; - min-width: 0; - overflow-x: auto; - } - - .chat-question-tab { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 2px 10px 2px 8px; - border-radius: var(--vscode-cornerRadius-medium); + .chat-question-step-indicator { font-size: var(--vscode-chat-font-size-body-s); - cursor: pointer; - font-weight: 500; - white-space: nowrap; - user-select: none; color: var(--vscode-descriptionForeground); - outline: none; } - .chat-question-tab .chat-question-tab-indicator { - font-size: 10px; - line-height: 1; - } - - .chat-question-tab .chat-question-tab-indicator.codicon-circle-filled { - color: var(--vscode-textLink-foreground); - } - - .chat-question-tab.no-icon { - padding: 2px 8px; - } - - .chat-question-tab:hover { - background: var(--vscode-list-hoverBackground); - color: var(--vscode-foreground); - } - - .chat-question-tab.active { - background: var(--vscode-list-activeSelectionBackground); - color: var(--vscode-list-activeSelectionForeground); - } - - .chat-question-tab:focus-visible { - outline: none; - } - - .chat-question-tab-controls { + .chat-question-carousel-nav { display: flex; align-items: center; gap: 4px; @@ -411,7 +389,34 @@ margin-left: auto; } - .chat-question-tab-controls .monaco-button.chat-question-submit-button { + .chat-question-nav-arrows { + display: flex; + align-items: center; + gap: 4px; + } + + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow { + min-width: 22px; + width: 22px; + height: 22px; + padding: 0; + border: none; + } + + /* Secondary buttons (prev, next) use gray secondary background */ + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev, + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next { + background: var(--vscode-button-secondaryBackground) !important; + color: var(--vscode-button-secondaryForeground) !important; + } + + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev:hover:not(.disabled), + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next:hover:not(.disabled) { + background: var(--vscode-button-secondaryHoverBackground) !important; + } + + /* Dedicated submit button uses primary background */ + .chat-question-carousel-nav .monaco-button.chat-question-submit-button { background: var(--vscode-button-background) !important; color: var(--vscode-button-foreground) !important; height: 22px; @@ -419,66 +424,20 @@ padding: 0 8px; } - .chat-question-tab-controls .monaco-button.chat-question-submit-button:hover:not(.disabled) { + .chat-question-carousel-nav .monaco-button.chat-question-submit-button:hover:not(.disabled) { background: var(--vscode-button-hoverBackground) !important; } - .chat-question-close-container { - flex-shrink: 0; - - .monaco-button.chat-question-close { - min-width: 22px; - width: 22px; - height: 22px; - padding: 0; - border: none !important; - box-shadow: none !important; - background: transparent !important; - color: var(--vscode-foreground) !important; - } - - .monaco-button.chat-question-close:hover:not(.disabled) { - background: var(--vscode-toolbar-hoverBackground) !important; - } - } -} - -/* footer with submit and cancel buttons */ -.interactive-session .chat-question-carousel-container .chat-question-footer-row { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 8px; - padding: 4px 8px; - border-top: 1px solid var(--vscode-chat-requestBorder); - background: var(--vscode-chat-list-background); - - .chat-question-submit-hint { - font-size: 11px; - color: var(--vscode-descriptionForeground); + /* Close button uses transparent background */ + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close { + background: transparent !important; + color: var(--vscode-foreground) !important; } - .monaco-button.chat-question-submit-button { - background: var(--vscode-button-background) !important; - color: var(--vscode-button-foreground) !important; - height: 22px; - width: auto; - flex: 0 0 auto; - min-width: auto; - padding: 0 8px; + .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close:hover:not(.disabled) { + background: var(--vscode-toolbar-hoverBackground) !important; } - .monaco-button.chat-question-submit-button:hover:not(.disabled) { - background: var(--vscode-button-hoverBackground) !important; - } - - .monaco-button.chat-question-cancel-button { - height: 22px; - width: auto; - flex: 0 0 auto; - min-width: auto; - padding: 0 8px; - } } /* summary (after finished) */ @@ -486,11 +445,13 @@ display: flex; flex-direction: column; gap: 8px; - padding: 16px; + padding: 8px; .chat-question-summary-item { display: flex; - flex-direction: column; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; gap: 0; font-size: var(--vscode-chat-font-size-body-s); } @@ -501,7 +462,19 @@ overflow-wrap: break-word; } - .chat-question-summary-answer { + .chat-question-summary-label::after { + content: ': '; + white-space: pre; + } + + .chat-question-summary-answer-title { + color: var(--vscode-foreground); + font-weight: 600; + word-wrap: break-word; + overflow-wrap: break-word; + } + + .chat-question-summary-answer-desc { color: var(--vscode-foreground); word-wrap: break-word; overflow-wrap: break-word; @@ -512,15 +485,4 @@ font-style: italic; font-size: var(--vscode-chat-font-size-body-s); } - - .chat-question-summary-empty { - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-chat-font-size-body-s); - padding: 0; - } - - .chat-question-summary-unanswered { - color: var(--vscode-descriptionForeground); - font-style: italic; - } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index 5cca23e2761..bf7dd424e25 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -53,22 +53,6 @@ function truncateToLimit(value: string | undefined, limit: number): string | und return value; } -export function formatHeaderForDisplay(header: string): string { - const normalized = header - .trim() - .replace(/[_-]+/g, ' ') - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') - .replace(/\s+/g, ' ') - .trim(); - - if (!normalized) { - return header; - } - - return normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase(); -} - export interface IQuestionOption { readonly label: string; readonly description?: string; @@ -211,7 +195,7 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { throw new CancellationError(); } - progress.report({ message: localize('askQuestionsTool.progress', 'Reviewing your answers') }); + progress.report({ message: localize('askQuestionsTool.progress', 'Analyzing your answers...') }); const converted = this.convertCarouselAnswers(questions, answerResult?.answers, idToHeaderMap); const { answeredCount, skippedCount, freeTextCount, recommendedAvailableCount, recommendedSelectedCount } = this.collectMetrics(questions, converted); @@ -316,9 +300,8 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { const internalId = generateUuid(); idToHeaderMap.set(internalId, question.header); - // Format + truncate header for display only; preserve original header for answer correlation - const formattedHeader = formatHeaderForDisplay(question.header); - const displayTitle = truncateToLimit(formattedHeader, HardLimits.header) ?? formattedHeader; + // Truncate header for display only + const displayTitle = truncateToLimit(question.header, HardLimits.header) ?? question.header; return { id: internalId, diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index f6520b66350..10045c7dce3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -58,7 +58,9 @@ suite('ChatQuestionCarouselPart', () => { createWidget(carousel); assert.ok(widget.domNode.classList.contains('chat-question-carousel-container')); + assert.ok(widget.domNode.querySelector('.chat-question-header-row')); assert.ok(widget.domNode.querySelector('.chat-question-carousel-content')); + assert.ok(widget.domNode.querySelector('.chat-question-carousel-nav')); }); test('renders question title', () => { @@ -123,7 +125,7 @@ suite('ChatQuestionCarouselPart', () => { assert.strictEqual(messageEl?.querySelector('.rendered-markdown'), null, 'plain string message should not use markdown renderer'); }); - test('renders tab bar for multi-question carousel', () => { + test('renders progress indicator correctly', () => { const carousel = createMockCarousel([ { id: 'q1', type: 'text', title: 'Question 1', message: 'Question 1' }, { id: 'q2', type: 'text', title: 'Question 2', message: 'Question 2' }, @@ -131,11 +133,11 @@ suite('ChatQuestionCarouselPart', () => { ]); createWidget(carousel); - const tabBar = widget.domNode.querySelector('.chat-question-tab-bar'); - assert.ok(tabBar, 'Tab bar should exist for multi-question carousel'); - const tabs = widget.domNode.querySelectorAll('.chat-question-tab'); - // 3 question tabs + 1 review tab - assert.strictEqual(tabs.length, 4, 'Should have 3 question tabs + 1 review tab'); + // Progress is shown in the step indicator in the footer as "1/3" + const stepIndicator = widget.domNode.querySelector('.chat-question-step-indicator'); + assert.ok(stepIndicator); + assert.ok(stepIndicator?.textContent?.includes('1')); + assert.ok(stepIndicator?.textContent?.includes('3')); }); }); @@ -269,16 +271,42 @@ suite('ChatQuestionCarouselPart', () => { }); suite('Navigation', () => { - test('single question has no tab bar or submit button', () => { + test('previous button is disabled on first question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } + ]); + createWidget(carousel); + + // Use dedicated class selectors for stability + const prevButton = widget.domNode.querySelector('.chat-question-nav-prev') as HTMLButtonElement; + assert.ok(prevButton, 'Previous button should exist'); + assert.ok(prevButton.classList.contains('disabled') || prevButton.disabled, 'Previous button should be disabled on first question'); + }); + + test('next button stays as arrow and is disabled on last question', () => { const carousel = createMockCarousel([ { id: 'q1', type: 'text', title: 'Only Question' } ]); createWidget(carousel); - const tabBar = widget.domNode.querySelector('.chat-question-tab-bar'); - assert.strictEqual(tabBar, null, 'Tab bar should not exist for single question'); - const submitButton = widget.domNode.querySelector('.chat-question-submit-button'); - assert.strictEqual(submitButton, null, 'Submit button is only in review panel for multi-question'); + // Use dedicated class selector for stability + const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLButtonElement; + assert.ok(nextButton, 'Next button should exist'); + assert.strictEqual(nextButton.getAttribute('aria-label'), 'Next', 'Next button should preserve Next aria-label on last question'); + assert.ok(nextButton.classList.contains('disabled') || nextButton.disabled, 'Next button should be disabled on last question'); + }); + + test('submit button is shown on last question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Only Question' } + ]); + createWidget(carousel); + + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; + assert.ok(submitButton, 'Submit button should exist'); + assert.strictEqual(submitButton.getAttribute('aria-label'), 'Submit'); + assert.notStrictEqual(submitButton.style.display, 'none', 'Submit button should be visible on last question'); }); }); @@ -373,14 +401,13 @@ suite('ChatQuestionCarouselPart', () => { suite('Accessibility', () => { test('navigation area has proper role and aria-label', () => { const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Question 1' }, - { id: 'q2', type: 'text', title: 'Question 2' } + { id: 'q1', type: 'text', title: 'Question 1' } ]); createWidget(carousel); - const tabList = widget.domNode.querySelector('.chat-question-tabs'); - assert.strictEqual(tabList?.getAttribute('role'), 'tablist'); - assert.ok(tabList?.getAttribute('aria-label'), 'Tab list should have aria-label'); + const nav = widget.domNode.querySelector('.chat-question-carousel-nav'); + assert.strictEqual(nav?.getAttribute('role'), 'navigation'); + assert.ok(nav?.getAttribute('aria-label'), 'Navigation should have aria-label'); }); test('single select list has proper role and aria-label', () => { @@ -559,20 +586,19 @@ suite('ChatQuestionCarouselPart', () => { ], true); const firstWidget = createWidget(carousel); - // Click the second tab to navigate - const tabs = firstWidget.domNode.querySelectorAll('.chat-question-tab'); - assert.ok(tabs.length >= 2, 'should have at least 2 tabs'); - (tabs[1] as HTMLElement).click(); - - // Verify navigation happened - assert.strictEqual(tabs[1].getAttribute('aria-selected'), 'true', 'second tab should be selected after click'); + const nextButton = firstWidget.domNode.querySelector('.chat-question-nav-next') as HTMLElement | null; + assert.ok(nextButton, 'next button should exist'); + nextButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); firstWidget.dispose(); firstWidget.domNode.remove(); const recreatedWidget = createWidget(carousel); - const recreatedTabs = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); - assert.strictEqual(recreatedTabs[1]?.getAttribute('aria-selected'), 'true', 'should restore to second tab after recreation'); + const stepIndicator = recreatedWidget.domNode.querySelector('.chat-question-step-indicator'); + assert.strictEqual(stepIndicator?.textContent, '2/2', 'should restore the current question index after navigation'); + + const title = recreatedWidget.domNode.querySelector('.chat-question-title'); + assert.ok(title?.textContent?.includes('Question 2'), 'should restore to the second question view'); }); test('retains draft answers and current question after widget recreation', () => { @@ -587,9 +613,9 @@ suite('ChatQuestionCarouselPart', () => { firstInput.value = 'first draft answer'; firstInput.dispatchEvent(new Event('input', { bubbles: true })); - // Click the second tab to navigate - const tabs = firstWidget.domNode.querySelectorAll('.chat-question-tab'); - (tabs[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); + const nextButton = firstWidget.domNode.querySelector('.chat-question-nav-next') as HTMLElement | null; + assert.ok(nextButton, 'next button should exist'); + nextButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); const secondInput = firstWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(secondInput, 'second question input should exist'); @@ -600,16 +626,16 @@ suite('ChatQuestionCarouselPart', () => { firstWidget.domNode.remove(); const recreatedWidget = createWidget(carousel); - const recreatedTabs = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); - assert.strictEqual(recreatedTabs[1]?.getAttribute('aria-selected'), 'true', 'should restore the current question index'); + const stepIndicator = recreatedWidget.domNode.querySelector('.chat-question-step-indicator'); + assert.strictEqual(stepIndicator?.textContent, '2/2', 'should restore the current question index'); const recreatedSecondInput = recreatedWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(recreatedSecondInput, 'recreated second question input should exist'); assert.strictEqual(recreatedSecondInput.value, 'second draft answer', 'should restore draft input for current question'); - // Click the first tab to go back - const recreatedTabsAgain = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); - (recreatedTabsAgain[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); + const prevButton = recreatedWidget.domNode.querySelector('.chat-question-nav-prev') as HTMLElement | null; + assert.ok(prevButton, 'previous button should exist'); + prevButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); const recreatedFirstInput = recreatedWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(recreatedFirstInput, 'recreated first question input should exist'); @@ -629,7 +655,7 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(summary, 'Should show summary container after skip'); const summaryItem = summary?.querySelector('.chat-question-summary-item'); assert.ok(summaryItem, 'Should have summary item for the question'); - const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer'); + const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer-title'); assert.ok(summaryValue?.textContent?.includes('default answer'), 'Summary should show the default answer'); }); @@ -663,7 +689,7 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); assert.ok(summary, 'Should show summary container when isUsed is true'); - const summaryValue = summary?.querySelector('.chat-question-summary-answer'); + const summaryValue = summary?.querySelector('.chat-question-summary-answer-title'); assert.ok(summaryValue?.textContent?.includes('saved answer'), 'Summary should show saved answer from data'); }); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts index 7db5b9b6031..f82b6bbe55d 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js'; import { NullLogService } from '../../../../../../../platform/log/common/log.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { AskQuestionsTool, formatHeaderForDisplay, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; +import { AskQuestionsTool, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; class TestableAskQuestionsTool extends AskQuestionsTool { @@ -149,20 +149,4 @@ suite('AskQuestionsTool - convertCarouselAnswers', () => { assert.deepStrictEqual(result.answers['Case'], { selected: [], freeText: 'yes', skipped: false }); }); - - test('formats headers for carousel tab title display', () => { - assert.deepStrictEqual([ - formatHeaderForDisplay('FocusArea'), - formatHeaderForDisplay('UserValue'), - formatHeaderForDisplay('RiskLevel'), - formatHeaderForDisplay('Already Spaced'), - formatHeaderForDisplay('snake_case_header'), - ], [ - 'Focus area', - 'User value', - 'Risk level', - 'Already spaced', - 'Snake case header', - ]); - }); }); From 48661ac8515d645519fb9f4185f43eb94b8088d7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:06:32 +0000 Subject: [PATCH 168/448] Fix singleSelect freeform answer showing [object Object] after reload (#299235) --- .../widget/chatContentParts/chatQuestionCarouselPart.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 47ba2a1fa5d..7aea002a1da 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -1327,6 +1327,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } return selectedLabel ?? String(selectedValue ?? ''); } + // Handle case where selectedValue was stripped during JSON serialization (undefined values are omitted by JSON.stringify) + if (typeof answer === 'object' && answer !== null && hasKey(answer, { freeformValue: true })) { + return (answer as { freeformValue?: string }).freeformValue ?? ''; + } const label = question.options?.find(opt => opt.value === answer)?.label; return label ?? String(answer); } From 587cae66663400dd8f819516f91fffcfe1531841 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:17:14 -0800 Subject: [PATCH 169/448] Bump xterm for imageAdded api (#299073) --- package-lock.json | 96 ++++++++++++++++++------------------ package.json | 22 ++++----- remote/package-lock.json | 96 ++++++++++++++++++------------------ remote/package.json | 20 ++++---- remote/web/package-lock.json | 88 ++++++++++++++++----------------- remote/web/package.json | 18 +++---- 6 files changed, 170 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9728ddebc18..90045ca2ed9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,16 +30,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3772,30 +3772,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.167.tgz", - "integrity": "sha512-+JSjagAk6okCaGVYFwkKl8qIBfy+W+h7p/qULIi9cC8QyeswOLaE4GOqY5yuGNQYU+zMlrpgR1ttyp0o6y9LHg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", + "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.167.tgz", - "integrity": "sha512-Bxi2oTaX7YM1gup0OSv02n9+tA3P1Ozlu5zyB/ZwSVkepB9FOxCODWD0l3DhWyLGMBqQ+OY/COw5SRxrKyvkNg==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", + "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.167.tgz", - "integrity": "sha512-d+9ANnoz6D4J06CjronVolcG+J0jqUWQXbzciRqQkHq0or5k8PYuIj2DuuyBx/0rOaN7JYN347KQ9iylk+++xA==", + "version": "0.11.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", + "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -3805,7 +3805,7 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { @@ -3827,63 +3827,63 @@ "license": "ISC" }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.167.tgz", - "integrity": "sha512-8eeaWnp0pnjYaKtOLsXVCE0hTFXS0A2kZCciWp52l6CbNGQsnky4VNWJXKaJrGbS+RHGxT6qWgcB+Mx5ETzZfg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", + "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.167.tgz", - "integrity": "sha512-1K6POdu0iCdjtW0Bs2z3IGWpMU4gJypbYxGnecbGnsH86rNRGwAKS0bKwWlHAiUQLlOxSGxTiNbazbrDln03FQ==", + "version": "0.17.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", + "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.167.tgz", - "integrity": "sha512-7EK/PN7QaUZcNE+bHmt7ELSNK3OBR2UZEuqNkE/0ooha7KqqI9mxZQG53Yn6wYcmRip34OFW9YF49kbTCkFuBg==", + "version": "0.15.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", + "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.167.tgz", - "integrity": "sha512-uOJCfsMhML8GTesUKqCC4CH2cPH9yCIFnixiwgpcE5eLVrLszXW3tny25S/bu6EM+rfvE4nwIvLNTMrQYYnMFA==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", + "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.166", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.166.tgz", - "integrity": "sha512-SZmz7HDeSMc4O0++x14ma/UWbK/0Ea8AikHw6V5ex/shjrjwbik7Uf2n8FfG2zMYNgBakvCy/SbwDPtQN+IbRQ==", + "version": "0.20.0-beta.167", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", + "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.167.tgz", - "integrity": "sha512-8TokXIwL8UeHhR4mAlUurzrqku5xaDXsikNi0HWpTcPCtZPdntxW36OaHxJmmpuHc8CecdaJehSuhApeW2TuZw==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.168.tgz", + "integrity": "sha512-9E8teq95/Hxv0r8WLMxfTgqnr1mDFiPpDJH71ZLWyb9TS9jAPQIoJF1h5FqOKx64NBxSQAJUJ7kr4yYbRWeE7g==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.167.tgz", - "integrity": "sha512-OOG2gcH9OhEjY+KW3X2s30e1KzaRlynhkF9/oKfb2PNUJBYUdXeww4YAugrz7+nLP8KxCeOdSJrq7VvRzyZrwA==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", + "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index 91ffe79555c..2a315661387 100644 --- a/package.json +++ b/package.json @@ -100,16 +100,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -242,4 +242,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} \ No newline at end of file +} diff --git a/remote/package-lock.json b/remote/package-lock.json index 49b97cb47d0..bd515187f0d 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -22,16 +22,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -578,30 +578,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.167.tgz", - "integrity": "sha512-+JSjagAk6okCaGVYFwkKl8qIBfy+W+h7p/qULIi9cC8QyeswOLaE4GOqY5yuGNQYU+zMlrpgR1ttyp0o6y9LHg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", + "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.167.tgz", - "integrity": "sha512-Bxi2oTaX7YM1gup0OSv02n9+tA3P1Ozlu5zyB/ZwSVkepB9FOxCODWD0l3DhWyLGMBqQ+OY/COw5SRxrKyvkNg==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", + "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.167.tgz", - "integrity": "sha512-d+9ANnoz6D4J06CjronVolcG+J0jqUWQXbzciRqQkHq0or5k8PYuIj2DuuyBx/0rOaN7JYN347KQ9iylk+++xA==", + "version": "0.11.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", + "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -611,67 +611,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.167.tgz", - "integrity": "sha512-8eeaWnp0pnjYaKtOLsXVCE0hTFXS0A2kZCciWp52l6CbNGQsnky4VNWJXKaJrGbS+RHGxT6qWgcB+Mx5ETzZfg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", + "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.167.tgz", - "integrity": "sha512-1K6POdu0iCdjtW0Bs2z3IGWpMU4gJypbYxGnecbGnsH86rNRGwAKS0bKwWlHAiUQLlOxSGxTiNbazbrDln03FQ==", + "version": "0.17.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", + "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.167.tgz", - "integrity": "sha512-7EK/PN7QaUZcNE+bHmt7ELSNK3OBR2UZEuqNkE/0ooha7KqqI9mxZQG53Yn6wYcmRip34OFW9YF49kbTCkFuBg==", + "version": "0.15.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", + "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.167.tgz", - "integrity": "sha512-uOJCfsMhML8GTesUKqCC4CH2cPH9yCIFnixiwgpcE5eLVrLszXW3tny25S/bu6EM+rfvE4nwIvLNTMrQYYnMFA==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", + "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.166", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.166.tgz", - "integrity": "sha512-SZmz7HDeSMc4O0++x14ma/UWbK/0Ea8AikHw6V5ex/shjrjwbik7Uf2n8FfG2zMYNgBakvCy/SbwDPtQN+IbRQ==", + "version": "0.20.0-beta.167", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", + "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.167.tgz", - "integrity": "sha512-8TokXIwL8UeHhR4mAlUurzrqku5xaDXsikNi0HWpTcPCtZPdntxW36OaHxJmmpuHc8CecdaJehSuhApeW2TuZw==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.168.tgz", + "integrity": "sha512-9E8teq95/Hxv0r8WLMxfTgqnr1mDFiPpDJH71ZLWyb9TS9jAPQIoJF1h5FqOKx64NBxSQAJUJ7kr4yYbRWeE7g==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.167.tgz", - "integrity": "sha512-OOG2gcH9OhEjY+KW3X2s30e1KzaRlynhkF9/oKfb2PNUJBYUdXeww4YAugrz7+nLP8KxCeOdSJrq7VvRzyZrwA==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", + "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index 2de8217e16d..cef1d91c5ca 100644 --- a/remote/package.json +++ b/remote/package.json @@ -17,16 +17,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 0cd6aee9fb7..0452b9cff95 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -14,15 +14,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/xterm": "^6.1.0-beta.168", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", @@ -100,30 +100,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.167.tgz", - "integrity": "sha512-+JSjagAk6okCaGVYFwkKl8qIBfy+W+h7p/qULIi9cC8QyeswOLaE4GOqY5yuGNQYU+zMlrpgR1ttyp0o6y9LHg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", + "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.167.tgz", - "integrity": "sha512-Bxi2oTaX7YM1gup0OSv02n9+tA3P1Ozlu5zyB/ZwSVkepB9FOxCODWD0l3DhWyLGMBqQ+OY/COw5SRxrKyvkNg==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", + "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.167.tgz", - "integrity": "sha512-d+9ANnoz6D4J06CjronVolcG+J0jqUWQXbzciRqQkHq0or5k8PYuIj2DuuyBx/0rOaN7JYN347KQ9iylk+++xA==", + "version": "0.11.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", + "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -133,58 +133,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.167.tgz", - "integrity": "sha512-8eeaWnp0pnjYaKtOLsXVCE0hTFXS0A2kZCciWp52l6CbNGQsnky4VNWJXKaJrGbS+RHGxT6qWgcB+Mx5ETzZfg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", + "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.167.tgz", - "integrity": "sha512-1K6POdu0iCdjtW0Bs2z3IGWpMU4gJypbYxGnecbGnsH86rNRGwAKS0bKwWlHAiUQLlOxSGxTiNbazbrDln03FQ==", + "version": "0.17.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", + "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.167.tgz", - "integrity": "sha512-7EK/PN7QaUZcNE+bHmt7ELSNK3OBR2UZEuqNkE/0ooha7KqqI9mxZQG53Yn6wYcmRip34OFW9YF49kbTCkFuBg==", + "version": "0.15.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", + "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.167.tgz", - "integrity": "sha512-uOJCfsMhML8GTesUKqCC4CH2cPH9yCIFnixiwgpcE5eLVrLszXW3tny25S/bu6EM+rfvE4nwIvLNTMrQYYnMFA==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", + "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.166", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.166.tgz", - "integrity": "sha512-SZmz7HDeSMc4O0++x14ma/UWbK/0Ea8AikHw6V5ex/shjrjwbik7Uf2n8FfG2zMYNgBakvCy/SbwDPtQN+IbRQ==", + "version": "0.20.0-beta.167", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", + "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.167.tgz", - "integrity": "sha512-OOG2gcH9OhEjY+KW3X2s30e1KzaRlynhkF9/oKfb2PNUJBYUdXeww4YAugrz7+nLP8KxCeOdSJrq7VvRzyZrwA==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", + "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index a641d6346c0..d91fc919490 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -9,15 +9,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/xterm": "^6.1.0-beta.168", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", From a19a6969a283679dcf5a0155708a56ec359c19c2 Mon Sep 17 00:00:00 2001 From: Elie Richa Date: Wed, 4 Mar 2026 19:17:46 +0100 Subject: [PATCH 170/448] Include remote debug extension host env in remote terminal shell env (#299007) * Include remote debug extension host env in remote terminal shell env * Update src/vs/workbench/services/environment/browser/environmentService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../terminal/common/remote/remoteTerminalChannel.ts | 10 +++++++++- .../services/environment/browser/environmentService.ts | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts index 6b6e849e344..6cdd98743ab 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IChannel } from '../../../../../base/parts/ipc/common/ipc.js'; import { IWorkbenchConfigurationService } from '../../../../services/configuration/common/configuration.js'; import { IRemoteAuthorityResolverService } from '../../../../../platform/remote/common/remoteAuthorityResolver.js'; +import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { serializeEnvironmentDescriptionMap, serializeEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariableShared.js'; import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js'; @@ -111,6 +112,7 @@ export class RemoteTerminalChannelClient implements IPtyHostController { @ITerminalLogService private readonly _logService: ITerminalLogService, @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, ) { } restartPtyHost(): Promise { @@ -152,7 +154,13 @@ export class RemoteTerminalChannelClient implements IPtyHostController { } const resolverResult = await this._remoteAuthorityResolverService.resolveAuthority(this._remoteAuthority); - const resolverEnv = resolverResult.options && resolverResult.options.extensionHostEnv; + const resolverEnv = { + /** + * If the extension host was spawned via a launch configuration, + * include the environment provided by that launch configuration. + */ + ...(this._environmentService.debugExtensionHost.env ?? {}), ...resolverResult.options?.extensionHostEnv + }; const workspace = this._workspaceContextService.getWorkspace(); const workspaceFolders = workspace.folders; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index d0bc952a874..e8d460f7967 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -322,6 +322,13 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi case 'inspect-extensions': extensionHostDebugEnvironment.params.port = parseInt(value); break; + case 'extensionEnvironment': + try { + extensionHostDebugEnvironment.params.env = JSON.parse(value); + } catch (error) { + onUnexpectedError(error); + } + break; case 'enableProposedApi': extensionHostDebugEnvironment.extensionEnabledProposedApi = []; break; From 4ab6fddf3e3ff2c19bb11a35648ede790e886c5f Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 4 Mar 2026 18:44:45 +0100 Subject: [PATCH 171/448] Revert "Adds support for stronglyRecommended extensions. Implements #299039" This reverts commit 240196b5955050f4be2d98839d02483dda0465a8. --- .vscode/extensions.json | 6 +- src/vs/platform/dialogs/common/dialogs.ts | 12 -- .../common/extensionRecommendations.ts | 2 - .../common/extensionRecommendationsIpc.ts | 9 - .../browser/parts/dialogs/dialogHandler.ts | 9 +- ...ensionRecommendationNotificationService.ts | 169 ------------------ .../extensionRecommendationsService.ts | 10 -- .../browser/extensions.contribution.ts | 11 -- .../stronglyRecommendedExtensionList.ts | 99 ---------- .../browser/workspaceRecommendations.ts | 29 +-- .../common/extensionsFileTemplate.ts | 9 - .../common/workspaceExtensionsConfig.ts | 8 - .../stronglyRecommendedDialog.fixture.ts | 85 --------- 13 files changed, 3 insertions(+), 455 deletions(-) delete mode 100644 src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts delete mode 100644 src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index bd45eb0e570..3fb87652c81 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,15 +4,11 @@ "recommendations": [ "dbaeumer.vscode-eslint", "editorconfig.editorconfig", + "github.vscode-pull-request-github", "ms-vscode.vscode-github-issue-notebooks", "ms-vscode.extension-test-runner", "jrieken.vscode-pr-pinger", "typescriptteam.native-preview", "ms-vscode.ts-customized-language-service" - ], - "stronglyRecommended": [ - "github.vscode-pull-request-github", - "ms-vscode.vscode-extras", - "ms-vscode.vscode-selfhost-test-provider" ] } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 925b82a8a28..fc73e57f824 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -5,7 +5,6 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Event } from '../../../base/common/event.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { basename } from '../../../base/common/resources.js'; @@ -287,23 +286,12 @@ export const IDialogService = createDecorator('dialogService'); export interface ICustomDialogOptions { readonly buttonDetails?: string[]; - readonly buttonOptions?: Array; readonly markdownDetails?: ICustomDialogMarkdown[]; - readonly renderBody?: (container: HTMLElement, disposables: DisposableStore) => void; readonly classes?: string[]; readonly icon?: ThemeIcon; readonly disableCloseAction?: boolean; } -export interface ICustomDialogButtonOptions { - readonly sublabel?: string; - readonly styleButton?: (button: ICustomDialogButtonControl) => void; -} - -export interface ICustomDialogButtonControl { - set enabled(value: boolean); -} - export interface ICustomDialogMarkdown { readonly markdown: IMarkdownString; readonly classes?: string[]; diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index 00b2a6ce993..fe258ed580c 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -45,7 +45,5 @@ export interface IExtensionRecommendationNotificationService { promptImportantExtensionsInstallNotification(recommendations: IExtensionRecommendations): Promise; promptWorkspaceRecommendations(recommendations: Array): Promise; - promptStronglyRecommendedExtensions(recommendations: Array): Promise; - resetStronglyRecommendedIgnoreState(): void; } diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts index 28ae56a38be..da863e3c6e2 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../base/common/event.js'; -import { URI } from '../../../base/common/uri.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult } from './extensionRecommendations.js'; @@ -24,18 +23,10 @@ export class ExtensionRecommendationNotificationServiceChannelClient implements throw new Error('not supported'); } - promptStronglyRecommendedExtensions(recommendations: Array): Promise { - throw new Error('not supported'); - } - hasToIgnoreRecommendationNotifications(): boolean { throw new Error('not supported'); } - resetStronglyRecommendedIgnoreState(): void { - throw new Error('not supported'); - } - } export class ExtensionRecommendationNotificationServiceChannel implements IServerChannel { diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index be802600908..5af4f540bac 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -9,7 +9,6 @@ import { IConfirmation, IConfirmationResult, IInputResult, ICheckbox, IInputElem import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import Severity from '../../../../base/common/severity.js'; -import { IButton } from '../../../../base/browser/ui/button/button.js'; import { Dialog, IDialogResult } from '../../../../base/browser/ui/dialog/dialog.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -106,14 +105,8 @@ export class BrowserDialogHandler extends AbstractDialogHandler { parent.appendChild(result.element); result.element.classList.add(...(markdownDetail.classes || [])); }); - customOptions.renderBody?.(parent, dialogDisposables); } : undefined; - const buttonOptions = customOptions?.buttonOptions?.map(opt => opt ? { - sublabel: opt.sublabel, - styleButton: opt.styleButton ? (button: IButton) => opt.styleButton!(button) : undefined - } : undefined) ?? customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })); - const dialog = new Dialog( this.layoutService.activeContainer, message, @@ -125,7 +118,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler { renderBody, icon: customOptions?.icon, disableCloseAction: customOptions?.disableCloseAction, - buttonOptions, + buttonOptions: customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })), checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, inputs diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 3caf43bdc45..b70a49c4016 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -6,7 +6,6 @@ import { distinct } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -14,13 +13,11 @@ import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { renderStronglyRecommendedExtensionList, StronglyRecommendedExtensionListResult } from './stronglyRecommendedExtensionList.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; @@ -45,18 +42,6 @@ type ExtensionWorkspaceRecommendationsNotificationClassification = { const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore'; const donotShowWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; -const stronglyRecommendedIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedIgnore'; -const stronglyRecommendedMajorVersionIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedMajorVersionIgnore'; - -interface MajorVersionIgnoreEntry { - readonly id: string; - readonly majorVersion: number; -} - -function parseMajorVersion(version: string): number { - const major = parseInt(version.split('.')[0], 10); - return isNaN(major) ? 0 : major; -} type RecommendationsNotificationActions = { onDidInstallRecommendedExtensions(extensions: IExtension[]): void; @@ -147,7 +132,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple constructor( @IConfigurationService private readonly configurationService: IConfigurationService, - @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -484,159 +468,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple } } - async promptStronglyRecommendedExtensions(recommendations: Array): Promise { - if (this.hasToIgnoreRecommendationNotifications()) { - return; - } - - const ignoredList = this._getStronglyRecommendedIgnoreList(); - recommendations = recommendations.filter(rec => { - const key = isString(rec) ? rec.toLowerCase() : rec.toString(); - return !ignoredList.includes(key); - }); - if (!recommendations.length) { - return; - } - - let installed = await this.extensionManagementService.getInstalled(); - installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); - recommendations = recommendations.filter(recommendation => - installed.every(local => - isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location) - ) - ); - if (!recommendations.length) { - return; - } - - const allExtensions = await this.getInstallableExtensions(recommendations); - if (!allExtensions.length) { - return; - } - - const majorVersionIgnoreList = this._getStronglyRecommendedMajorVersionIgnoreList(); - const extensions = allExtensions.filter(ext => { - const ignored = majorVersionIgnoreList.find(e => e.id === ext.identifier.id.toLowerCase()); - return !ignored || parseMajorVersion(ext.version) > ignored.majorVersion; - }); - if (!extensions.length) { - return; - } - - const message = extensions.length === 1 - ? localize('stronglyRecommended1', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName) - : localize('stronglyRecommendedN', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length); - - let listResult!: StronglyRecommendedExtensionListResult; - - const { result } = await this.dialogService.prompt({ - message, - buttons: [ - { - label: localize('install', "Install"), - run: () => true, - }, - ], - cancelButton: localize('cancel', "Cancel"), - custom: { - icon: Codicon.extensions, - renderBody: (container, disposables) => { - listResult = renderStronglyRecommendedExtensionList(container, disposables, extensions); - }, - buttonOptions: [{ - styleButton: (button) => listResult.styleInstallButton(button), - }], - }, - }); - - if (result) { - const selected = extensions.filter(e => listResult.checkboxStates.get(e)); - const unselected = extensions.filter(e => !listResult.checkboxStates.get(e)); - if (unselected.length) { - this._addToStronglyRecommendedIgnoreList( - unselected.map(e => e.identifier.id) - ); - } - if (listResult.doNotShowAgainUnlessMajorVersionChange()) { - this._addToStronglyRecommendedIgnoreWithMajorVersion( - extensions.map(e => ({ id: e.identifier.id, majorVersion: parseMajorVersion(e.version) })) - ); - } - if (selected.length) { - const galleryExtensions: IGalleryExtension[] = []; - const resourceExtensions: IExtension[] = []; - for (const extension of selected) { - if (extension.gallery) { - galleryExtensions.push(extension.gallery); - } else if (extension.resourceExtension) { - resourceExtensions.push(extension); - } - } - await Promises.settled([ - Promises.settled(selected.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), - galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: {} }))) : Promise.resolve(), - resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve(), - ]); - } - } - } - - private _getStronglyRecommendedIgnoreList(): string[] { - const raw = this.storageService.get(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE); - if (raw === undefined) { - return []; - } - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - - private _addToStronglyRecommendedIgnoreList(recommendations: Array): void { - const list = this._getStronglyRecommendedIgnoreList(); - for (const rec of recommendations) { - const key = isString(rec) ? rec.toLowerCase() : rec.toString(); - if (!list.includes(key)) { - list.push(key); - } - } - this.storageService.store(stronglyRecommendedIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - private _getStronglyRecommendedMajorVersionIgnoreList(): MajorVersionIgnoreEntry[] { - const raw = this.storageService.get(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE); - if (raw === undefined) { - return []; - } - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - - private _addToStronglyRecommendedIgnoreWithMajorVersion(entries: MajorVersionIgnoreEntry[]): void { - const list = this._getStronglyRecommendedMajorVersionIgnoreList(); - for (const entry of entries) { - const key = entry.id.toLowerCase(); - const existing = list.findIndex(e => e.id === key); - if (existing !== -1) { - list[existing] = { id: key, majorVersion: entry.majorVersion }; - } else { - list.push({ id: key, majorVersion: entry.majorVersion }); - } - } - this.storageService.store(stronglyRecommendedMajorVersionIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - resetStronglyRecommendedIgnoreState(): void { - this.storageService.remove(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE); - this.storageService.remove(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE); - } - private setIgnoreRecommendationsConfig(configVal: boolean) { this.configurationService.updateValue('extensions.ignoreRecommendations', configVal); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 9cba507bb0b..12cf0a6c61f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -110,7 +110,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); this.promptWorkspaceRecommendations(); - this.promptStronglyRecommendedExtensions(); } private isEnabled(): boolean { @@ -275,15 +274,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte } } - private async promptStronglyRecommendedExtensions(): Promise { - const allowedRecommendations = this.workspaceRecommendations.stronglyRecommended - .filter(rec => !isString(rec) || this.isExtensionAllowedToBeRecommended(rec)); - - if (allowedRecommendations.length) { - await this.extensionRecommendationNotificationService.promptStronglyRecommendedExtensions(allowedRecommendations); - } - } - private _registerP(o: CancelablePromise): CancelablePromise { this._register(toDisposable(() => o.cancel())); return o; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 218741bdd99..37e6e916e16 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1895,17 +1895,6 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations') }); - this.registerExtensionAction({ - id: 'workbench.extensions.action.resetStronglyRecommendedIgnoreState', - title: localize2('workbench.extensions.action.resetStronglyRecommendedIgnoreState', "Reset Strongly Recommended Extensions Ignore State"), - category: EXTENSIONS_CATEGORY, - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.notEqualsTo('empty'), - }, - run: async (accessor: ServicesAccessor) => accessor.get(IExtensionRecommendationNotificationService).resetStronglyRecommendedIgnoreState() - }); - this.registerExtensionAction({ id: ConfigureWorkspaceRecommendedExtensionsAction.ID, title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, diff --git a/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts b/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts deleted file mode 100644 index c4cb34ce443..00000000000 --- a/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts +++ /dev/null @@ -1,99 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { $, addDisposableListener } from '../../../../base/browser/dom.js'; -import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { ICustomDialogButtonControl } from '../../../../platform/dialogs/common/dialogs.js'; -import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; - -export interface StronglyRecommendedExtensionEntry { - readonly displayName: string; - readonly publisherDisplayName: string; - readonly version: string; -} - -export interface StronglyRecommendedExtensionListResult { - readonly checkboxStates: ReadonlyMap; - readonly hasSelection: boolean; - readonly doNotShowAgainUnlessMajorVersionChange: () => boolean; - styleInstallButton(button: ICustomDialogButtonControl): void; -} - -export function renderStronglyRecommendedExtensionList( - container: HTMLElement, - disposables: DisposableStore, - extensions: readonly T[], -): StronglyRecommendedExtensionListResult { - const checkboxStates = new Map(); - const onSelectionChanged = disposables.add(new Emitter()); - - container.style.display = 'flex'; - container.style.flexDirection = 'column'; - container.style.gap = '8px'; - container.style.padding = '8px 0'; - - const updateCheckbox = (ext: T, cb: Checkbox) => { - checkboxStates.set(ext, cb.checked); - onSelectionChanged.fire(); - }; - - for (const ext of extensions) { - checkboxStates.set(ext, true); - - const row = container.appendChild($('.strongly-recommended-extension-row')); - row.style.display = 'flex'; - row.style.alignItems = 'center'; - row.style.gap = '8px'; - - const cb = disposables.add(new Checkbox(ext.displayName, true, defaultCheckboxStyles)); - disposables.add(cb.onChange(() => updateCheckbox(ext, cb))); - row.appendChild(cb.domNode); - - const label = row.appendChild($('span')); - label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`; - label.style.cursor = 'pointer'; - disposables.add(addDisposableListener(label, 'click', () => { - cb.checked = !cb.checked; - updateCheckbox(ext, cb); - })); - } - - const separator = container.appendChild($('div')); - separator.style.borderTop = '1px solid var(--vscode-widget-border)'; - separator.style.marginTop = '4px'; - separator.style.paddingTop = '4px'; - - const doNotShowRow = container.appendChild($('.strongly-recommended-do-not-show-row')); - doNotShowRow.style.display = 'flex'; - doNotShowRow.style.alignItems = 'center'; - doNotShowRow.style.gap = '8px'; - - const doNotShowCb = disposables.add(new Checkbox( - localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"), - false, - defaultCheckboxStyles, - )); - doNotShowRow.appendChild(doNotShowCb.domNode); - - const doNotShowLabel = doNotShowRow.appendChild($('span')); - doNotShowLabel.textContent = localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"); - doNotShowLabel.style.cursor = 'pointer'; - disposables.add(addDisposableListener(doNotShowLabel, 'click', () => { doNotShowCb.checked = !doNotShowCb.checked; })); - - const hasSelection = () => [...checkboxStates.values()].some(v => v); - - return { - checkboxStates, - get hasSelection() { return hasSelection(); }, - doNotShowAgainUnlessMajorVersionChange: () => doNotShowCb.checked, - styleInstallButton(button: ICustomDialogButtonControl) { - const updateEnabled = () => { button.enabled = hasSelection(); }; - disposables.add(onSelectionChanged.event(updateEnabled)); - }, - }; -} diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 8563dcc4c15..69ff685c658 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -25,9 +25,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { private _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } - private _stronglyRecommended: Array = []; - get stronglyRecommended(): ReadonlyArray { return this._stronglyRecommended; } - private _onDidChangeRecommendations = this._register(new Emitter()); readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; @@ -35,7 +32,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } private workspaceExtensions: URI[] = []; - private workspaceExtensionIds = new Map(); private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler; constructor( @@ -94,12 +90,8 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { // ignore } } - this.workspaceExtensionIds.clear(); if (workspaceExtensions.length) { const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions); - for (const ext of resourceExtensions) { - this.workspaceExtensionIds.set(ext.identifier.id.toLowerCase(), ext.location); - } return resourceExtensions.map(extension => extension.location); } return []; @@ -118,7 +110,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } this._recommendations = []; - this._stronglyRecommended = []; this._ignoredRecommendations = []; for (const extensionsConfig of extensionsConfigs) { @@ -142,24 +133,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } } - if (extensionsConfig.stronglyRecommended) { - for (const extensionId of extensionsConfig.stronglyRecommended) { - if (invalidRecommendations.indexOf(extensionId) === -1) { - const workspaceExtUri = this.workspaceExtensionIds.get(extensionId.toLowerCase()); - const extension = workspaceExtUri ?? extensionId; - const reason = { - reasonId: ExtensionRecommendationReason.Workspace, - reasonText: localize('stronglyRecommendedExtension', "This extension is strongly recommended by users of the current workspace.") - }; - this._stronglyRecommended.push(extension); - if (workspaceExtUri) { - this._recommendations.push({ extension: workspaceExtUri, reason }); - } else { - this._recommendations.push({ extension: extensionId, reason }); - } - } - } - } } for (const extension of this.workspaceExtensions) { @@ -179,7 +152,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { const invalidExtensions: string[] = []; let message = ''; - const allRecommendations = distinct(contents.flatMap(({ recommendations, stronglyRecommended }) => [...(recommendations || []), ...(stronglyRecommended || [])])); + const allRecommendations = distinct(contents.flatMap(({ recommendations }) => recommendations || [])); const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN); for (const extensionId of allRecommendations) { if (regEx.test(extensionId)) { diff --git a/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts b/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts index 574806a1bc8..818e662847e 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts @@ -25,15 +25,6 @@ export const ExtensionsConfigurationSchema: IJSONSchema = { errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }, }, - stronglyRecommended: { - type: 'array', - description: localize('app.extensions.json.stronglyRecommended', "List of extensions that are strongly recommended for users of this workspace. Users will be prompted with a dialog to install these extensions. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), - items: { - type: 'string', - pattern: EXTENSION_IDENTIFIER_PATTERN, - errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") - }, - }, unwantedRecommendations: { type: 'array', description: localize('app.extensions.json.unwantedRecommendations', "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), diff --git a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts index ba02eaf679f..a48ce69a12b 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts @@ -24,7 +24,6 @@ export const EXTENSIONS_CONFIG = '.vscode/extensions.json'; export interface IExtensionsConfigContent { recommendations?: string[]; - stronglyRecommended?: string[]; unwantedRecommendations?: string[]; } @@ -36,7 +35,6 @@ export interface IWorkspaceExtensionsConfigService { readonly onDidChangeExtensionsConfigs: Event; getExtensionsConfigs(): Promise; getRecommendations(): Promise; - getStronglyRecommended(): Promise; getUnwantedRecommendations(): Promise; toggleRecommendation(extensionId: string): Promise; @@ -86,11 +84,6 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor return distinct(configs.flatMap(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : [])); } - async getStronglyRecommended(): Promise { - const configs = await this.getExtensionsConfigs(); - return distinct(configs.flatMap(c => c.stronglyRecommended ? c.stronglyRecommended.map(c => c.toLowerCase()) : [])); - } - async getUnwantedRecommendations(): Promise { const configs = await this.getExtensionsConfigs(); return distinct(configs.flatMap(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : [])); @@ -303,7 +296,6 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent { return { recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())), - stronglyRecommended: distinct((extensionsConfigContent.stronglyRecommended || []).map(e => e.toLowerCase())), unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase())) }; } diff --git a/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts deleted file mode 100644 index def925aad9c..00000000000 --- a/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; -import { localize } from '../../../../nls.js'; -import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { StronglyRecommendedExtensionEntry, renderStronglyRecommendedExtensionList } from '../../../contrib/extensions/browser/stronglyRecommendedExtensionList.js'; -import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; - -export default defineThemedFixtureGroup({ - TwoExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions) }), - SingleExtension: defineComponentFixture({ render: ctx => renderDialog(ctx, singleExtension) }), - ManyExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, manyExtensions) }), - NoneSelected: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions, { allUnchecked: true }) }), -}); - -const twoExtensions: StronglyRecommendedExtensionEntry[] = [ - { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, - { displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' }, -]; - -const singleExtension: StronglyRecommendedExtensionEntry[] = [ - { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, -]; - -const manyExtensions: StronglyRecommendedExtensionEntry[] = [ - { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, - { displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' }, - { displayName: 'ESLint', publisherDisplayName: 'Dirk Baeumer', version: '2.4.4' }, - { displayName: 'Prettier', publisherDisplayName: 'Esben Petersen', version: '10.1.0' }, - { displayName: 'GitLens', publisherDisplayName: 'GitKraken', version: '15.6.2' }, -]; - -function renderDialog({ container, disposableStore }: ComponentFixtureContext, extensions: StronglyRecommendedExtensionEntry[], options?: { allUnchecked?: boolean }): void { - container.style.width = '700px'; - container.style.height = '500px'; - container.style.position = 'relative'; - container.style.overflow = 'hidden'; - - // The dialog uses position:fixed on its modal block, which escapes the shadow DOM container. - // Override to position:absolute so it stays within the fixture bounds. - const fixtureStyle = new CSSStyleSheet(); - fixtureStyle.replaceSync('.monaco-dialog-modal-block { position: absolute; }'); - const shadowRoot = container.getRootNode() as ShadowRoot; - shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, fixtureStyle]; - - const message = extensions.length === 1 - ? localize('strongExtensionFixture', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName) - : localize('strongExtensionsFixture', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length); - - let listResult!: ReturnType; - - const dialog = disposableStore.add(new Dialog( - container, - message, - [ - localize('install', "Install"), - localize('cancel', "Cancel"), - ], - { - type: 'info', - renderBody: (bodyContainer: HTMLElement) => { - listResult = renderStronglyRecommendedExtensionList(bodyContainer, disposableStore, extensions); - }, - buttonOptions: [{ - styleButton: (button) => listResult.styleInstallButton(button), - }], - cancelId: 1, - buttonStyles: defaultButtonStyles, - checkboxStyles: defaultCheckboxStyles, - inputBoxStyles: defaultInputBoxStyles, - dialogStyles: defaultDialogStyles, - } - )); - - dialog.show(); - - if (options?.allUnchecked) { - for (const cb of container.querySelectorAll('.strongly-recommended-extension-row .monaco-custom-toggle')) { - cb.click(); - } - } -} From 252340a237d9faf9440903e22480d930f7a3e3ef Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:34:24 +0100 Subject: [PATCH 172/448] Engineering - update notebooks (#299232) Co-authored-by: Johannes Rieken --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d3f716f5749..40235fad54e 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"February 2026\"" + "value": "$MILESTONE=milestone:\"1.111.0\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index b58910ad675..50ebbfdb750 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"February 2026\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.111.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, From 7dee96c44e9076728389953826d5e14be7f14ddd Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:02:44 -0800 Subject: [PATCH 173/448] chore: fix serialize-javascript alerts (#299248) --- package-lock.json | 67 ++++++++++++++++------------------- package.json | 3 +- test/monaco/package-lock.json | 48 ++----------------------- test/sanity/package-lock.json | 39 +++----------------- test/sanity/package.json | 3 ++ 5 files changed, 43 insertions(+), 117 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90045ca2ed9..d031af36fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1055,29 +1055,6 @@ "node": ">=0.4.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/@gulp-sourcemaps/map-sources": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", @@ -13734,6 +13711,31 @@ "node": ">=0.10.0" } }, + "node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true, + "license": "ISC" + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -13990,15 +13992,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14879,13 +14872,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-static": { diff --git a/package.json b/package.json index 2a315661387..7567da0e40b 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,8 @@ "node-gyp-build": "4.8.1", "kerberos@2.1.1": { "node-addon-api": "7.1.0" - } + }, + "serialize-javascript": "^7.0.3" }, "repository": { "type": "git", diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index e1e44348099..45660fad3e8 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -1552,16 +1552,6 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -1629,27 +1619,6 @@ "node": ">=8" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -1680,16 +1649,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -1836,16 +1795,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { diff --git a/test/sanity/package-lock.json b/test/sanity/package-lock.json index 79d178a5f5c..c441eb7cf18 100644 --- a/test/sanity/package-lock.json +++ b/test/sanity/package-lock.json @@ -918,15 +918,6 @@ "node": ">=18" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -949,33 +940,13 @@ "node": ">=0.10.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/shebang-command": { diff --git a/test/sanity/package.json b/test/sanity/package.json index 0b281e7a2ca..93d586f98e0 100644 --- a/test/sanity/package.json +++ b/test/sanity/package.json @@ -20,5 +20,8 @@ "@types/mocha": "^10.0.10", "@types/node": "22.x", "typescript": "^6.0.0-dev.20251110" + }, + "overrides": { + "serialize-javascript": "^7.0.3" } } From 54780aa5779cdec62af2fe72689b1efa44ec23f2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 4 Mar 2026 20:03:28 +0100 Subject: [PATCH 174/448] Sessions: Ask for workspace trust when picking a folder in new chat view (#299242) * feat: enhance folder selection with workspace trust verification * feat: add workspace trust request for local targets in NewChatWidget * feat: implement workspace trust request for folder selection in NewChatWidget * fix: update trust message for folder selection in NewChatWidget * fix: update trust message to clarify agent session capabilities in folder selection * fix: handle folder selection trust request and restore previous selection if untrusted * fix: simplify trust message for folder selection in NewChatWidget * fix: refactor folder trust handling in NewChatWidget for improved clarity and maintainability * fix: make _setNewSession and folder trust request asynchronous in NewChatWidget * fix: update cursor styles for send button states in chat widget * fix: refactor folder trust handling in NewChatWidget for improved session management * fix: simplify default repository URI handling in NewChatWidget * fix: handle undefined default repository URI in new session creation * fix: remove 'diffEditor.renderSideBySide' configuration from default settings * fix: remove unused workspace context service from NewChatWidget --- .../contrib/chat/browser/folderPicker.ts | 27 ++++++------ .../contrib/chat/browser/media/chatWidget.css | 5 +++ .../contrib/chat/browser/newChatViewPane.ts | 41 ++++++++++++++++--- .../browser/configuration.contribution.ts | 1 - 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/folderPicker.ts b/src/vs/sessions/contrib/chat/browser/folderPicker.ts index dbc253e1cbf..29411f26b73 100644 --- a/src/vs/sessions/contrib/chat/browser/folderPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/folderPicker.ts @@ -17,7 +17,6 @@ import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs. import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { INewSession } from './newSession.js'; const STORAGE_KEY_LAST_FOLDER = 'agentSessions.lastPickedFolder'; const STORAGE_KEY_RECENT_FOLDERS = 'agentSessions.recentlyPickedFolders'; @@ -43,7 +42,6 @@ export class FolderPicker extends Disposable { private _selectedFolderUri: URI | undefined; private _recentlyPickedFolders: URI[] = []; - private _newSession: INewSession | undefined; private _triggerElement: HTMLElement | undefined; private readonly _renderDisposables = this._register(new DisposableStore()); @@ -52,14 +50,6 @@ export class FolderPicker extends Disposable { return this._selectedFolderUri; } - /** - * Sets the pending session that this picker writes to. - * When the user selects a folder, it calls `setRepoUri` on the session. - */ - setNewSession(session: INewSession | undefined): void { - this._newSession = session; - } - constructor( @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, @IStorageService private readonly storageService: IStorageService, @@ -162,7 +152,7 @@ export class FolderPicker extends Disposable { } /** - * Programmatically set the selected folder. + * Programmatically set the selected folder (e.g. restoring draft state). */ setSelectedFolder(folderUri: URI): void { this._selectFolder(folderUri); @@ -181,7 +171,6 @@ export class FolderPicker extends Disposable { this._addToRecentlyPickedFolders(folderUri); this.storageService.store(STORAGE_KEY_LAST_FOLDER, folderUri.toString(), StorageScope.PROFILE, StorageTarget.MACHINE); this._updateTriggerLabel(this._triggerElement); - this._newSession?.setRepoUri(folderUri); this._onDidSelectFolder.fire(folderUri); } @@ -271,6 +260,20 @@ export class FolderPicker extends Disposable { return items; } + /** + * Removes a folder from the recently picked list and storage. + */ + removeFromRecents(folderUri: URI): void { + this._recentlyPickedFolders = this._recentlyPickedFolders.filter(f => !isEqual(f, folderUri)); + this.storageService.store(STORAGE_KEY_RECENT_FOLDERS, JSON.stringify(this._recentlyPickedFolders.map(f => f.toString())), StorageScope.PROFILE, StorageTarget.MACHINE); + // If this was the last picked folder, clear it + if (this._selectedFolderUri && isEqual(this._selectedFolderUri, folderUri)) { + this._selectedFolderUri = undefined; + this.storageService.remove(STORAGE_KEY_LAST_FOLDER, StorageScope.PROFILE); + this._updateTriggerLabel(this._triggerElement); + } + } + private _removeFolder(folderUri: URI): void { this._recentlyPickedFolders = this._recentlyPickedFolders.filter(f => !isEqual(f, folderUri)); this.storageService.store(STORAGE_KEY_RECENT_FOLDERS, JSON.stringify(this._recentlyPickedFolders.map(f => f.toString())), StorageScope.PROFILE, StorageTarget.MACHINE); diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index 247b4cdae06..ef3a5d80a79 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -115,6 +115,11 @@ color: var(--vscode-icon-foreground); background: transparent !important; border: none !important; + cursor: pointer; +} + +.sessions-chat-send-button .monaco-button.disabled { + cursor: default; } .sessions-chat-send-button .monaco-button:not(.disabled):hover { diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 8f78d2590da..47c38236ad3 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -49,6 +49,7 @@ import { EnhancedModelPickerActionItem } from '../../../../workbench/contrib/cha import { IChatInputPickerOptions } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js'; import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js'; import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js'; import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; @@ -184,10 +185,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILogService private readonly logService: ILogService, @IHoverService private readonly hoverService: IHoverService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, @IGitService private readonly gitService: IGitService, @IStorageService private readonly storageService: IStorageService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, ) { super(); this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); @@ -223,7 +224,11 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._focusEditor(); })); - this._register(this._folderPicker.onDidSelectFolder(() => { + this._register(this._folderPicker.onDidSelectFolder(async (folderUri) => { + const trusted = await this._requestFolderTrust(folderUri); + if (trusted) { + this._newSession.value?.setRepoUri(folderUri); + } this._updateDraftState(); this._focusEditor(); })); @@ -329,7 +334,16 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private async _createNewSession(): Promise { const target = this._targetPicker.selectedTarget; - const defaultRepoUri = this._folderPicker.selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + let defaultRepoUri = this._folderPicker.selectedFolderUri; + + // For local targets, request workspace trust before creating the session + if (target === AgentSessionProviders.Background && defaultRepoUri) { + const trusted = await this._requestFolderTrust(defaultRepoUri); + if (!trusted) { + defaultRepoUri = undefined; + } + } + const resource = getResourceForNewChatSession({ type: target, position: this._options.sessionPosition ?? ChatSessionPosition.Sidebar, @@ -350,12 +364,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Wire pickers to the new session and disconnect inactive ones const target = this._targetPicker.selectedTarget; if (target === AgentSessionProviders.Background) { - this._folderPicker.setNewSession(session); this._isolationModePicker.setNewSession(session); this._branchPicker.setNewSession(session); this._repoPicker.setNewSession(undefined); } else { - this._folderPicker.setNewSession(undefined); this._isolationModePicker.setNewSession(undefined); this._branchPicker.setNewSession(undefined); this._repoPicker.setNewSession(session); @@ -587,7 +599,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { const target = this._targetPicker.selectedTarget; if (target === AgentSessionProviders.Background) { - return this._folderPicker.selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + return this._folderPicker.selectedFolderUri; } // For cloud targets, use the repo picker's selection @@ -1033,6 +1045,23 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } } + private async _requestFolderTrust(folderUri: URI): Promise { + const trusted = await this.workspaceTrustRequestService.requestResourcesTrust({ + uri: folderUri, + message: localize('trustFolderMessage', "An agent session will be able to read files, run commands, and make changes in this folder."), + }); + if (!trusted) { + this._folderPicker.removeFromRecents(folderUri); + const previousFolderUri = this._newSession.value?.repoUri; + if (previousFolderUri) { + this._folderPicker.setSelectedFolder(previousFolderUri); + } else { + this._folderPicker.clearSelection(); + } + } + return !!trusted; + } + private _resolveDefaultTarget(options: INewChatWidgetOptions): AgentSessionProviders { const draft = this._getDraftState(); diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 0effc9b662a..e94d1317988 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -19,7 +19,6 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'breadcrumbs.enabled': false, - 'diffEditor.renderSideBySide': false, 'diffEditor.hideUnchangedRegions.enabled': true, 'extensions.ignoreRecommendations': true, From 02c0cbabcedd0bc374472c2a66860e814775d72f Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:18:03 -0800 Subject: [PATCH 175/448] [MCP-Sandboxing]Changes for variable substitution and code refactor (#299124) changes to enable variable substitution for sandboxing --- src/vs/platform/mcp/common/mcpManagement.ts | 3 +- .../mcp/common/mcpManagementService.ts | 7 +- .../platform/mcp/common/mcpPlatformTypes.ts | 1 - .../mcp/common/mcpResourceScannerService.ts | 11 +- .../test/common/mcpManagementService.test.ts | 123 +++++++++++++++++- .../api/common/extHostTypeConverters.ts | 1 + .../discovery/installedMcpServersDiscovery.ts | 2 +- .../discovery/nativeMcpDiscoveryAdapters.ts | 1 + .../common/discovery/pluginMcpDiscovery.ts | 1 + .../contrib/mcp/common/mcpSandboxService.ts | 36 ++--- .../workbench/contrib/mcp/common/mcpTypes.ts | 12 +- .../contrib/mcp/test/common/mcpIcons.test.ts | 3 +- .../mcp/test/common/mcpRegistry.test.ts | 4 + .../mcp/test/common/mcpRegistryTypes.ts | 2 +- .../test/common/mcpServerConnection.test.ts | 3 +- .../contrib/mcp/test/common/mcpTypes.test.ts | 9 +- 16 files changed, 162 insertions(+), 57 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpManagement.ts b/src/vs/platform/mcp/common/mcpManagement.ts index 9c2b7e73d90..834068a98b6 100644 --- a/src/vs/platform/mcp/common/mcpManagement.ts +++ b/src/vs/platform/mcp/common/mcpManagement.ts @@ -10,13 +10,14 @@ import { IIterativePager } from '../../../base/common/paging.js'; import { URI } from '../../../base/common/uri.js'; import { SortBy, SortOrder } from '../../extensionManagement/common/extensionManagement.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js'; +import { IMcpSandboxConfiguration, IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js'; export type InstallSource = 'gallery' | 'local'; export interface ILocalMcpServer { readonly name: string; readonly config: IMcpServerConfiguration; + readonly rootSandbox?: IMcpSandboxConfiguration; readonly version?: string; readonly mcpResource: URI; readonly location?: URI; diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 5f72d29a8fe..ec10b0f0ea9 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -22,7 +22,7 @@ import { ILogService } from '../../log/common/log.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IGalleryMcpServerConfiguration, InstallMcpServerEvent, InstallMcpServerResult, RegistryType, UninstallMcpServerEvent, InstallOptions, UninstallOptions, IInstallableMcpServer, IAllowedMcpServersService, IMcpServerArgument, IMcpServerKeyValueInput, McpServerConfigurationParseResult } from './mcpManagement.js'; -import { IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js'; +import { IMcpSandboxConfiguration, IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js'; import { IMcpResourceScannerService, McpResourceTarget } from './mcpResourceScannerService.js'; export interface ILocalMcpServerInfo { @@ -358,7 +358,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target); if (scannedMcpServers.servers) { await Promise.allSettled(Object.entries(scannedMcpServers.servers).map(async ([name, scannedServer]) => { - const server = await this.scanLocalServer(name, scannedServer); + const server = await this.scanLocalServer(name, scannedServer, scannedMcpServers.sandbox); local.set(name, server); })); } @@ -426,7 +426,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo return Array.from(this.local.values()); } - protected async scanLocalServer(name: string, config: IMcpServerConfiguration): Promise { + protected async scanLocalServer(name: string, config: IMcpServerConfiguration, rootSandbox?: IMcpSandboxConfiguration): Promise { let mcpServerInfo = await this.getLocalServerInfo(name, config); if (!mcpServerInfo) { mcpServerInfo = { name, version: config.version, galleryUrl: isString(config.gallery) ? config.gallery : undefined }; @@ -435,6 +435,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo return { name, config, + rootSandbox, mcpResource: this.mcpResource, version: mcpServerInfo.version, location: mcpServerInfo.location, diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index 985d17f1dc7..dc4fb38172e 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -58,7 +58,6 @@ export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfigurat readonly envFile?: string; readonly cwd?: string; readonly sandboxEnabled?: boolean; - readonly sandbox?: IMcpSandboxConfiguration; readonly dev?: IMcpDevModeConfig; } diff --git a/src/vs/platform/mcp/common/mcpResourceScannerService.ts b/src/vs/platform/mcp/common/mcpResourceScannerService.ts index e0c67bb8b83..151238228b5 100644 --- a/src/vs/platform/mcp/common/mcpResourceScannerService.ts +++ b/src/vs/platform/mcp/common/mcpResourceScannerService.ts @@ -188,7 +188,7 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc if (servers.length > 0) { userMcpServers.servers = {}; for (const [serverName, server] of servers) { - userMcpServers.servers[serverName] = this.sanitizeServer(server, scannedMcpServers.sandbox); + userMcpServers.servers[serverName] = this.sanitizeServer(server); } } return userMcpServers; @@ -203,14 +203,14 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc if (servers.length > 0) { scannedMcpServers.servers = {}; for (const [serverName, config] of servers) { - const serverConfig = this.sanitizeServer(config, scannedMcpServers.sandbox); + const serverConfig = this.sanitizeServer(config); scannedMcpServers.servers[serverName] = serverConfig; } } return scannedMcpServers; } - private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable, sandbox?: IMcpSandboxConfiguration): IMcpServerConfiguration { + private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable): IMcpServerConfiguration { let server: IMcpServerConfiguration; if ((serverOrConfig).config) { const oldScannedMcpServer = serverOrConfig; @@ -226,11 +226,6 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc if (server.type === undefined || (server.type !== McpServerType.REMOTE && server.type !== McpServerType.LOCAL)) { (>server).type = (server).command ? McpServerType.LOCAL : McpServerType.REMOTE; } - - if (sandbox && server.type === McpServerType.LOCAL) { - (>server).sandbox = sandbox; - } - return server; } diff --git a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts index 78ee3299652..7245ffd376b 100644 --- a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts +++ b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts @@ -4,14 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { VSBuffer } from '../../../../base/common/buffer.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { AbstractCommonMcpManagementService } from '../../common/mcpManagementService.js'; -import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, ILocalMcpServer, InstallOptions, RegistryType, TransportType, UninstallOptions } from '../../common/mcpManagement.js'; -import { McpServerType, McpServerVariableType, IMcpServerVariable } from '../../common/mcpPlatformTypes.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { AbstractCommonMcpManagementService, AbstractMcpResourceManagementService } from '../../common/mcpManagementService.js'; +import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, ILocalMcpServer, IMcpGalleryService, InstallOptions, RegistryType, TransportType, UninstallOptions } from '../../common/mcpManagement.js'; +import { IMcpSandboxConfiguration, McpServerType, McpServerVariableType, IMcpServerConfiguration, IMcpServerVariable } from '../../common/mcpPlatformTypes.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; +import { ConfigurationTarget } from '../../../configuration/common/configuration.js'; +import { FileService } from '../../../files/common/fileService.js'; +import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; +import { McpResourceScannerService } from '../../common/mcpResourceScannerService.js'; +import { UriIdentityService } from '../../../uriIdentity/common/uriIdentityService.js'; class TestMcpManagementService extends AbstractCommonMcpManagementService { @@ -42,6 +50,44 @@ class TestMcpManagementService extends AbstractCommonMcpManagementService { } } +class TestMcpResourceManagementService extends AbstractMcpResourceManagementService { + constructor(mcpResource: URI, fileService: FileService, uriIdentityService: UriIdentityService, mcpResourceScannerService: McpResourceScannerService) { + super( + mcpResource, + ConfigurationTarget.USER, + {} as IMcpGalleryService, + fileService, + uriIdentityService, + new NullLogService(), + mcpResourceScannerService, + ); + } + + public reload(): Promise { + return this.updateLocal(); + } + + override canInstall(_server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString { + throw new Error('Not supported'); + } + + protected override getLocalServerInfo(_name: string, _mcpServerConfig: IMcpServerConfiguration) { + return Promise.resolve(undefined); + } + + protected override installFromUri(_uri: URI): Promise { + throw new Error('Not supported'); + } + + override installFromGallery(_server: IGalleryMcpServer, _options?: InstallOptions): Promise { + throw new Error('Not supported'); + } + + override updateMetadata(_local: ILocalMcpServer, _server: IGalleryMcpServer): Promise { + throw new Error('Not supported'); + } +} + suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { let service: TestMcpManagementService; @@ -1073,3 +1119,74 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { }); }); }); + +suite('McpResourceManagementService', () => { + const mcpResource = URI.from({ scheme: Schemas.inMemory, path: '/mcp.json' }); + let disposables: DisposableStore; + let fileService: FileService; + let service: TestMcpResourceManagementService; + + setup(async () => { + disposables = new DisposableStore(); + fileService = disposables.add(new FileService(new NullLogService())); + disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); + const uriIdentityService = disposables.add(new UriIdentityService(fileService)); + const scannerService = disposables.add(new McpResourceScannerService(fileService, uriIdentityService)); + service = disposables.add(new TestMcpResourceManagementService(mcpResource, fileService, uriIdentityService, scannerService)); + + await fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({ + sandbox: { + network: { allowedDomains: ['example.com'] } + }, + servers: { + test: { + type: 'stdio', + command: 'node', + sandboxEnabled: true + } + } + }, null, '\t'))); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires update when root sandbox changes', async () => { + const initial = await service.getInstalled(); + assert.strictEqual(initial.length, 1); + assert.deepStrictEqual(initial[0].rootSandbox, { + network: { allowedDomains: ['example.com'] } + }); + + let updateCount = 0; + const updatePromise = new Promise(resolve => disposables.add(service.onDidUpdateMcpServers(e => { + assert.strictEqual(e.length, 1); + updateCount++; + resolve(); + }))); + + const updatedSandbox: IMcpSandboxConfiguration = { + network: { allowedDomains: ['changed.example.com'] } + }; + + await fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({ + sandbox: updatedSandbox, + servers: { + test: { + type: 'stdio', + command: 'node', + sandboxEnabled: true + } + } + }, null, '\t'))); + await service.reload(); + await updatePromise; + const updated = await service.getInstalled(); + + assert.strictEqual(updateCount, 1); + assert.deepStrictEqual(updated[0].rootSandbox, updatedSandbox); + }); +}); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 61a6ce830c2..22adb59bbae 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -4052,6 +4052,7 @@ export namespace McpServerDefinition { command: item.command, env: item.env, envFile: undefined, + sandbox: undefined } ); } diff --git a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts index 1dc17864797..c3d16718ad3 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts @@ -96,6 +96,7 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc env: config.env || {}, envFile: config.envFile, cwd: config.cwd, + sandbox: server.rootSandbox }; definitions[1].push({ @@ -103,7 +104,6 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc label: server.name, launch, sandboxEnabled: config.type === 'http' ? undefined : config.sandboxEnabled, - sandbox: config.type === 'http' ? undefined : config.sandbox, cacheNonce: await McpServerLaunch.hash(launch), roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined, variableReplacement: { diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts index 3287e0347e7..fe7aca88798 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -49,6 +49,7 @@ export async function claudeConfigToServerDefinition(idPrefix: string, contents: env: server.env || {}, envFile: undefined, cwd: cwd?.fsPath, + sandbox: undefined }; return { diff --git a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts index a173a33b7d2..371bd107215 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts @@ -100,6 +100,7 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { env: config.env ? { ...config.env } : {}, envFile: config.envFile, cwd: config.cwd, + sandbox: undefined, }; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts index 0f8db744c93..24fac57f1de 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts @@ -19,9 +19,8 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IMcpResourceScannerService, McpResourceTarget } from '../../../../platform/mcp/common/mcpResourceScannerService.js'; import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -import { IMcpSandboxConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IMcpSandboxConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { IMcpPotentialSandboxBlock, McpServerDefinition, McpServerLaunch, McpServerTransportType } from './mcpTypes.js'; -import { Mutable } from '../../../../base/common/types.js'; export const IMcpSandboxService = createDecorator('mcpSandboxService'); @@ -85,7 +84,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService } if (await this.isEnabled(serverDef, remoteAuthority)) { this._logService.trace(`McpSandboxService: Launching with config target ${configTarget}`); - const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, serverDef.sandbox, launch.cwd); + const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, launch.sandbox, launch.cwd); const sandboxArgs = this._getSandboxCommandArgs(launch.command, launch.args, launchDetails.sandboxConfigPath); const sandboxEnv = this._getSandboxEnvVariables(launchDetails.tempDir, remoteAuthority); if (launchDetails.srtPath) { @@ -160,7 +159,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService let didChange = false; await this._mcpResourceScannerService.updateSandboxConfig(data => { - const existingSandbox = data.sandbox ?? serverDef.sandbox; + const existingSandbox = data.sandbox; const suggestedAllowedDomains = suggestedSandboxConfig?.network?.allowedDomains ?? []; const suggestedAllowWrite = suggestedSandboxConfig?.filesystem?.allowWrite ?? []; @@ -178,41 +177,24 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService } } - didChange = currentAllowedDomains.size !== (existingSandbox?.network?.allowedDomains?.length ?? 0) - || currentAllowWrite.size !== (existingSandbox?.filesystem?.allowWrite?.length ?? 0); - - if (!didChange) { + if (suggestedAllowedDomains.length === 0 && suggestedAllowWrite.length === 0) { return data; } - const nextSandboxConfig: IMcpSandboxConfiguration = { - ...existingSandbox, - }; - - if (currentAllowedDomains.size > 0 || existingSandbox?.network?.deniedDomains?.length) { + didChange = true; + const nextSandboxConfig: IMcpSandboxConfiguration = {}; + if (currentAllowedDomains.size > 0) { nextSandboxConfig.network = { ...existingSandbox?.network, - allowedDomains: [...currentAllowedDomains], + allowedDomains: [...currentAllowedDomains] }; } - - if (currentAllowWrite.size > 0 || existingSandbox?.filesystem?.denyRead?.length || existingSandbox?.filesystem?.denyWrite?.length) { + if (currentAllowWrite.size > 0) { nextSandboxConfig.filesystem = { ...existingSandbox?.filesystem, allowWrite: [...currentAllowWrite], }; } - - //always remove sandbox at server level when writing back, it should only exist at the top level. This is to sanitize any old or malformed configs that may have sandbox defined at the server level. - if (data.servers) { - for (const serverName in data.servers) { - const serverConfig = data.servers[serverName]; - if (serverConfig.type === McpServerType.LOCAL) { - delete (serverConfig as Mutable).sandbox; - } - } - } - return { ...data, sandbox: nextSandboxConfig, diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 8ce685dfb1d..4c77b0a2f54 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -138,8 +138,6 @@ export interface McpServerDefinition { readonly staticMetadata?: McpServerStaticMetadata; /** Indicates if the sandbox is enabled for this server. */ readonly sandboxEnabled?: boolean; - /** Sandbox configuration to apply for this server. */ - readonly sandbox?: IMcpSandboxConfiguration; readonly presentation?: { @@ -173,7 +171,6 @@ export namespace McpServerDefinition { readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; readonly staticMetadata?: McpServerStaticMetadata; readonly sandboxEnabled?: boolean; - readonly sandbox?: IMcpSandboxConfiguration; } export function toSerialized(def: McpServerDefinition): McpServerDefinition.Serialized { @@ -188,7 +185,6 @@ export namespace McpServerDefinition { staticMetadata: def.staticMetadata, launch: McpServerLaunch.fromSerialized(def.launch), sandboxEnabled: def.sandboxEnabled, - sandbox: def.sandboxEnabled ? def.sandbox : undefined, variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, }; } @@ -202,8 +198,8 @@ export namespace McpServerDefinition { && objectsEqual(a.presentation, b.presentation) && objectsEqual(a.variableReplacement, b.variableReplacement) && objectsEqual(a.devMode, b.devMode) - && a.sandboxEnabled === b.sandboxEnabled - && objectsEqual(a.sandbox, b.sandbox); + && a.sandboxEnabled === b.sandboxEnabled; + } } @@ -519,6 +515,7 @@ export interface McpServerTransportStdio { readonly args: readonly string[]; readonly env: Record; readonly envFile: string | undefined; + readonly sandbox: IMcpSandboxConfiguration | undefined; } export interface McpServerTransportHTTPAuthentication { @@ -551,7 +548,7 @@ export type McpServerLaunch = export namespace McpServerLaunch { export type Serialized = | { type: McpServerTransportType.HTTP; uri: UriComponents; headers: [string, string][]; authentication?: McpServerTransportHTTPAuthentication } - | { type: McpServerTransportType.Stdio; cwd: string | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; + | { type: McpServerTransportType.Stdio; cwd: string | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined; sandbox: IMcpSandboxConfiguration | undefined }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { return launch; @@ -569,6 +566,7 @@ export namespace McpServerLaunch { args: launch.args, env: launch.env, envFile: launch.envFile, + sandbox: launch.sandbox }; } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts index 7cdb2b661d0..90430507be1 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts @@ -22,7 +22,8 @@ const createStdioLaunch = (): McpServerTransportStdio => ({ command: 'cmd', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined }); suite('MCP Icons', () => { diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index e052e97d131..722a4ee2fe7 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -239,6 +239,7 @@ suite('Workbench - MCP - Registry', () => { env: {}, envFile: undefined, cwd: '/test', + sandbox: undefined } }; }); @@ -301,6 +302,7 @@ suite('Workbench - MCP - Registry', () => { }, envFile: undefined, cwd: '/test', + sandbox: undefined }, variableReplacement: { section: 'mcp', @@ -402,6 +404,7 @@ suite('Workbench - MCP - Registry', () => { env: {}, envFile: undefined, cwd: '/test', + sandbox: undefined }, }; @@ -726,6 +729,7 @@ suite('Workbench - MCP - Registry', () => { env: {}, envFile: undefined, cwd: '/test', + sandbox: undefined } }; } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts index 2648adedeb4..69b07fdf55f 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts @@ -171,7 +171,7 @@ export class TestMcpRegistry implements IMcpRegistry { serverDefinitions: observableValue(this, [{ id: 'test-server', label: 'Test Server', - launch: { type: McpServerTransportType.Stdio, command: 'echo', args: ['Hello MCP'], env: {}, envFile: undefined, cwd: undefined }, + launch: { type: McpServerTransportType.Stdio, command: 'echo', args: ['Hello MCP'], env: {}, envFile: undefined, cwd: undefined, sandbox: undefined }, cacheNonce: 'a', } satisfies McpServerDefinition]), trustBehavior: McpServerTrust.Kind.Trusted, diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index 33114bc70d4..44f180efe2f 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -108,7 +108,8 @@ suite('Workbench - MCP - ServerConnection', () => { args: [], env: {}, envFile: undefined, - cwd: '/test' + cwd: '/test', + sandbox: undefined } }; }); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts index b73a1d48fb5..d7a6d11283b 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts @@ -40,7 +40,8 @@ suite('MCP Types', () => { command: 'test-command', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined }, ...overrides }); @@ -89,7 +90,8 @@ suite('MCP Types', () => { command: 'command1', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined } }); const def2 = createBasicDefinition({ @@ -99,7 +101,8 @@ suite('MCP Types', () => { command: 'command2', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined } }); assert.strictEqual(McpServerDefinition.equals(def1, def2), false); From 1a29c31071da8bcd5473ed597ee73146fde3a457 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 4 Mar 2026 19:42:30 +0100 Subject: [PATCH 176/448] update screenshot baselines from CI --- .../baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow1Line/Light.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png | 3 +++ .../agentSessionsViewer/ApprovalRow3LongLines/Dark.png | 3 +++ .../agentSessionsViewer/ApprovalRow3LongLines/Light.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRowBash/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRowBash/Light.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRowJson/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRowJson/Light.png | 3 +++ .../baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png | 3 +++ .../agentSessionsViewer/ApprovalRowLongLabel/Light.png | 3 +++ .../agentSessionsViewer/ApprovalRowPowerShell/Dark.png | 3 +++ .../agentSessionsViewer/ApprovalRowPowerShell/Light.png | 3 +++ .../baseline/agentSessionsViewer/Archived/Dark.png | 3 +++ .../baseline/agentSessionsViewer/Archived/Light.png | 3 +++ .../baseline/agentSessionsViewer/ArchivedUnread/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ArchivedUnread/Light.png | 3 +++ .../baseline/agentSessionsViewer/BackgroundProvider/Dark.png | 3 +++ .../baseline/agentSessionsViewer/BackgroundProvider/Light.png | 3 +++ .../baseline/agentSessionsViewer/ClaudeProvider/Dark.png | 3 +++ .../baseline/agentSessionsViewer/ClaudeProvider/Light.png | 3 +++ .../baseline/agentSessionsViewer/CloudProvider/Dark.png | 3 +++ .../baseline/agentSessionsViewer/CloudProvider/Light.png | 3 +++ .../agentSessionsViewer/CloudProviderInProgress/Dark.png | 3 +++ .../agentSessionsViewer/CloudProviderInProgress/Light.png | 3 +++ .../baseline/agentSessionsViewer/CompletedRead/Dark.png | 3 +++ .../baseline/agentSessionsViewer/CompletedRead/Light.png | 3 +++ .../baseline/agentSessionsViewer/CompletedUnread/Dark.png | 3 +++ .../baseline/agentSessionsViewer/CompletedUnread/Light.png | 3 +++ .../baseline/agentSessionsViewer/FailedWithDuration/Dark.png | 3 +++ .../baseline/agentSessionsViewer/FailedWithDuration/Light.png | 3 +++ .../agentSessionsViewer/FailedWithoutDuration/Dark.png | 3 +++ .../agentSessionsViewer/FailedWithoutDuration/Light.png | 3 +++ .../baseline/agentSessionsViewer/InProgress/Dark.png | 3 +++ .../baseline/agentSessionsViewer/InProgress/Light.png | 3 +++ .../agentSessionsViewer/InProgressWithDescription/Dark.png | 3 +++ .../agentSessionsViewer/InProgressWithDescription/Light.png | 3 +++ .../baseline/agentSessionsViewer/NeedsInput/Dark.png | 3 +++ .../baseline/agentSessionsViewer/NeedsInput/Light.png | 3 +++ .../baseline/agentSessionsViewer/SectionArchived/Dark.png | 3 +++ .../baseline/agentSessionsViewer/SectionArchived/Light.png | 3 +++ .../baseline/agentSessionsViewer/SectionLastWeek/Dark.png | 3 +++ .../baseline/agentSessionsViewer/SectionLastWeek/Light.png | 3 +++ .../baseline/agentSessionsViewer/SectionMore/Dark.png | 3 +++ .../baseline/agentSessionsViewer/SectionMore/Light.png | 3 +++ .../baseline/agentSessionsViewer/SectionOlder/Dark.png | 3 +++ .../baseline/agentSessionsViewer/SectionOlder/Light.png | 3 +++ .../baseline/agentSessionsViewer/SectionToday/Dark.png | 3 +++ .../baseline/agentSessionsViewer/SectionToday/Light.png | 3 +++ .../baseline/agentSessionsViewer/SectionYesterday/Dark.png | 3 +++ .../baseline/agentSessionsViewer/SectionYesterday/Light.png | 3 +++ .../baseline/agentSessionsViewer/WithBadge/Dark.png | 3 +++ .../baseline/agentSessionsViewer/WithBadge/Light.png | 3 +++ .../baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png | 3 +++ .../baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png | 3 +++ .../baseline/agentSessionsViewer/WithDescription/Dark.png | 3 +++ .../baseline/agentSessionsViewer/WithDescription/Light.png | 3 +++ .../baseline/agentSessionsViewer/WithDiffChanges/Dark.png | 3 +++ .../baseline/agentSessionsViewer/WithDiffChanges/Light.png | 3 +++ .../baseline/agentSessionsViewer/WithFileChangesList/Dark.png | 3 +++ .../baseline/agentSessionsViewer/WithFileChangesList/Light.png | 3 +++ .../baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png | 3 +++ .../baseline/agentSessionsViewer/WithMarkdownBadge/Light.png | 3 +++ .../agentSessionsViewer/WithMarkdownDescription/Dark.png | 3 +++ .../agentSessionsViewer/WithMarkdownDescription/Light.png | 3 +++ .../.screenshots/baseline/aiStats/AiStatsHover/Dark.png | 3 --- .../.screenshots/baseline/aiStats/AiStatsHover/Light.png | 3 --- .../.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png | 3 +++ .../.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png | 3 +++ .../baseline/{ => chat}/aiStats/AiStatsHoverNoData/Dark.png | 0 .../baseline/{ => chat}/aiStats/AiStatsHoverNoData/Light.png | 0 .../baseline/chat/chatProgressContentPart/Completed/Dark.png | 3 +++ .../baseline/chat/chatProgressContentPart/Completed/Light.png | 3 +++ .../baseline/chat/chatProgressContentPart/LongMessage/Dark.png | 3 +++ .../chat/chatProgressContentPart/LongMessage/Light.png | 3 +++ .../chat/chatProgressContentPart/WithCustomIcon/Dark.png | 3 +++ .../chat/chatProgressContentPart/WithCustomIcon/Light.png | 3 +++ .../chat/chatProgressContentPart/WithInlineCode/Dark.png | 3 +++ .../chat/chatProgressContentPart/WithInlineCode/Light.png | 3 +++ .../baseline/chat/chatProgressContentPart/WithSpinner/Dark.png | 3 +++ .../chat/chatProgressContentPart/WithSpinner/Light.png | 3 +++ .../chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png | 3 +++ .../chat/chatQuestionCarousel/MultiSelectQuestion/Light.png | 3 +++ .../chat/chatQuestionCarousel/MultipleQuestions/Dark.png | 3 +++ .../chat/chatQuestionCarousel/MultipleQuestions/Light.png | 3 +++ .../baseline/chat/chatQuestionCarousel/NoSkip/Dark.png | 3 +++ .../baseline/chat/chatQuestionCarousel/NoSkip/Light.png | 3 +++ .../chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png | 3 +++ .../chat/chatQuestionCarousel/SingleSelectQuestion/Light.png | 3 +++ .../chat/chatQuestionCarousel/SingleTextQuestion/Dark.png | 3 +++ .../chat/chatQuestionCarousel/SingleTextQuestion/Light.png | 3 +++ .../baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png | 3 +++ .../chat/chatQuestionCarousel/SkippedSummary/Light.png | 3 +++ .../chat/chatQuestionCarousel/SubmittedSummary/Dark.png | 3 +++ .../chat/chatQuestionCarousel/SubmittedSummary/Light.png | 3 +++ .../InstructionFilesWithAgentInstructions/Dark.png | 3 +++ .../InstructionFilesWithAgentInstructions/Light.png | 3 +++ .../baseline/chat/promptFilePickers/PromptFiles/Dark.png | 3 +++ .../baseline/chat/promptFilePickers/PromptFiles/Light.png | 3 +++ .../baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png | 3 --- .../chatQuestionCarousel/MultiSelectQuestion/Light.png | 3 --- .../baseline/chatQuestionCarousel/MultipleQuestions/Dark.png | 3 --- .../baseline/chatQuestionCarousel/MultipleQuestions/Light.png | 3 --- .../.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png | 3 --- .../baseline/chatQuestionCarousel/NoSkip/Light.png | 3 --- .../chatQuestionCarousel/SingleSelectQuestion/Dark.png | 3 --- .../chatQuestionCarousel/SingleSelectQuestion/Light.png | 3 --- .../baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png | 3 --- .../baseline/chatQuestionCarousel/SingleTextQuestion/Light.png | 3 --- .../baseline/chatQuestionCarousel/SkippedSummary/Dark.png | 3 --- .../baseline/chatQuestionCarousel/SkippedSummary/Light.png | 3 --- .../baseline/chatQuestionCarousel/SubmittedSummary/Dark.png | 3 --- .../baseline/chatQuestionCarousel/SubmittedSummary/Light.png | 3 --- .../baseline/codeActionList/GroupedCodeActions/Dark.png | 3 --- .../baseline/codeActionList/GroupedCodeActions/Light.png | 3 --- .../baseline/codeActionList/SimpleQuickFixes/Dark.png | 3 --- .../baseline/codeActionList/SimpleQuickFixes/Light.png | 3 --- .../baseline/editor/codeActionList/GroupedCodeActions/Dark.png | 3 +++ .../editor/codeActionList/GroupedCodeActions/Light.png | 3 +++ .../baseline/editor/codeActionList/SimpleQuickFixes/Dark.png | 3 +++ .../baseline/editor/codeActionList/SimpleQuickFixes/Light.png | 3 +++ .../baseline/{ => editor}/codeEditor/CodeEditor/Dark.png | 0 .../baseline/{ => editor}/codeEditor/CodeEditor/Light.png | 0 .../.screenshots/baseline/editor/findWidget/Find/Dark.png | 3 +++ .../.screenshots/baseline/editor/findWidget/Find/Light.png | 3 +++ .../baseline/editor/findWidget/FindAndReplace/Dark.png | 3 +++ .../baseline/editor/findWidget/FindAndReplace/Light.png | 3 +++ .../baseline/editor/inlineCompletions/InsertionView/Dark.png | 3 +++ .../baseline/editor/inlineCompletions/InsertionView/Light.png | 3 +++ .../baseline/editor/inlineCompletions/SideBySideView/Dark.png | 3 +++ .../baseline/editor/inlineCompletions/SideBySideView/Light.png | 3 +++ .../editor/inlineCompletions/WordReplacementView/Dark.png | 3 +++ .../editor/inlineCompletions/WordReplacementView/Light.png | 3 +++ .../editor/inlineCompletionsExtras/HintsToolbar/Dark.png | 3 +++ .../editor/inlineCompletionsExtras/HintsToolbar/Light.png | 3 +++ .../inlineCompletionsExtras/HintsToolbarHovered/Dark.png | 3 +++ .../inlineCompletionsExtras/HintsToolbarHovered/Light.png | 3 +++ .../{ => editor}/inlineCompletionsExtras/JumpToHint/Dark.png | 0 .../{ => editor}/inlineCompletionsExtras/JumpToHint/Light.png | 0 .../editor/inlineCompletionsExtras/LongDistanceHint/Dark.png | 3 +++ .../editor/inlineCompletionsExtras/LongDistanceHint/Light.png | 3 +++ .../baseline/{ => editor}/renameWidget/RenameClass/Dark.png | 0 .../baseline/{ => editor}/renameWidget/RenameClass/Light.png | 0 .../baseline/{ => editor}/renameWidget/RenameVariable/Dark.png | 0 .../{ => editor}/renameWidget/RenameVariable/Light.png | 0 .../baseline/editor/suggestWidget/MethodCompletions/Dark.png | 3 +++ .../baseline/editor/suggestWidget/MethodCompletions/Light.png | 3 +++ .../baseline/editor/suggestWidget/MixedKinds/Dark.png | 3 +++ .../baseline/editor/suggestWidget/MixedKinds/Light.png | 3 +++ .../.screenshots/baseline/findWidget/Find/Dark.png | 3 --- .../.screenshots/baseline/findWidget/Find/Light.png | 3 --- .../.screenshots/baseline/findWidget/FindAndReplace/Dark.png | 3 --- .../.screenshots/baseline/findWidget/FindAndReplace/Light.png | 3 --- .../baseline/inlineCompletions/InsertionView/Dark.png | 3 --- .../baseline/inlineCompletions/InsertionView/Light.png | 3 --- .../baseline/inlineCompletions/SideBySideView/Dark.png | 3 --- .../baseline/inlineCompletions/SideBySideView/Light.png | 3 --- .../baseline/inlineCompletions/WordReplacementView/Dark.png | 3 --- .../baseline/inlineCompletions/WordReplacementView/Light.png | 3 --- .../baseline/inlineCompletionsExtras/HintsToolbar/Dark.png | 3 --- .../baseline/inlineCompletionsExtras/HintsToolbar/Light.png | 3 --- .../inlineCompletionsExtras/HintsToolbarHovered/Dark.png | 3 --- .../inlineCompletionsExtras/HintsToolbarHovered/Light.png | 3 --- .../baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png | 3 --- .../inlineCompletionsExtras/LongDistanceHint/Light.png | 3 --- .../InstructionFilesWithAgentInstructions/Dark.png | 3 --- .../InstructionFilesWithAgentInstructions/Light.png | 3 --- .../baseline/promptFilePickers/PromptFiles/Dark.png | 3 --- .../baseline/promptFilePickers/PromptFiles/Light.png | 3 --- .../sessions/accountWidget/AvailableForDownload/Dark.png | 3 +++ .../sessions/accountWidget/AvailableForDownload/Light.png | 3 +++ .../sessions/accountWidget/CheckingForUpdatesHidden/Dark.png | 3 +++ .../sessions/accountWidget/CheckingForUpdatesHidden/Light.png | 3 +++ .../sessions/accountWidget/DownloadedInstalling/Dark.png | 3 +++ .../sessions/accountWidget/DownloadedInstalling/Light.png | 3 +++ .../sessions/accountWidget/Downloading30Percent/Dark.png | 3 +++ .../sessions/accountWidget/Downloading30Percent/Light.png | 3 +++ .../sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png | 3 +++ .../sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png | 3 +++ .../baseline/sessions/accountWidget/Overwriting/Dark.png | 3 +++ .../baseline/sessions/accountWidget/Overwriting/Light.png | 3 +++ .../baseline/sessions/accountWidget/Ready/Dark.png | 3 +++ .../baseline/sessions/accountWidget/Ready/Light.png | 3 +++ .../baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png | 3 +++ .../baseline/sessions/accountWidget/SignedInNoUpdate/Light.png | 3 +++ .../baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png | 3 +++ .../sessions/accountWidget/SignedOutNoUpdate/Light.png | 3 +++ .../baseline/sessions/accountWidget/Updating/Dark.png | 3 +++ .../baseline/sessions/accountWidget/Updating/Light.png | 3 +++ .../sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png | 3 +++ .../aiCustomizationShortcutsWidget/Collapsed/Light.png | 3 +++ .../CollapsedWithMcpServers/Dark.png | 3 +++ .../CollapsedWithMcpServers/Light.png | 3 +++ .../sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png | 3 +++ .../sessions/aiCustomizationShortcutsWidget/Expanded/Light.png | 3 +++ .../aiCustomizationShortcutsWidget/WithCounts/Dark.png | 3 +++ .../aiCustomizationShortcutsWidget/WithCounts/Light.png | 3 +++ .../aiCustomizationShortcutsWidget/WithMcpServers/Dark.png | 3 +++ .../aiCustomizationShortcutsWidget/WithMcpServers/Light.png | 3 +++ .../updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png | 3 +++ .../UpdateHoverAvailableForDownload/Light.png | 3 +++ .../updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png | 3 +++ .../UpdateHoverDownloading30Percent/Light.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverInstalling/Light.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverReady/Dark.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverReady/Light.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png | 3 +++ .../updateHoverWidget/UpdateHoverSameVersion/Light.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png | 3 +++ .../sessions/updateHoverWidget/UpdateHoverUpdating/Light.png | 3 +++ .../baseline/suggestWidget/MethodCompletions/Dark.png | 3 --- .../baseline/suggestWidget/MethodCompletions/Light.png | 3 --- .../.screenshots/baseline/suggestWidget/MixedKinds/Dark.png | 3 --- .../.screenshots/baseline/suggestWidget/MixedKinds/Light.png | 3 --- .../baseline/updateWidget/AvailableForDownload/Dark.png | 3 --- .../baseline/updateWidget/AvailableForDownload/Light.png | 3 --- .../baseline/updateWidget/CheckingForUpdates/Dark.png | 3 --- .../baseline/updateWidget/CheckingForUpdates/Light.png | 3 --- .../.screenshots/baseline/updateWidget/Downloaded/Dark.png | 3 --- .../.screenshots/baseline/updateWidget/Downloaded/Light.png | 3 --- .../baseline/updateWidget/Downloading0Percent/Dark.png | 3 --- .../baseline/updateWidget/Downloading0Percent/Light.png | 3 --- .../baseline/updateWidget/Downloading100Percent/Dark.png | 3 --- .../baseline/updateWidget/Downloading100Percent/Light.png | 3 --- .../baseline/updateWidget/Downloading30Percent/Dark.png | 3 --- .../baseline/updateWidget/Downloading30Percent/Light.png | 3 --- .../baseline/updateWidget/Downloading65Percent/Dark.png | 3 --- .../baseline/updateWidget/Downloading65Percent/Light.png | 3 --- .../baseline/updateWidget/DownloadingIndeterminate/Dark.png | 3 --- .../baseline/updateWidget/DownloadingIndeterminate/Light.png | 3 --- .../.screenshots/baseline/updateWidget/Overwriting/Dark.png | 3 --- .../.screenshots/baseline/updateWidget/Overwriting/Light.png | 3 --- .../.screenshots/baseline/updateWidget/Ready/Dark.png | 3 --- .../.screenshots/baseline/updateWidget/Ready/Light.png | 3 --- .../.screenshots/baseline/updateWidget/Updating/Dark.png | 3 --- .../.screenshots/baseline/updateWidget/Updating/Light.png | 3 --- 242 files changed, 498 insertions(+), 198 deletions(-) create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png rename test/componentFixtures/.screenshots/baseline/{ => chat}/aiStats/AiStatsHoverNoData/Dark.png (100%) rename test/componentFixtures/.screenshots/baseline/{ => chat}/aiStats/AiStatsHoverNoData/Light.png (100%) create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png rename test/componentFixtures/.screenshots/baseline/{ => editor}/codeEditor/CodeEditor/Dark.png (100%) rename test/componentFixtures/.screenshots/baseline/{ => editor}/codeEditor/CodeEditor/Light.png (100%) create mode 100644 test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png rename test/componentFixtures/.screenshots/baseline/{ => editor}/inlineCompletionsExtras/JumpToHint/Dark.png (100%) rename test/componentFixtures/.screenshots/baseline/{ => editor}/inlineCompletionsExtras/JumpToHint/Light.png (100%) create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png rename test/componentFixtures/.screenshots/baseline/{ => editor}/renameWidget/RenameClass/Dark.png (100%) rename test/componentFixtures/.screenshots/baseline/{ => editor}/renameWidget/RenameClass/Light.png (100%) rename test/componentFixtures/.screenshots/baseline/{ => editor}/renameWidget/RenameVariable/Dark.png (100%) rename test/componentFixtures/.screenshots/baseline/{ => editor}/renameWidget/RenameVariable/Light.png (100%) create mode 100644 test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png create mode 100644 test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png delete mode 100644 test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png new file mode 100644 index 00000000000..6e97bb89568 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb56239cc915c18dbdf70d98049cc8386350c6e394b988a2df86df95ef10b52c +size 7064 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png new file mode 100644 index 00000000000..b8635ead60e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:163f0620ca91d6a7636ec58362e7dbc53a338fd26d2c9577ddb893c880bf86aa +size 7053 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png new file mode 100644 index 00000000000..654a095b444 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5d405d46064d7ae8cf9917587c51db8b80528590d4c9718729460781aa25ff9 +size 8657 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png new file mode 100644 index 00000000000..1350551e929 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02e137eff8bd38674c35f3d4ab472ff380bfa0c31e0f261e64a66822665c98a3 +size 8717 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png new file mode 100644 index 00000000000..9aa34fcadd4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029ca626c8b89d7efce7d86b401774373d473282fa29b45f2a0b6eff314090da +size 8737 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png new file mode 100644 index 00000000000..11afd7ebade --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98519a1e1420101c2efb7fedf417432aa9509b3614d0df4fd51e57b02f791a9c +size 8684 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png new file mode 100644 index 00000000000..23cae64ce72 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3619142a6234cafb5b20bae5007daf1af201bb58f8b0f1e7e404621a77d74123 +size 12355 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png new file mode 100644 index 00000000000..fbe36047f39 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:717467794a22315bb378be412b331db00635d0171043deb50fa6e50a3caf9af3 +size 12363 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png new file mode 100644 index 00000000000..3f081322682 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a68b3828da8ddd4254a8ff14d740d07ed9463012a6646d466ad052f917080462 +size 9167 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png new file mode 100644 index 00000000000..e3e96fe51a8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29a3581395521fe377cb76135f9df237ba8d6d241b5bcee4707a4c92e560e410 +size 9169 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png new file mode 100644 index 00000000000..c6025168a8c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:349e1054718733508b1e8ccab0c08039e13319458a1a4e730d98531c9f8065a3 +size 7889 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png new file mode 100644 index 00000000000..c9bdeeb188d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49dd6690cb406f928925ad0000624128efc5e60999a6472f5c14f5d34a048b8a +size 7940 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png new file mode 100644 index 00000000000..29f4c5e28a7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfdc23fd51094966957b9487c71aa7dc69d9ace05bc9315adf6e8ae296de3d89 +size 7338 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png new file mode 100644 index 00000000000..7684550a5f6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0fc1e6917fd33d4c46dd70b74d787bc0c2bc2b2d6f38c1d1c8923ca84b11009 +size 7439 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png new file mode 100644 index 00000000000..40cf1fbcec9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6827c363512a28c506670d9bce96efe463bb2e7b16864c1d3ef78bdb27c5d8b9 +size 7915 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png new file mode 100644 index 00000000000..035820555cc --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d90a5b77afd766a79dc78ec15369ae6fd8d37791f3b365cd51293b86089a268 +size 8005 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png new file mode 100644 index 00000000000..843641053a1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10b958efa32aa1dd506894c1663639bfd8252907b3640e4d2c6fb1893f0798dc +size 7497 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png new file mode 100644 index 00000000000..4f679b73ace --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083d37a41eb68a5f0841c4461d4ee9df3e5e093b7295d069877fce8933c30581 +size 7548 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png new file mode 100644 index 00000000000..18b0f2f3393 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcb0046c2d6bf62981df0f57af2f04bc21abff8f2c25bda0bce72577c8825234 +size 3955 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png new file mode 100644 index 00000000000..0518b7db987 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6955c3424c2907b6b8472c60e5c5cee6ef104dd8482fd932e0c83ba611c3522b +size 3883 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png new file mode 100644 index 00000000000..2e9a78f3e9c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:761643f249dddaaa1e7f307b3372463e252f67ed30cdd860052ca2b1b9534601 +size 4136 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png new file mode 100644 index 00000000000..c4edc35a169 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed79c20b8c4adb9c4253ebed499554e3d8e898b0b5f9d389c8ce865345b85ad6 +size 4059 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png new file mode 100644 index 00000000000..2f4f1029af5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211c5915f952e93eede04b6ff57b552ccfd7a524ec17ded20819f531e07289de +size 4323 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png new file mode 100644 index 00000000000..b5a83bc3227 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76981d631106433e066dd7b6354337c3bbba1018587beb26ebd9d662f25093c0 +size 4440 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png new file mode 100644 index 00000000000..64427aeaa23 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd6f5753251dfd4ebad48ac881d2a18baeeb6dac683d8c991b23676d80e79f7d +size 4627 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png new file mode 100644 index 00000000000..ff344e500fe --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46d1010d543349b526f2b6cdfc17177fdda11cc994a6522c6a827c883755e21b +size 4720 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png new file mode 100644 index 00000000000..2ffb6d1dfa9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a357eca1ff0edae842d2137e53c4648a8a24dda806056f7cbb047ea02bc05250 +size 4402 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png new file mode 100644 index 00000000000..c7f3c6cdfdb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4127bb1d38d3c49f0393afcf4d04415f328cccbe8473de7e0894b671f5c6ec51 +size 4475 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png new file mode 100644 index 00000000000..94ecc186613 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11e38388dbe35d160b60aa8d8b1b45b2a71c9604caed3b610b318f4fc5beb5c0 +size 3746 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png new file mode 100644 index 00000000000..6c780125a43 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768a1bfc002b37facc1d7e7c4a096148134b2f037f6739611b0ebdac8694515e +size 3741 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png new file mode 100644 index 00000000000..9379588d303 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ef5c71cd24e7527c325436f778498f4ce843c4118fb6df9132d152d195b77b5 +size 4131 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png new file mode 100644 index 00000000000..cc372129852 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:461481ef366395b7a3283de9dbac92581b182420f6439d15af3c5ad1b95f315f +size 4236 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png new file mode 100644 index 00000000000..e7b1e95cc9b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29611b9811d052d0c311abf299aaf4009eb4cf9adc39b50a362f23dbb30e3012 +size 4281 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png new file mode 100644 index 00000000000..53e514e305b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cafcc1fb16a92c55106474771945cc7063152922633db3d3acc8f455677e93a2 +size 4303 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png new file mode 100644 index 00000000000..0f129c39bf1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f18d15f95ce04361389e3685c0d04d5b5846ee7ac97f192ad75605879e3ef711 +size 3952 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png new file mode 100644 index 00000000000..b2c232971db --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:823534baac31274e97febf864f4c70430672f573a35abe360c406f0ef8394563 +size 3984 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png new file mode 100644 index 00000000000..60ae250bdb3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a4986f49d637f978fdef48c75c7307b1d2fe60c0a301682481a2d9271fe9c0 +size 3473 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png new file mode 100644 index 00000000000..77c3257a44b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e0c6543a97ca59eeb9bced9530976e6697e31df4cc0e392dae5721cdef90a35 +size 3590 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png new file mode 100644 index 00000000000..cf64cff29d7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39e4ce2cd4020f790d3a2f54ab29da693ce34b35a82a4ef9ebbc7e685b6c5f29 +size 3942 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png new file mode 100644 index 00000000000..4e2ed61233f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c0310d172ee83a1f0260a7731e321a8e193dbb4c58afa0115bf3e2246b623db +size 3997 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png new file mode 100644 index 00000000000..75e19d71278 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9a68bfda118629ddd5095f2fdd202c5baaccbc6b92d1d9584ef5053113af328 +size 4693 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png new file mode 100644 index 00000000000..bc5cbfc0de7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:616343f0b523bd7a88c02349345baedebbccb3251b34030903e5afe54c14ee0d +size 4793 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png new file mode 100644 index 00000000000..50cbb3efca4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea3e3728408c21a161655ad68304442ea348f4c95b613e5abc5be491073d5a11 +size 3767 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png new file mode 100644 index 00000000000..82d68f0fd0d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78eb1956b1a37699a678f73d79cb6c1d7b4deda79e857c03017910054fdbae90 +size 3815 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png new file mode 100644 index 00000000000..60acf6fe15b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67e30acb57253a7a9c02e93e8120019eb1afc9932a99c59a241968ae75ee3752 +size 896 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png new file mode 100644 index 00000000000..44015ee3881 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d6a95e55db9415c49043745f6eb5682e20137d0d487edb6323ef101c2e46016 +size 870 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png new file mode 100644 index 00000000000..130f5f1c40a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:379ebb29923b4874351469a1a60a7e4bc76397c85e61741dc86361dd123f68d0 +size 1032 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png new file mode 100644 index 00000000000..cc2acbb5916 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:109ad01629c5fb42759102c4a9fdf3b9e4f69452d06d6b2f6b478be418261e01 +size 1013 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png new file mode 100644 index 00000000000..da3fbefb55c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d1c9f3fba50deacb99898e7e403239716b918e4901258b155f3c87558938219 +size 634 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png new file mode 100644 index 00000000000..1fd51cdd988 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9083bcd9e6dd0fddf1b86389abbf2c8be5db4c84a80870acee1ebb6209b3734c +size 610 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png new file mode 100644 index 00000000000..5a3f629e0d9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fb24f8743a805c51eed4cb0822e6f031f1663ebf3860fa0f555fc9105d6aa47 +size 655 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png new file mode 100644 index 00000000000..9a01d0f11bd --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b33d4a2df45aadb2ddd03a4e78107993407390a0fcdcd551cc5e46cfe3bfd79 +size 629 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png new file mode 100644 index 00000000000..7df9b9dc3e3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dba102faa5ab22be300c2f3b144ccb9fd7d191b7ce345934aac0cf2a22a4cf2 +size 700 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png new file mode 100644 index 00000000000..97bfabf75bb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0896ebcac18fc2c67051318eccf952a4af9cded093bb8ed22da0d4f369a90fd +size 691 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png new file mode 100644 index 00000000000..44f782bea73 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf4cc56f6e7472b91ee5291f7b7963c88c575152c01914af66b79d3f983072a1 +size 1034 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png new file mode 100644 index 00000000000..fd7a76b1e55 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f11009c1529cb97619d437c5563764d6cf9deed1db7d7b539506a8500d58694d +size 1011 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png new file mode 100644 index 00000000000..e8a6dcc8dde --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dfad96ea810926d39db4d8f689f66c717fd4a2b51de380010046e7610ec14a4 +size 4819 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png new file mode 100644 index 00000000000..7605ff11511 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b6b40e3ab4d73221357a136dcac51eb0081c344a98930dd5e738662137af955 +size 4886 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png new file mode 100644 index 00000000000..5e3bbe18e1e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768613c86e446829341642343180cd84a0376dfafae7399dc2a50cf3b0e21575 +size 3900 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png new file mode 100644 index 00000000000..3b1b9440317 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d471d8eeb3bcf0a285a3b80a7dac70d5258c7fee903185377bddad1199562259 +size 4018 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png new file mode 100644 index 00000000000..842e5652928 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83b83d1b89bbee53b1e7f81990b30e8f2124b2099e0da948bcc0e807d2593f1e +size 5389 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png new file mode 100644 index 00000000000..a15244fa8b9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cd38f065604d4c1ea6564d0da9cd5436e7587547583783408c7e7df3812f80 +size 5507 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png new file mode 100644 index 00000000000..760959402ec --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ea6dc5746ca3dba73ee8a96df90fd2c4f34625d58ac9d616134ba5d2ff5f87 +size 3473 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png new file mode 100644 index 00000000000..14d28a1f999 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d502f79bc59e00f539745df156fa704cc3db11c16653633943b6edf26401e25 +size 3617 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png new file mode 100644 index 00000000000..10de39113fe --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e652cd030f299556fa1fc6455533c38ac953cae0bd472248aed8034d7f19a12e +size 3129 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png new file mode 100644 index 00000000000..980b2d57aba --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a189aa2bcce54111a8c078985ca611efde6e10dde26bb227bf1afafa2d065cc0 +size 3250 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png new file mode 100644 index 00000000000..0a497294064 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c44f892307b93c5bb48b836d47c5c66a318e299ed59ae96ca8107eecb5ad1012 +size 5621 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png new file mode 100644 index 00000000000..86f7c7e3145 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82245981f37c430eab84149196ccc9769fc90347b1b8fc28942dcdd7d3a5357c +size 5711 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png new file mode 100644 index 00000000000..456cc163ea3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b702e865b2a24a3ebf2028662a7aa1f9dc4176e90761aa50627736a65f4a8000 +size 5368 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png new file mode 100644 index 00000000000..efa1d0dbda7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cf08481a25b64169c03ee6321326f2108ed37a08b4ade5e75c7b3ff13e63ce7 +size 5393 diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png b/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png deleted file mode 100644 index 89032243eae..00000000000 --- a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cea3d365efe7e033cfd1fb8bc408fa0853d769af9cf7fcd8c1217d7c1e7982ba -size 15184 diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png b/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png deleted file mode 100644 index dde67257b60..00000000000 --- a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3503662354e0c76df5ed180db789547fa2a1b1099892d9628df8f5510a0d385b -size 14312 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png new file mode 100644 index 00000000000..bfe8d842cf7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0f41bf819fed7de2b99f15776cdb0d353f9bfaa4526dbb857f12be7c1343881 +size 15185 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png new file mode 100644 index 00000000000..914374bffd9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50328c742ab8f5d52b66bc5e693277733be6adb62262925c16527e92b52bf47a +size 14309 diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Dark.png rename to test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Light.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Light.png rename to test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png new file mode 100644 index 00000000000..7093082f758 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84ff989328d86ec68ab4b2771e8798051794c85c80e830e6061bf7b538dbe342 +size 1998 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png new file mode 100644 index 00000000000..e490a13bc02 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d590d4f26fb679fc1c0be2031660fc0568d0d5512e8f7272c81ad6932cde7079 +size 1994 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png new file mode 100644 index 00000000000..5df4b562cf9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfc063153b0a031ec15142d02b494acf778323b3f77c2d4a470936c8f52ac481 +size 7895 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png new file mode 100644 index 00000000000..942afa17055 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f61d72a6e32d7f82c198c827d999462dbb84f3154220ff9b96b2339b9c2ee4f7 +size 7832 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png new file mode 100644 index 00000000000..993c9cdea97 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f4b1725f22fe74e252757ef5e3011112c86831087eb0762518295af64f085da +size 1604 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png new file mode 100644 index 00000000000..fa997f05c87 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecf877a05b5bab1f506ef7db0369e535434ef4ec712ae6cfc1e34aa62defd492 +size 1567 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png new file mode 100644 index 00000000000..b09bad6fccd --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0ab968d64c65fbd57da2751037b3795dd44edfdb4288e1b8164affce136d694 +size 4924 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png new file mode 100644 index 00000000000..b55193ca7a0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:715427f1205744485b04ca1e1c2371116d53cc0078ba7b59b39ce492c64499ae +size 4901 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png new file mode 100644 index 00000000000..4ce32cefd91 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5674c1f5e9cfbf497eec8dccb23abb9bace0742e74804ea752b2fd652bdb75a3 +size 3541 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png new file mode 100644 index 00000000000..65dca4cf368 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:857f0f66eac110ae84befc845a8557fd85d64d3c9b3c38fb3f1390b98e5eea47 +size 3567 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png new file mode 100644 index 00000000000..eed451005b5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bf0c0deb7567010db027a90da11dbe87b2e0e0e1a19b80bde4ae389363451d3 +size 15762 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png new file mode 100644 index 00000000000..5822d248ad9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aa1238a25cb27637f4e6505dbbdcb2a4d732a45d6238a46f55205bb9db1227f +size 16001 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png new file mode 100644 index 00000000000..41af30d168d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d37f98e5c7c4534b0500f0ee559fccea0136b53bafd5ae69cd5c5005fd1bbad +size 9914 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png new file mode 100644 index 00000000000..b59acaf8d25 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70cb69e09936cfa033136995be7ab2d25ddc73fa4043004953d4c7b685adff1c +size 9803 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png new file mode 100644 index 00000000000..111aefc0557 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1e88af6f572f98d7110e3efd9c7980446aeee5dc46bc30ae033130788da222e +size 24999 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png new file mode 100644 index 00000000000..f74bfcb0b0d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89e45041078c82051075e4882b4a8beffae1298d3610270c9d95049c5f05c5b9 +size 25094 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png new file mode 100644 index 00000000000..55185e54f2a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e7ffe28aa3ddcaf263df3d829825c6941274a67d454c3416ebdfdc56c7b2fb +size 25463 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png new file mode 100644 index 00000000000..1457a5c4359 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f33c81825ee8ebb528dfdc7dacd1689dd28dedf2250faaf38ca21db82f9bd25 +size 25513 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png new file mode 100644 index 00000000000..a4e786b07a0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29edbbb4350570a92063105121d358bb82557d02e297783ac9aa94ca5857db99 +size 10082 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png new file mode 100644 index 00000000000..fc33b965edf --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aafbf7aab60ca0474cd01a36192fa4faeab7eeb3b9e7d432121f1ae977c1441e +size 10033 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png new file mode 100644 index 00000000000..bd57128f488 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:183fd97931b5f9edd0cdf7c6374328f6fa12ac728246c4156b8416f68f9d6960 +size 1954 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png new file mode 100644 index 00000000000..3c0dbd6ae6b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa5471c7656b76e40d6e7123ee8506aeb9a4b42a7157b9bf24a02e117017b6cc +size 1918 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png new file mode 100644 index 00000000000..b5cab20b4ff --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e725fcfce7606554463ee6aa335a3530a6e37935b2a163e78d940b3557e58d6 +size 17933 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png new file mode 100644 index 00000000000..12a8743c86e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90b08fe37901ea04d0b815f73014168cb2ec0643c4e7f8fa8993783384e884b3 +size 16311 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png new file mode 100644 index 00000000000..b12335959e1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4bf1714a905c76cc16aae7c5b7f20c6418018e0820e946831348797c13bb2a4 +size 27743 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png new file mode 100644 index 00000000000..2d24452c304 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:153095490477eaa37e4a0648c41e30e7ba8d62dd699a6e3cf3ec04399bbfa91a +size 27090 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png new file mode 100644 index 00000000000..aeab5d11124 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b2e11c4f828e0fbf8ca8ef0c7607965aa1b974280851806a038a45f04b9de9 +size 23556 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png new file mode 100644 index 00000000000..6f13702b87e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb5027255ce762a6de6990d20d43a0aae0365ce3e2ca69e374b9b8791a4d758e +size 23201 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png deleted file mode 100644 index b5f1d62d15e..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05f0f518e03f8b91e2178761c977215bd2561b10b04ad69685aa054231cd81be -size 15058 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png deleted file mode 100644 index 8d17c778055..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c5de42c54bcd1d35f59711f2c8c9c24f747df926017f1c3a52e3703c9d69da7 -size 15326 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png deleted file mode 100644 index 597e7cbbc67..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7510376219beff4c9bb17bccb3de3631a633bda27fe53d6418309de484167688 -size 7506 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png deleted file mode 100644 index 93470614a41..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8918a56e08760211087b9cd0c798758cb7a55c002fcce1b343d06fd9f078197f -size 7434 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png deleted file mode 100644 index 356200049e4..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a324c6cde2596228ddc260d04688bf5153860b7e7de5b41f9211b456285ee581 -size 25804 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png deleted file mode 100644 index 2b1bfefce81..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0eb74e618241b7d863bf8bac7132204b332c47e44e15a9c0b12270fa31a4fb71 -size 25874 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png deleted file mode 100644 index 1ecda11f8e8..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d5ede2eec1393f07f015cd724e9bb0a84492d85b175577705e195ee948e80ab -size 26210 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png deleted file mode 100644 index 5025b7c37cd..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:695e11e18a05b7f9d0b73612edc7f4b0288408bedbf7fc259fccb2c7fe5f7dd0 -size 26288 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png deleted file mode 100644 index 2bc00d0ba84..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d03152872902d21cea55c0d8ba464894c9a6ec77e7a58f68360c3be26a9a62a4 -size 7302 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png deleted file mode 100644 index 26a90f10d7a..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d727ac8876b301c7bf81d799197b6a8b0962a95bc9eb9b269e28f6c1eb4acd88 -size 7246 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png deleted file mode 100644 index ccf46b5de69..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a3e31fb939e811c76b4fb03a7211cc84b86794dbdce2ecf26d1d42324dc86fd4 -size 2099 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png deleted file mode 100644 index 29bea3c013d..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d7fd7e2f20d9a7a8e2ea867e0383e88291547b5e9d2c04642a84b5087491f64 -size 2064 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png deleted file mode 100644 index 7b0b53c68a4..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c84efc6a1c010c303da05377ae980ead28d0f7412d0ce0e49bfc66ce23c766d3 -size 20333 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png deleted file mode 100644 index 858f4e5ccd3..00000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1cf2233bec9a1ab64995e6168081da43e485d6b2388f868648613d63034160b8 -size 18278 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png b/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png deleted file mode 100644 index b48c12e655e..00000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b99678e6d41e30c874e0517eb6d97ccb3eaece31c489482fae81dd92904051c9 -size 14963 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png b/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png deleted file mode 100644 index 8b2121d3a7c..00000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfaa85a662524e06f3d8323bf9a4655822d36567ffa438db85719a79addb6cd3 -size 14439 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png b/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png deleted file mode 100644 index f82d6f544e6..00000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38e0cc34e642669cd200ac977f5e8948c100c100457f202b20a642ca211ab959 -size 6540 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png b/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png deleted file mode 100644 index ecbb1c1dc43..00000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9dfd9431a64258c6939f06c59d9163d33b65278cfd3ace4ae3c269f1e7e12d40 -size 6111 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png new file mode 100644 index 00000000000..524b085f8b0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab8379f105e879912a52e2d25c88e26870df79bd220a6789a3f61e010d2f6788 +size 14944 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png new file mode 100644 index 00000000000..cd0c2d36a62 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e641aa3e70e91cfbaf2db48fd1a095850a422b55b0acbeef4b2fdd74d07674e +size 14446 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png new file mode 100644 index 00000000000..7527ac7cf76 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94aa1fc6a75d1506459f1a0bfe4d29723350a59e3d4c5002a4916461245bcd6b +size 6517 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png new file mode 100644 index 00000000000..0de7a3d04ab --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42e4676c9b49dec849b275057f4e8a06a3acbb7fb54304d9fb9509ff7277459d +size 6110 diff --git a/test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png new file mode 100644 index 00000000000..48dbd2f491c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9df123fb04b4c6ecd4337bb19af8626b095c3fd5f149c0bcb2d46d216fa392ad +size 33087 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png new file mode 100644 index 00000000000..2c81c27c782 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:531f9d16ecde8fc7b868eafc0541f04dfa476065095119ef2b02be2ee85a4788 +size 32732 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png new file mode 100644 index 00000000000..027b13b8042 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa6ec0238ac1ac4974548b73e205b758e910d0604d1c28e0f26231e1df8129f2 +size 31006 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png new file mode 100644 index 00000000000..82991aea52e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:416cf9214bac2a0e275f9cf83c41e656e94de14ee870886f55caae230eb8871d +size 30907 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png new file mode 100644 index 00000000000..d55a1ec7dfc --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e8973161acbdd94816922272fcb0406263e58f624d86c0d7980c329a65a1b24 +size 11278 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png new file mode 100644 index 00000000000..5a87432a3d4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d3166596fd41609c4a622d7f7267fd6f3f9605d893efc34aac65ef7881ad71 +size 11203 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png new file mode 100644 index 00000000000..9da6404a655 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39335e6fa03078f804f848b634329c9391e4d8fe099d0fa1e0d4808ba4eb3a7 +size 11117 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png new file mode 100644 index 00000000000..42acebe80c0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c70bb66d053f20dccf9075399a801dceaa92d5830642e8f2afc9f89c81d9315c +size 11005 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png new file mode 100644 index 00000000000..4f1806f3f3e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a54d85c0fcf622977f412e8fc2953c973f1da427c91d319d0708b6e758969f3f +size 10280 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png new file mode 100644 index 00000000000..ad3fa141565 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d494bbb9ebfbf2156c5d7a21f39c52e14313a5218af083cd91f2dbc6199f62da +size 10195 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png new file mode 100644 index 00000000000..1dbc63aea44 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e4226c71af8ae21a5f0c13ba521b50518afb4a39831e832d725ebe0289344ff +size 10266 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png new file mode 100644 index 00000000000..224da35d3dd --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518ecbddf013bbc749b62a8a74bad39d7683332ebe0d9f2a4b7b13ad54fcabab +size 10104 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png new file mode 100644 index 00000000000..1dbc63aea44 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e4226c71af8ae21a5f0c13ba521b50518afb4a39831e832d725ebe0289344ff +size 10266 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png new file mode 100644 index 00000000000..224da35d3dd --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518ecbddf013bbc749b62a8a74bad39d7683332ebe0d9f2a4b7b13ad54fcabab +size 10104 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png new file mode 100644 index 00000000000..19c633f2edd --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3923f85be8bcdbfcdecfd80b07abdbd71763edc379de4e6a5e946f20f160db5c +size 55650 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png new file mode 100644 index 00000000000..823489ce7fc --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e81911488f22e9517b9e2f8bd7b8bc7a14d7fff1b2d012c2368b840d4bcb49cf +size 55028 diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Light.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Light.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png new file mode 100644 index 00000000000..cd464aa178c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b0b1f65ed8a0963da2b4f7f64b3d06683895a57d8101856f711742954cedc23 +size 23358 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png new file mode 100644 index 00000000000..b293ccc538f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f348cbe0e5d401778f4f532b6254d981f9ac98220b290d8d5401dcd4526410 +size 22475 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png new file mode 100644 index 00000000000..8d9e892d9e3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0222974a073f5b12431b1ebc4f498ce8a795b860016520e103a35d6a68401d98 +size 13689 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png new file mode 100644 index 00000000000..5e93e621faf --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:998dde573af3df84cb836248c71f2bc141eb6845a7c407852f1a02969b14dec5 +size 13538 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png b/test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png deleted file mode 100644 index 55c823622c7..00000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ed115cf70c25bc400d265a08db2e1d6a4d7596674d7a6ddc932ca1ad33ccaa25 -size 33367 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png b/test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png deleted file mode 100644 index 9e80b63bb48..00000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0cfebc3c9b7caa2d40cfb8db5b47ae6f5a446bb72883bdce5198ed5d296ae82f -size 32889 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png b/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png deleted file mode 100644 index cc9b2aa4376..00000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d3a4d62c5226a9ca16b2327d95337a68286107a4fb12d1d5bc43530f17f6b1d -size 33379 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png b/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png deleted file mode 100644 index c8a73252512..00000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:522ba17bdd1d4378c0be262631d8e6bb1fd867f2999c07c06c4991cd9971a4cb -size 33114 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png deleted file mode 100644 index ebee6a6096c..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0286377eace47ff7549c02aca2f00ad5c0200b29187ad187554b16304a93127c -size 11188 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png deleted file mode 100644 index 63b05939a6d..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0af0112e3441d983dc0cde0a2bfa60542a32dfe552c1c377d8cf07c08b275429 -size 11067 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png deleted file mode 100644 index ad2595b0483..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc4b2b1a2972264878f5a4018c1030a2d1f34a5d5f468a1820f48cd041817421 -size 11036 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png deleted file mode 100644 index 82b0147e7f2..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d8cb9f3a30edccfc6d304365b103c9787d2a1dbd2c7c8b011ef05d37825f326 -size 10906 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png deleted file mode 100644 index 63033ca9918..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b1e1b59a00dcea1e7f5f360907850f889959158f1ac32a7c276d771e2dea768 -size 10179 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png deleted file mode 100644 index 4d303775e26..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8438c8892a19b9b479e706efb002ec30c9b0c17cfd1575c0a4a883c6aba74e89 -size 10049 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png deleted file mode 100644 index 201fc1a7bf5..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a88fb3139a351b8c46df4309f64ed71210c456d4a89aad6b087ee512e26d9e1b -size 9762 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png deleted file mode 100644 index be9dca0e21b..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0679cbdb00bacb421b6c8c65b632211a5dd153ae4c698c7a49903e53632310f -size 9569 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png deleted file mode 100644 index 201fc1a7bf5..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a88fb3139a351b8c46df4309f64ed71210c456d4a89aad6b087ee512e26d9e1b -size 9762 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png deleted file mode 100644 index be9dca0e21b..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0679cbdb00bacb421b6c8c65b632211a5dd153ae4c698c7a49903e53632310f -size 9569 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png deleted file mode 100644 index 83d5938ca6c..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05deae07ae1a6a5f738d6ebd4d475ed2e7d14630977b8b64cdae9030da2885ff -size 55645 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png deleted file mode 100644 index 23db3df30fb..00000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d9d81860fc8c296f9cc718ed3e6690669eb0f7301c17ae2f15639c984d499178 -size 54993 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png deleted file mode 100644 index dba2c92968e..00000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c54a17b190dde0caa1f175b1ef025850a6ede44f03d8d124859355c309aa28a -size 27557 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png deleted file mode 100644 index 63742c15bcd..00000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eac6ff34521293e8f1f7e70f3f1e4227cc371861763fe954cce62d49b181955b -size 26846 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png deleted file mode 100644 index ffc64ba9aac..00000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:099e8f6e3b7c1c33fae6a75f47e387ab7f87536953607ec0f3a7b5160e0d4d59 -size 23377 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png deleted file mode 100644 index c9ced02aeb7..00000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e7def912d49d13cd496386ef22a39df862a558e5585b01bfc46fd6f5f3276d4 -size 22969 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png new file mode 100644 index 00000000000..f6117771773 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a +size 3899 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png new file mode 100644 index 00000000000..f8d4a437c6f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e +size 3830 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png new file mode 100644 index 00000000000..586892704bb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3da890451d346196f63be699e367ad2eccf46091167ac005d4eb5adb8169d12d +size 3778 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png new file mode 100644 index 00000000000..d5f5baa7a48 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc0b866a49984f2e17b2da44d15fa4525a21519010e4df5a0651ffcc88b594f0 +size 3674 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png new file mode 100644 index 00000000000..a628d1aae9c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60be48fef4a63200229dda97c1f8664e9410d76be3a113926355867c8f58c3fa +size 3815 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png new file mode 100644 index 00000000000..0f6db97e2ab --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d8727fcac81f4669821e279f370cde522e2be40758d2d2975a9331466e6d49 +size 3716 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png new file mode 100644 index 00000000000..f6117771773 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a +size 3899 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png new file mode 100644 index 00000000000..f8d4a437c6f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e +size 3830 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png new file mode 100644 index 00000000000..9c33813aae3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c282cd99bdd694629caf8bd310013e3aec0f5b25dd5376ac7c1bb897a1b4388 +size 1593 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png new file mode 100644 index 00000000000..753352cf64f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1380daf9f875ed2502b0669af7844bae000168bcba00d89ea5d6634f590637c1 +size 1606 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png new file mode 100644 index 00000000000..f6117771773 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a +size 3899 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png new file mode 100644 index 00000000000..f8d4a437c6f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e +size 3830 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png new file mode 100644 index 00000000000..aa6921a198c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263cc0cd0dd5fe9eb7839cd0ff5c1698c26b76fd162c6f20cd9bf048094cb308 +size 4299 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png new file mode 100644 index 00000000000..7a19687be82 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:651452ec039ff26c1fce42ccd541d0ce70f39a993457939be3bcd3f1d67ec4cb +size 4202 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png new file mode 100644 index 00000000000..586892704bb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3da890451d346196f63be699e367ad2eccf46091167ac005d4eb5adb8169d12d +size 3778 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png new file mode 100644 index 00000000000..d5f5baa7a48 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc0b866a49984f2e17b2da44d15fa4525a21519010e4df5a0651ffcc88b594f0 +size 3674 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png new file mode 100644 index 00000000000..3dea19001f2 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c6eba3c086a1b7c69b8f1bff63ccd43bbe6bad82a049e8b32230e4ce7ffde07 +size 1367 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png new file mode 100644 index 00000000000..3e8db51c450 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b782263b3ffed17f79b95444cc6f479e9408ed252f5d1c227b4c7f741858b82 +size 1240 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png new file mode 100644 index 00000000000..cca6f012d85 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa8e4d7c2bffaa4b3fd149ce1316b78ba9ac40ce5878a8867dd6422342c5a18f +size 3977 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png new file mode 100644 index 00000000000..c0152452dbf --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13659a984dabca598789efd2b5a3f0753c5d8d8ba705b018c046ee1ebf9d861b +size 3855 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png new file mode 100644 index 00000000000..dc686609d7f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6332afcc844f37a800f11ba7b49f8d5ef61f160f9ea285a34fcf08df462c8de +size 1749 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png new file mode 100644 index 00000000000..dbca2be966b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c567d79fce21467c90bdf833d5d387c1a7e91070fa908d7fdb53595a0adfcb1 +size 1686 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png new file mode 100644 index 00000000000..3c121b4057c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b217c46a4e2cfd993def8baf8e2fb2808a825d0b38fad553ec82a65ee1148e72 +size 1936 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png new file mode 100644 index 00000000000..08073d4119a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86dd22c02f0f4d1f4a46ce55f8546dfe70032a199ffa10683a9daa47cecfad59 +size 1889 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png new file mode 100644 index 00000000000..162ecbb0952 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdc6ca3ffa8ea88ed6184379239b81d0ecd90cfc56dc3cecd84be616d3c22c0 +size 8122 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png new file mode 100644 index 00000000000..03dd01d13f4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ff42ad20ea3c0fa7e43d1db6fac737890f149942caff51e3e0e1d041a3c015 +size 7865 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png new file mode 100644 index 00000000000..2bebcf187a9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a797c09dcd81f2128eb937eb53f1f7a28131978a7f654dc95092b39172d098 +size 9388 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png new file mode 100644 index 00000000000..556520e8b8b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a26cc493b80a889baa2495307abd147405ef256c0713a84fcae26cb5918b39e4 +size 9131 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png new file mode 100644 index 00000000000..621b8b35147 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b835e8c51e723ecb0a11cb167b81a76b4a4938895a1b05291ac5fd609446923 +size 8341 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png new file mode 100644 index 00000000000..b5d3ff1d64e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae32840ac5637faa899f21c94acdc7e2f5c065d705e2a3b103a23a381849dfde +size 8065 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png new file mode 100644 index 00000000000..d06f5a35857 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:125f1d570fdad186e94fc580d44e8560b6ecc3de58e3adc86d23045bdda21a8d +size 7784 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png new file mode 100644 index 00000000000..9ce0500c3ba --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afb19ee8b136f65d80f8aa7de11cf983926458999b153568d6fe6303ab5c0236 +size 7692 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png new file mode 100644 index 00000000000..01d70e70f0a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28bdc58d5f8ec2b28df3b6762fddb4dcc9477fd3dfe98af5a7d4c311ebf4e3c1 +size 7218 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png new file mode 100644 index 00000000000..4ffae5781dc --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:977c95e98e10ec21093424a2d4038131b9d95c69d27ef225b501529c4308e497 +size 7076 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png new file mode 100644 index 00000000000..571d58ad4c3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:595c42b464f87696ead04be8a367c1ba36dd75ec5115fa00ad7ee2ddc80c7644 +size 6875 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png new file mode 100644 index 00000000000..3c0f774ad10 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d393b6215ebc0f299a2a7e21076835c6b3ee9035b94917ddc5a7d298222c1b3 +size 6706 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png new file mode 100644 index 00000000000..acbc58e86db --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abdf653a0b09dd50655217b4c05e55cbb7a1f53ab8768809125d4eff48cace22 +size 7523 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png new file mode 100644 index 00000000000..7755b21fe51 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:269f58f05dd9f4cea1b4f09a92f6f58b2cc55345e9252b05297b7328489dd0bf +size 7303 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png new file mode 100644 index 00000000000..a574cc4e101 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:631998b418a3af1a791470fb4b557e2b3fcd35d66d50e6c98e5c70d578b016cb +size 7367 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png new file mode 100644 index 00000000000..9f89acdebc1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f96bd7a669274b547229bce41ffc400983dc4656d97ecf820fc94d65df1a418 +size 7078 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png new file mode 100644 index 00000000000..1b1a0fe693f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e15ec5f380f9bc357fc6ddbd02b954930815bbdabd0d86c52b9e05550ad3a21c +size 7101 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png new file mode 100644 index 00000000000..0fd86f37325 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f50b8ee6944db9878190b9ec57fa87c4032d6697cb3827db3fa651f25aa38183 +size 6980 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png deleted file mode 100644 index 20494f46254..00000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:869d6db0575e1e0dce468df0227cd68c1725d7341c5efb5b47ab929d3717761d -size 22978 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png deleted file mode 100644 index aefc052d100..00000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cff0734d5bbb48f8900e8cff6892aab851f58206db69a4c0a49425c0981f3967 -size 22208 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png deleted file mode 100644 index f44709ad5bf..00000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b84d2c246a9594012baff0a70212b6768e6b0d784936c4f2f9d37378cf813248 -size 13541 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png deleted file mode 100644 index 2f008a94f3e..00000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c3aba099609c37eb2e58de791958665aec9fd8d2931b2208ec91515cac41b96 -size 13386 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png deleted file mode 100644 index 4024eff5bc9..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63aa8f046ab23ac6bd53722db86fd254c7cd88a487f45feb68cc9a9bbd18300f -size 1209 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png deleted file mode 100644 index 5baee833287..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d69bf362154015616fbb8c95052466061b7982f54203faea3ae89ad5afeaec3 -size 1221 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png deleted file mode 100644 index 1197206909f..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43c9b8702922e8892a0cf20afc053e8ac56cdff65c02a9d47cb0183262da7bd4 -size 1908 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png deleted file mode 100644 index 11d36a29387..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e0a17ec5b2f0e18be88383e491a09260654ce828236e37c11d5028e6ac326daa -size 1924 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png deleted file mode 100644 index 0e6b503cc23..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6897095d4c9d17a31bce693d2ce0f800d13ec56f2e27f4c5ec303ee64c7d19c3 -size 2189 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png deleted file mode 100644 index 91af710d879..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3491ca579cf94b7148f4a943dc1e1a3eb44dd444d5268963c6a15a949d13bf88 -size 2056 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png deleted file mode 100644 index db4f67e3b5d..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3345ce1dc95c59d627fd618ddbf2f4028f0e7b13528bd3325fe6882cfb73891 -size 1781 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png deleted file mode 100644 index a59cf06f888..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d4921395a33a571db185f7015160a23ee6e8ba3f82281a8ab4a935e43950b44 -size 1811 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png deleted file mode 100644 index 28c4dcb5478..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a7bc3e108e03debccd5881474ff30f42bf5c56a43a7b4525d3d2fc16b075b56d -size 2482 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png deleted file mode 100644 index 118bab91cc1..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8df766b159576a0c12683af17f7d9ed391a2bd765743494f3760d8b415380c22 -size 2252 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png deleted file mode 100644 index 9bb317db875..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c1f6f054881033c6162405b99e99ce8f049b9b20d320a74abc07793987474d9 -size 2249 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png deleted file mode 100644 index c97505c005c..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b6101c92d7073116c81eba73ddc08c721c2e947b4f7714887c54ffb91284d4e -size 2138 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png deleted file mode 100644 index c1853513bec..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3179d62298aff75dfc01442a4589a61d83b6ba7f14c4b8441c9a688ca655738d -size 2434 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png deleted file mode 100644 index 8060149471e..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5e9dbcbf22233e5b48617067c8993a51c6b10afd940cd499270183e8eb4430f4 -size 2217 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png deleted file mode 100644 index 4b0fc104ee8..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13295a45a513349c0c8ae6129301d5f2fb89549cae3b4453c6962a770b6290ec -size 3716 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png deleted file mode 100644 index d7a897ebda5..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21dbc34d1ec2776453bc176631f2a0e9a03f7fcd528369efcb40fd5c49536c92 -size 3837 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png deleted file mode 100644 index db4f67e3b5d..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3345ce1dc95c59d627fd618ddbf2f4028f0e7b13528bd3325fe6882cfb73891 -size 1781 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png deleted file mode 100644 index a59cf06f888..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d4921395a33a571db185f7015160a23ee6e8ba3f82281a8ab4a935e43950b44 -size 1811 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png deleted file mode 100644 index 017a1ebcebe..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f97568f25aeafee2be621a345675fb29afe6c17a6655eeb2b053e09a671f2fe7 -size 2116 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png deleted file mode 100644 index 230402c3327..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:faf0132e6298ffe9f285c4a2d6b7a0ef13c440175a22fa8febaff6b881cb4907 -size 1932 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png deleted file mode 100644 index 4024eff5bc9..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63aa8f046ab23ac6bd53722db86fd254c7cd88a487f45feb68cc9a9bbd18300f -size 1209 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png deleted file mode 100644 index 5baee833287..00000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d69bf362154015616fbb8c95052466061b7982f54203faea3ae89ad5afeaec3 -size 1221 From a1ef9c86f2477374e90957b35c856543992c7926 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 4 Mar 2026 19:57:17 +0100 Subject: [PATCH 177/448] fixes change detection in vscode-extras --- .../vscode-extras/src/npmUpToDateFeature.ts | 21 +++++------------- build/npm/installStateHash.ts | 22 +++++++++++++------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts index df9abf863ae..8927b0b7064 100644 --- a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts +++ b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts @@ -197,26 +197,17 @@ export class NpmUpToDateFeature extends vscode.Disposable { return ''; } try { - return this._normalizeFileContent(path.join(this._root, file)); + const script = path.join(this._root, 'build', 'npm', 'installStateHash.ts'); + return cp.execFileSync(process.execPath, [script, '--normalize-file', path.join(this._root, file)], { + cwd: this._root, + timeout: 10_000, + encoding: 'utf8', + }); } catch { return ''; } } - private _normalizeFileContent(filePath: string): string { - const raw = fs.readFileSync(filePath, 'utf8'); - if (path.basename(filePath) === 'package.json') { - const json = JSON.parse(raw); - for (const key of NpmUpToDateFeature._packageJsonIgnoredKeys) { - delete json[key]; - } - return JSON.stringify(json, null, '\t') + '\n'; - } - return raw; - } - - private static readonly _packageJsonIgnoredKeys = ['distro']; - private _getChangedFiles(state: InstallState): { readonly label: string; readonly isFile: boolean }[] { if (!state.saved) { return [{ label: '(no postinstall state found)', isFile: false }]; diff --git a/build/npm/installStateHash.ts b/build/npm/installStateHash.ts index 1ee80522d6c..f52c0a4696d 100644 --- a/build/npm/installStateHash.ts +++ b/build/npm/installStateHash.ts @@ -141,11 +141,19 @@ export function readSavedContents(): Record | undefined { // When run directly, output state as JSON for tooling (e.g. the vscode-extras extension). if (import.meta.filename === process.argv[1]) { - console.log(JSON.stringify({ - root, - stateContentsFile, - current: computeState(), - saved: readSavedState(), - files: [...collectInputFiles(), stateFile], - })); + if (process.argv[2] === '--normalize-file') { + const filePath = process.argv[3]; + if (!filePath) { + process.exit(1); + } + process.stdout.write(normalizeFileContent(filePath)); + } else { + console.log(JSON.stringify({ + root, + stateContentsFile, + current: computeState(), + saved: readSavedState(), + files: [...collectInputFiles(), stateFile], + })); + } } From 9404c9733f2c7dfc476b9fd451ffc102216880c6 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 4 Mar 2026 14:44:12 -0500 Subject: [PATCH 178/448] Fix context widget hover race (#298273) --- src/vs/platform/hover/browser/hoverService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/platform/hover/browser/hoverService.ts b/src/vs/platform/hover/browser/hoverService.ts index 6779e7e1c5e..cfb53e2e686 100644 --- a/src/vs/platform/hover/browser/hoverService.ts +++ b/src/vs/platform/hover/browser/hoverService.ts @@ -248,6 +248,7 @@ export class HoverService extends Disposable implements IHoverService { } private _createHover(options: IHoverOptions, skipLastFocusedUpdate?: boolean): ICreateHoverResult | undefined { + this._currentDelayedHover?.dispose(); this._currentDelayedHover = undefined; if (options.content === '') { From 7508207d2935445d7e76b46920939b1b4613c2f2 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:45:02 -0800 Subject: [PATCH 179/448] autopilot mode + secondary chat input toolbar (#296691) * autopilot mode + /yolo commands * fix tests * revert fix * fix disposable leak * address a few comments, make sure it works when switching sessions * make some tests * fix tests * some reverts to cleaner state * add secondary toolbar, permissions * don't use query selector, surface toolbar in the template * UI polish: context usage widget, secondary toolbar layout, theme tweaks - Move context usage widget to secondary toolbar with percentage label on hover - Adjust secondary toolbar padding/gap and input part bottom padding - Lower icon-only threshold to 300px - Darken input placeholder foreground in 2026 dark theme - Update agent mode description * update names * update api for tool call limits * add true autopilot * move error retry logic to extension * address some more comments * make sure to hide tool * bump version # * better tool description * fix conflict * enterprise restrictions * revert some stuff, fix sessions window containers * fix actions * fix delegate vs. session target * fix compile + add setting --------- Co-authored-by: David Dossett <25163139+daviddossett@users.noreply.github.com> --- extensions/theme-2026/themes/2026-dark.json | 2 +- src/vs/platform/actions/common/actions.ts | 1 + .../common/extensionsApiProposals.ts | 2 +- .../api/common/extHostTypeConverters.ts | 1 + .../browser/actions/chatExecuteActions.ts | 83 +++++++++- .../contrib/chat/browser/chat.contribution.ts | 6 + src/vs/workbench/contrib/chat/browser/chat.ts | 6 + .../tools/languageModelToolsService.ts | 40 ++++- .../chat/browser/widget/chatListRenderer.ts | 6 +- .../contrib/chat/browser/widget/chatWidget.ts | 8 +- .../browser/widget/input/chatInputPart.ts | 118 ++++++++++++- .../input/permissionPickerActionItem.ts | 156 ++++++++++++++++++ .../chat/browser/widget/media/chat.css | 92 ++++++++++- .../viewPane/chatContextUsageWidget.ts | 9 + .../widgetHosts/viewPane/chatViewPane.ts | 1 + .../viewPane/media/chatContextUsageWidget.css | 23 ++- .../chat/common/actions/chatContextKeys.ts | 3 +- .../common/chatService/chatServiceImpl.ts | 2 + .../contrib/chat/common/constants.ts | 21 +++ .../contrib/chat/common/model/chatModel.ts | 3 +- .../chat/common/participants/chatAgents.ts | 8 +- .../tools/builtinTools/askQuestionsTool.ts | 75 +++++++++ .../tools/builtinTools/taskCompleteTool.ts | 73 ++++++++ .../chat/common/tools/builtinTools/tools.ts | 7 +- ...scode.proposed.chatParticipantPrivate.d.ts | 9 +- 25 files changed, 718 insertions(+), 37 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts create mode 100644 src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index b7652bcfb0d..3d9c30b258c 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -34,7 +34,7 @@ "input.background": "#191A1B", "input.border": "#333536FF", "input.foreground": "#bfbfbf", - "input.placeholderForeground": "#777777", + "input.placeholderForeground": "#555555", "inputOption.activeBackground": "#3994BC33", "inputOption.activeForeground": "#bfbfbf", "inputOption.activeBorder": "#2A2B2CFF", diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index f7168ac83af..e563293ea2a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -255,6 +255,7 @@ export class MenuId { static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteQueue = new MenuId('ChatExecuteQueue'); static readonly ChatInput = new MenuId('ChatInput'); + static readonly ChatInputSecondary = new MenuId('ChatInputSecondary'); static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly ChatModePicker = new MenuId('ChatModePicker'); static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar'); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index fb796867b8b..a9bc2d2fa10 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -60,7 +60,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 14 + version: 15 }, chatPromptFiles: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts', diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 22adb59bbae..9c5adf7bc0d 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3440,6 +3440,7 @@ export namespace ChatAgentRequest { editedFileEvents: request.editedFileEvents, modeInstructions: request.modeInstructions?.content, modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), + permissionLevel: request.permissionLevel, subAgentInvocationId: request.subAgentInvocationId, subAgentName: request.subAgentName, parentRequestId: request.parentRequestId, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 29fb309c4db..58043423887 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -22,11 +22,12 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ILogService } from '../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { getModeNameForTelemetry, IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatVariableLeader } from '../../common/requestParser/chatParserTypes.js'; import { ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatService } from '../../common/chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { isInClaudeAgentsFolder } from '../../common/promptSyntax/config/promptFileLocations.js'; @@ -447,6 +448,44 @@ export class OpenModelPickerAction extends Action2 { } } } + +export class OpenPermissionPickerAction extends Action2 { + static readonly ID = 'workbench.action.chat.openPermissionPicker'; + + constructor() { + super({ + id: OpenPermissionPickerAction.ID, + title: localize2('interactive.openPermissionPicker.label', "Open Permission Picker"), + tooltip: localize('setPermissionLevel', "Set Permissions"), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + menu: { + id: MenuId.ChatInputSecondary, + order: 10, + group: 'navigation', + when: + ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.lockedToCodingAgent.negate(), + IsSessionsWindowContext.negate(), + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + if (widget) { + widget.input.openPermissionPicker(); + } + } +} + export class OpenModePickerAction extends Action2 { static readonly ID = 'workbench.action.chat.openModePicker'; @@ -515,6 +554,18 @@ export class OpenSessionTargetPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.chatSessionIsEmpty, + IsSessionsWindowContext), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + IsSessionsWindowContext.negate(), ChatContextKeys.chatSessionIsEmpty), group: 'navigation', }, @@ -550,7 +601,19 @@ export class OpenDelegationPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.chatSessionIsEmpty.negate()), + ChatContextKeys.chatSessionIsEmpty.negate(), + IsSessionsWindowContext), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0.5, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.chatSessionIsEmpty.negate(), + IsSessionsWindowContext.negate()), group: 'navigation', }, ] @@ -583,7 +646,18 @@ export class OpenWorkspacePickerAction extends Action2 { order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo('local') + ChatContextKeys.chatSessionType.isEqualTo('local'), + IsSessionsWindowContext + ), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0.6, + when: ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.isEqualTo('local'), + IsSessionsWindowContext.negate() ), group: 'navigation', }, @@ -601,7 +675,7 @@ export class ChatSessionPrimaryPickerAction extends Action2 { constructor() { super({ id: ChatSessionPrimaryPickerAction.ID, - title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Model Picker"), + title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Primary Session Picker"), category: CHAT_CATEGORY, f1: false, precondition: ChatContextKeys.enabled, @@ -960,6 +1034,7 @@ export function registerChatExecuteActions() { registerAction2(ToggleChatModeAction); registerAction2(SwitchToNextModelAction); registerAction2(OpenModelPickerAction); + registerAction2(OpenPermissionPickerAction); registerAction2(OpenModePickerAction); registerAction2(OpenSessionTargetPickerAction); registerAction2(OpenDelegationPickerAction); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 954b313b84a..2c38f3c9874 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -371,6 +371,12 @@ configurationRegistry.registerConfiguration({ scope: ConfigurationScope.APPLICATION_MACHINE, tags: ['experimental', 'advanced'], }, + [ChatConfiguration.AutopilotEnabled]: { + type: 'boolean', + markdownDescription: nls.localize('chat.autopilot.enabled', "Controls whether the Autopilot mode is available in the permissions picker. When enabled, Autopilot auto-approves all tool calls and continues until the task is done."), + default: true, + tags: ['experimental'], + }, [ChatConfiguration.GlobalAutoApprove]: { default: false, markdownDescription: globalAutoApproveDescription.value, diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 7fa8e87b01e..9b3fc446d6c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -289,6 +289,12 @@ export interface IChatWidgetViewOptions { * redirect to a different workspace rather than executing locally. */ submitHandler?: (query: string, mode: ChatModeKind) => Promise; + + /** + * Whether we are running in the sessions window. + * When true, the secondary toolbar (permissions picker) is hidden. + */ + isSessionsWindow?: boolean; } export interface IChatViewViewContext { diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 13eb89e07cb..d9bc284672e 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -40,7 +40,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { IVariableReference } from '../../common/chatModes.js'; import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; -import { ChatConfiguration } from '../../common/constants.js'; +import { ChatConfiguration, isAutoApproveLevel } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -641,7 +641,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this.ensureToolDetails(dto, toolResult, tool.data); const afterExecuteState = await toolInvocation?.didExecuteTool(toolResult, undefined, () => - this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource)); + this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource, dto.chatRequestId)); if (toolInvocation && afterExecuteState?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { const postConfirm = await IChatToolInvocation.awaitPostConfirmation(toolInvocation, token); @@ -791,7 +791,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } // No hook decision - use normal auto-confirm logic - const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource); + const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource, dto.chatRequestId); return { autoConfirmed, preparedInvocation }; } @@ -996,6 +996,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo }); } + /** + * Returns true if enterprise policy has explicitly disabled the global auto-approve setting. + * When this is the case, Bypass Approvals and Autopilot permission levels should not auto-approve tools. + */ + private _isAutoApprovePolicyRestricted(): boolean { + const inspected = this._configurationService.inspect(ChatConfiguration.GlobalAutoApprove); + return inspected.policyValue === false; + } + private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined { if (toolData.id === 'vscode_fetchWebPage_internal') { return 'fetch'; @@ -1040,12 +1049,22 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return true; } - private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { const tool = this._tools.get(toolId); if (!tool) { return undefined; } + // Auto-Approve All permission level bypasses all tool confirmations, + // unless enterprise policy has explicitly disabled global auto-approve. + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; + } + } + if (!this.isToolEligibleForAutoApproval(tool.data)) { return undefined; } @@ -1077,7 +1096,17 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { + // Auto-Approve All permission level bypasses all post-execution confirmations, + // unless enterprise policy has explicitly disabled global auto-approve. + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; + } + } + if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } @@ -1186,7 +1215,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Clean up any pending tool calls that belong to this request for (const [toolCallId, invocation] of this._pendingToolCalls) { if (invocation.chatRequestId === requestId) { - invocation.cancelFromStreaming(ToolConfirmKind.Skipped); this._pendingToolCalls.delete(toolCallId); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index b559869616f..efd1ff467d6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -64,7 +64,7 @@ import { IChatRequestVariableEntry } from '../../common/attachments/chatVariable import { IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM, IChatPendingDividerViewModel, isPendingDividerVM } from '../../common/model/chatViewModel.js'; import { getNWords } from '../../common/model/chatWordCounter.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; import { ClickAnimation } from '../../../../../base/browser/ui/animations/animations.js'; import { MarkHelpfulActionId, MarkUnhelpfulActionId } from '../actions/chatTitleActions.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from '../chat.js'; @@ -2335,7 +2335,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (!shouldAutoReply) { + // always autoreply in autopilot mode. + const isAutopilot = isResponseVM(context.element) && context.element.model.request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot; + if (!shouldAutoReply && !isAutopilot) { // Roll back the in-progress mark if auto-reply is not enabled. if (stableKey) { this._autoRepliedQuestionCarousels.delete(stableKey); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 8a391fa71d6..eb97584dd41 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -64,7 +64,7 @@ import { IChatTodoListService } from '../../common/tools/chatTodoListService.js' import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../../common/constants.js'; import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js'; import { IHandOff, PromptHeader } from '../../common/promptSyntax/promptFileParser.js'; @@ -1552,6 +1552,7 @@ export class ChatWidget extends Disposable implements IChatWidget { rowContainer.appendChild(this.inputContainer); this.createInput(this.inputContainer); this.input.setChatMode(this.inputPart.currentModeObs.get().id); + this.input.setPermissionLevel(this.inputPart.currentModeInfo.permissionLevel ?? ChatPermissionLevel.Default); this.input.setEditing(true, isEditingSentRequest); this._onDidChangeActiveInputEditor.fire(); } else { @@ -1652,6 +1653,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!isInput) { this.inputPart.setChatMode(this.input.currentModeObs.get().id); + this.inputPart.setPermissionLevel(this.input.currentModeInfo.permissionLevel ?? ChatPermissionLevel.Default); const currentModel = this.input.selectedLanguageModel.get(); if (currentModel) { this.inputPart.switchModel(currentModel.metadata); @@ -1735,6 +1737,7 @@ export class ChatWidget extends Disposable implements IChatWidget { defaultMode: this.viewOptions.defaultMode, sessionTypePickerDelegate: this.viewOptions.sessionTypePickerDelegate, workspacePickerDelegate: this.viewOptions.workspacePickerDelegate, + isSessionsWindow: this.viewOptions.isSessionsWindow, }; if (this.viewModel?.editing) { @@ -2139,7 +2142,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const options: IChatSendRequestOptions = { attempt: lastRequest.attempt + 1, location: this.location, - userSelectedModelId: this.input.currentLanguageModel + userSelectedModelId: this.input.currentLanguageModel, + modeInfo: this.input.currentModeInfo, }; return await this.chatService.resendRequest(lastRequest, options); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 8bd16f83c31..db90c77372d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -85,7 +85,7 @@ import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEnt import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, validateChatMode } from '../../../common/constants.js'; import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; @@ -95,7 +95,7 @@ import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chat import { IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; -import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; +import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenPermissionPickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; @@ -122,6 +122,7 @@ import { ChatSelectedTools } from './chatSelectedTools.js'; import { DelegationSessionPickerActionItem } from './delegationSessionPickerActionItem.js'; import { IModelPickerDelegate } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; +import { IPermissionPickerDelegate, PermissionPickerActionItem } from './permissionPickerActionItem.js'; import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; @@ -169,6 +170,11 @@ export interface IChatInputPartOptions { * for their chat request. This is useful for empty window contexts. */ workspacePickerDelegate?: IWorkspacePickerDelegate; + /** + * Whether we are running in the sessions window. + * When true, the secondary toolbar (permissions picker) is hidden. + */ + isSessionsWindow?: boolean; } export interface IWorkingSetEntry { @@ -285,6 +291,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private container!: HTMLElement; private inputSideToolbarContainer?: HTMLElement; + private secondaryToolbarContainer!: HTMLElement; + private secondaryToolbar!: MenuWorkbenchToolBar; private followupsContainer!: HTMLElement; private readonly followupsDisposables: DisposableStore = this._register(new DisposableStore()); @@ -366,6 +374,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatSessionHasTargetedModels: IContextKey; private modelWidget: EnhancedModelPickerActionItem | undefined; private modeWidget: ModePickerActionItem | undefined; + private permissionWidget: PermissionPickerActionItem | undefined; private sessionTargetWidget: SessionTypePickerActionItem | undefined; private delegationWidget: DelegationSessionPickerActionItem | undefined; private chatSessionPickerWidgets: Map = new Map(); @@ -400,6 +409,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly onDidChangeCurrentChatMode: Event = this._onDidChangeCurrentChatMode.event; private readonly _currentModeObservable: ISettableObservable; + private readonly _currentPermissionLevel: ISettableObservable; + private permissionLevelKey: IContextKey; public get currentModeKind(): ChatModeKind { const mode = this._currentModeObservable.get(); @@ -412,6 +423,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._currentModeObservable; } + public get currentPermissionLevelObs(): IObservable { + return this._currentPermissionLevel; + } + public get currentModeInfo(): IChatRequestModeInfo { const mode = this._currentModeObservable.get(); const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; @@ -429,6 +444,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, + permissionLevel: this._currentPermissionLevel.get(), }; } @@ -529,6 +545,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); this._currentModeObservable = observableValue('currentMode', this.options.defaultMode ?? ChatMode.Agent); + this._currentPermissionLevel = observableValue('permissionLevel', ChatPermissionLevel.Default); this._register(this.editorService.onDidActiveEditorChange(() => { this._indexOfLastOpenedContext = -1; this.refreshChatSessionPickers(); @@ -580,6 +597,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService); this.chatModeNameKey = ChatContextKeys.chatModeName.bindTo(contextKeyService); this.chatModelIdKey = ChatContextKeys.chatModelId.bindTo(contextKeyService); + this.permissionLevelKey = ChatContextKeys.chatPermissionLevel.bindTo(contextKeyService); this.withinEditSessionKey = ChatContextKeys.withinEditSessionDiff.bindTo(contextKeyService); this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); this.chatSessionHasOptions = ChatContextKeys.chatSessionHasModels.bindTo(contextKeyService); @@ -790,6 +808,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.modeWidget?.show(); } + public openPermissionPicker(): void { + this.permissionWidget?.show(); + } + + public setPermissionLevel(level: ChatPermissionLevel): void { + this._currentPermissionLevel.set(level, undefined); + this.permissionLevelKey.set(level); + this.permissionWidget?.refresh(); + } + public openSessionTargetPicker(): void { this.sessionTargetWidget?.show(); } @@ -876,6 +904,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.selectedToolsModel.resetSessionEnablementState(); this._chatSessionIsEmpty = chatSessionIsEmpty; + // Reset permission level to default on new sessions + if (chatSessionIsEmpty) { + this._currentPermissionLevel.set(ChatPermissionLevel.Default, undefined); + this.permissionLevelKey.set(ChatPermissionLevel.Default); + this.permissionWidget?.refresh(); + } + // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. if (chatSessionIsEmpty) { this._setEmptyModelState(); @@ -1931,11 +1966,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ dom.h('.chat-editor-container@editorContainer'), - dom.h('.chat-input-toolbars@inputToolbars', [ - dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), - ]), + dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), + dom.h('.chat-secondary-toolbar@secondaryToolbar', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + ]), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attached-context@attachedContextContainer'), ]), @@ -1956,11 +1992,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-attached-context@attachedContextContainer'), ]), dom.h('.chat-editor-container@editorContainer'), - dom.h('.chat-input-toolbars@inputToolbars', [ - dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), - ]), + dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), + dom.h('.chat-secondary-toolbar@secondaryToolbar', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + ]), ]); } this.container = elements.root; @@ -1981,6 +2018,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachmentsContainer = elements.attachmentsContainer; this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; + this.secondaryToolbarContainer = elements.secondaryToolbar; + if (this.options.isSessionsWindow) { + this.secondaryToolbarContainer.style.display = 'none'; + } this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatGettingStartedTipContainer = elements.chatGettingStartedTipContainer; @@ -1989,6 +2030,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; + if (this.options.isSessionsWindow) { + toolbarsContainer.prepend(this.contextUsageWidgetContainer); + } + // Context usage widget — will be positioned in the toolbar after toolbars are created this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget)); this.contextUsageWidgetContainer.appendChild(this.contextUsageWidget.domNode); @@ -2240,7 +2285,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // eslint-disable-next-line no-restricted-syntax const container = toolbarElement.querySelector('.chat-sessionPicker-container'); this.chatSessionPickerContainer = container as HTMLElement | undefined; - if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { this._toolbarRelayoutScheduler.schedule(); } @@ -2273,6 +2317,62 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge toolbarSide.context = { widget } satisfies IChatExecuteActionContext; } + // Secondary toolbar (permissions) — below the input box + this.secondaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.secondaryToolbarContainer, MenuId.ChatInputSecondary, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { shouldForwardArgs: true }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + hoverDelegate, + actionViewItemProvider: (action, options) => { + if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { + const getActiveSessionType = () => { + const sessionResource = this._widget?.viewModel?.sessionResource; + return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; + }; + const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { + getActiveSessionProvider: () => { + return getActiveSessionType(); + }, + getPendingDelegationTarget: () => { + return this._pendingDelegationTarget; + }, + setPendingDelegationTarget: (provider: AgentSessionProviders) => { + const isActive = getActiveSessionType() === provider; + this._pendingDelegationTarget = isActive ? undefined : provider; + this.updateWidgetLockStateFromSessionType(provider); + this.updateAgentSessionTypeContextKey(); + this.refreshChatSessionPickers(); + }, + }; + const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; + const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); + } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { + return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); + } else { + const empty = new BaseActionViewItem(undefined, action); + if (empty.element) { + empty.element.style.display = 'none'; + } + return empty; + } + } else if (action.id === OpenPermissionPickerAction.ID && action instanceof MenuItemAction) { + const delegate: IPermissionPickerDelegate = { + currentPermissionLevel: this._currentPermissionLevel, + setPermissionLevel: (level: ChatPermissionLevel) => { + this._currentPermissionLevel.set(level, undefined); + this.permissionLevelKey.set(level); + }, + }; + return this.permissionWidget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, pickerOptions); + } + return undefined; + } + })); + this.secondaryToolbar.getElement().classList.add('chat-secondary-input-toolbar'); + this.secondaryToolbar.context = { widget } satisfies IChatExecuteActionContext; + let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { inputModel = this.modelService.createModel('', null, this.inputUri, true); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts new file mode 100644 index 00000000000..5f0cc0988d2 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { localize } from '../../../../../../nls.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { ChatConfiguration, ChatPermissionLevel } from '../../../common/constants.js'; +import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; + +export interface IPermissionPickerDelegate { + readonly currentPermissionLevel: IObservable; + readonly setPermissionLevel: (level: ChatPermissionLevel) => void; +} + +export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { + constructor( + action: MenuItemAction, + private readonly delegate: IPermissionPickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, + ) { + const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; + const isAutopilotEnabled = () => configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const currentLevel = delegate.currentPermissionLevel.get(); + const policyRestricted = isAutoApprovePolicyRestricted(); + const actions: IActionWidgetDropdownAction[] = [ + { + ...action, + id: 'chat.permissions.default', + label: localize('permissions.default', "Default Approvals"), + icon: ThemeIcon.fromId(Codicon.shield.id), + checked: currentLevel === ChatPermissionLevel.Default, + tooltip: '', + hover: { + content: localize('permissions.default.description', "Use configured approval settings"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.Default); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + { + ...action, + id: 'chat.permissions.autoApprove', + label: localize('permissions.autoApprove', "Bypass Approvals"), + icon: ThemeIcon.fromId(Codicon.warning.id), + checked: currentLevel === ChatPermissionLevel.AutoApprove, + enabled: !policyRestricted, + tooltip: policyRestricted ? localize('permissions.autoApprove.policyDisabled', "Disabled by enterprise policy") : '', + hover: { + content: policyRestricted + ? localize('permissions.autoApprove.policyDescription', "Disabled by enterprise policy") + : localize('permissions.autoApprove.description', "Auto-approve all tool calls and retry on errors"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + ]; + if (isAutopilotEnabled()) { + actions.push({ + ...action, + id: 'chat.permissions.autopilot', + label: localize('permissions.autopilot', "Autopilot (Preview)"), + icon: ThemeIcon.fromId(Codicon.rocket.id), + checked: currentLevel === ChatPermissionLevel.Autopilot, + enabled: !policyRestricted, + tooltip: policyRestricted ? localize('permissions.autopilot.policyDisabled', "Disabled by enterprise policy") : '', + hover: { + content: policyRestricted + ? localize('permissions.autopilot.policyDescription', "Disabled by enterprise policy") + : localize('permissions.autopilot.description', "Auto-approve all tool calls and continue until the task is done"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.Autopilot); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction); + } + return actions; + } + }; + + super(action, { + actionProvider, + reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, + }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + + const level = this.delegate.currentPermissionLevel.get(); + let icon: ThemeIcon; + let label: string; + switch (level) { + case ChatPermissionLevel.Autopilot: + icon = Codicon.rocket; + label = localize('permissions.autopilot.label', "Autopilot (Preview)"); + break; + case ChatPermissionLevel.AutoApprove: + icon = Codicon.warning; + label = localize('permissions.autoApprove.label', "Bypass Approvals"); + break; + default: + icon = Codicon.shield; + label = localize('permissions.default.label', "Default Approvals"); + break; + } + + const labelElements = []; + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + element.classList.toggle('warning', level === ChatPermissionLevel.Autopilot); + element.classList.toggle('info', level === ChatPermissionLevel.AutoApprove); + return null; + } + + public refresh(): void { + if (this.element) { + this.renderLabel(this.element); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 86c54b7bd96..08572fadc85 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -822,11 +822,18 @@ have to be updated for changes to the rules above, or to support more deeply nes overflow: hidden; } -/* Context usage widget container - positioned in the bottom toolbar */ -.interactive-session .chat-input-toolbars .chat-context-usage-container { +/* Context usage widget container - positioned in the secondary toolbar below input */ +.interactive-session .chat-input-toolbars .chat-context-usage-container, +.interactive-session .chat-secondary-toolbar .chat-context-usage-container { display: flex; align-items: center; flex-shrink: 0; + margin-left: auto; + order: 1; +} + +/* When context usage is inside the toolbars (compact mode), keep the ordering */ +.interactive-session .chat-input-toolbars .chat-context-usage-container { order: 1; } @@ -1330,6 +1337,85 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-top: 4px; } +/* Secondary toolbar below the input box */ +.interactive-session .chat-secondary-toolbar { + display: flex; + align-items: center; + gap: 6px; + padding: 2px 5px 2px 6px; +} + +.interactive-session .chat-secondary-toolbar:empty { + display: none; +} + +.interactive-session .chat-secondary-toolbar > .chat-secondary-input-toolbar { + overflow: hidden; + min-width: 0px; + color: var(--vscode-icon-foreground); + + .monaco-action-bar .action-item .codicon { + color: var(--vscode-icon-foreground); + } + + .chat-input-picker-item { + min-width: 0px; + overflow: hidden; + + .action-label { + min-width: 0px; + overflow: hidden; + position: relative; + + .chat-input-picker-label { + overflow: hidden; + text-overflow: ellipsis; + } + + span + .chat-input-picker-label { + margin-left: 2px; + } + + .codicon { + font-size: 12px; + } + } + + .codicon { + flex-shrink: 0; + } + } +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label { + height: 16px; + padding: 3px 0px 3px 6px; + display: flex; + align-items: center; + color: var(--vscode-icon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.warning { + color: var(--vscode-problemsWarningIcon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.warning .codicon { + color: var(--vscode-problemsWarningIcon-foreground) !important; +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.info { + color: var(--vscode-problemsInfoIcon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.info .codicon { + color: var(--vscode-problemsInfoIcon-foreground) !important; +} + +.monaco-workbench .interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label .codicon-chevron-down { + font-size: 12px; + margin-left: 2px; +} + .interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child { margin-right: auto; } @@ -1669,7 +1755,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .interactive-input-part { margin: 0px 12px; - padding: 4px 0 12px 0px; + padding: 4px 0 6px 0px; display: flex; flex-direction: column; gap: 4px; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index b20f39c7cf6..e051fbcfb3f 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -92,6 +92,7 @@ export class ChatContextUsageWidget extends Disposable { readonly domNode: HTMLElement; private readonly progressIndicator: CircularProgressIndicator; + private readonly percentageLabel: HTMLElement; private readonly _isVisible = observableValue(this, false); get isVisible(): IObservable { return this._isVisible; } @@ -130,6 +131,9 @@ export class ChatContextUsageWidget extends Disposable { this.progressIndicator = new CircularProgressIndicator(); iconContainer.appendChild(this.progressIndicator.domNode); + // Percentage label (visible on hover/focus) + this.percentageLabel = this.domNode.appendChild($('.percentage-label')); + // Track context usage opened state this._contextUsageOpenedKey = ChatContextKeys.contextUsageHasBeenOpened.bindTo(this.contextKeyService); @@ -286,6 +290,11 @@ export class ChatContextUsageWidget extends Disposable { // Update pie chart progress this.progressIndicator.setProgress(percentage); + // Update percentage label and aria-label + const roundedPercentage = Math.round(percentage); + this.percentageLabel.textContent = `${roundedPercentage}%`; + this.domNode.setAttribute('aria-label', localize('contextUsagePercentageLabel', "Context window usage: {0}%", roundedPercentage)); + // Update color based on usage level this.domNode.classList.remove('warning', 'error'); if (percentage >= 90) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 905351e3adf..ab722bf9ad4 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -540,6 +540,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { supportsChangingModes: true, dndContainer: parent, inputEditorMinLines: this.workbenchEnvironmentService.isSessionsWindow ? 2 : undefined, + isSessionsWindow: this.workbenchEnvironmentService.isSessionsWindow, }, { listForeground: SIDE_BAR_FOREGROUND, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css index c3abb1332f0..e09c5e1aa23 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css @@ -6,9 +6,7 @@ .chat-context-usage-widget { display: flex; align-items: center; - justify-content: center; - height: 22px; - width: 22px; + gap: 4px; flex-shrink: 0; cursor: pointer; padding: 3px; @@ -55,7 +53,7 @@ .chat-context-usage-widget .progress-arc { fill: none; - stroke: var(--vscode-descriptionForeground); + stroke: var(--vscode-icon-foreground); stroke-width: 4; stroke-linecap: round; transform: rotate(-90deg); @@ -70,3 +68,20 @@ .chat-context-usage-widget.error .progress-arc { stroke: var(--vscode-editorError-foreground); } + +.chat-context-usage-widget .percentage-label { + font-size: 11px; + line-height: 1; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + max-width: 0; + opacity: 0; + overflow: hidden; + transition: max-width 0.1s ease-out, opacity 0.1s ease-out; +} + +.chat-context-usage-widget:hover .percentage-label, +.chat-context-usage-widget:focus .percentage-label { + max-width: 4em; + opacity: 1; +} diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 926ba9d9f97..cce18bceca5 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -9,7 +9,7 @@ import { IsWebContext } from '../../../../../platform/contextkey/common/contextk import { RemoteNameContext } from '../../../../common/contextkeys.js'; import { ViewContainerLocation } from '../../../../common/views.js'; import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; -import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; export namespace ChatContextKeys { export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); @@ -46,6 +46,7 @@ export namespace ChatContextKeys { export const multipleChatTips = new RawContextKey('multipleChatTips', false, { type: 'boolean', description: localize('multipleChatTips', "True when there are multiple chat tips available.") }); export const inChatTerminalToolOutput = new RawContextKey('inChatTerminalToolOutput', false, { type: 'boolean', description: localize('inChatTerminalToolOutput', "True when focus is in the chat terminal output region.") }); export const chatModeKind = new RawContextKey('chatAgentKind', ChatModeKind.Ask, { type: 'string', description: localize('agentKind', "The 'kind' of the current agent.") }); + export const chatPermissionLevel = new RawContextKey('chatPermissionLevel', ChatPermissionLevel.Default, { type: 'string', description: localize('chatPermissionLevel', "The current permission level for tool auto-approval.") }); export const chatModeName = new RawContextKey('chatModeName', '', { type: 'string', description: localize('chatModeName', "The name of the current chat mode (e.g. 'Plan' for custom modes).") }); export const chatModelId = new RawContextKey('chatModelId', '', { type: 'string', description: localize('chatModelId', "The short id of the currently selected chat model (for example 'gpt-4.1').") }); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 0d1bb0635de..498a1ef40da 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1024,6 +1024,7 @@ export class ChatService extends Disposable implements IChatService { userSelectedModelId: options?.userSelectedModelId, userSelectedTools: options?.userSelectedTools?.get(), modeInstructions: options?.modeInfo?.modeInstructions, + permissionLevel: options?.modeInfo?.permissionLevel, editedFileEvents: request.editedFileEvents, hooks: collectedHooks, hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0), @@ -1180,6 +1181,7 @@ export class ChatService extends Disposable implements IChatService { shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested; request.response?.complete(); + if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request!, followups); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 9ed80f0451e..1cc89241a27 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -54,6 +54,7 @@ export enum ChatConfiguration { ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', GrowthNotificationEnabled = 'chat.growthNotification.enabled', ChatCustomizationMenuEnabled = 'chat.customizationsMenu.enabled', + AutopilotEnabled = 'chat.autopilot.enabled', } /** @@ -76,6 +77,26 @@ export function validateChatMode(mode: unknown): ChatModeKind | undefined { } } +/** + * The permission level controlling tool auto-approval behavior. + */ +export enum ChatPermissionLevel { + /** Use existing auto-approve settings */ + Default = 'default', + /** Auto-approve all tool calls, auto-retry on error */ + AutoApprove = 'autoApprove', + /** Everything AutoApprove does plus an internal stop hook that continues until the task is done */ + Autopilot = 'autopilot' +} + +/** + * Returns true if the permission level enables auto-approval of all tool calls. + * Both {@link ChatPermissionLevel.AutoApprove} and {@link ChatPermissionLevel.Autopilot} enable auto-approval. + */ +export function isAutoApproveLevel(level: ChatPermissionLevel | undefined): boolean { + return level === ChatPermissionLevel.AutoApprove || level === ChatPermissionLevel.Autopilot; +} + export function isChatMode(mode: unknown): mode is ChatModeKind { return !!validateChatMode(mode); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 3fb3a88c82f..98098bb9376 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -30,7 +30,7 @@ import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCo import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js'; import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; -import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js'; import { ToolDataSource, IToolData } from '../tools/languageModelToolsService.js'; import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js'; @@ -314,6 +314,7 @@ export interface IChatRequestModeInfo { modeInstructions: IChatRequestModeInstructions | undefined; modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; + permissionLevel?: ChatPermissionLevel; } export interface IChatRequestModeInstructions { diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 817ebbb18b8..e8f4fbe4a03 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -24,7 +24,7 @@ import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRe import { ChatRequestHooks } from '../promptSyntax/hookSchema.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from '../chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ILanguageModelsService } from '../languageModels.js'; //#region agent service, commands etc @@ -158,6 +158,12 @@ export interface IChatAgentRequest { * Whether any hooks are enabled for this request. */ hasHooksEnabled?: boolean; + /** + * The permission level for tool auto-approval in this request. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. + */ + permissionLevel?: ChatPermissionLevel; /** * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index bf7dd424e25..7f7bb156f07 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -13,6 +13,7 @@ import { localize } from '../../../../../../nls.js'; import { IChatQuestion, IChatService } from '../../chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../model/chatProgressTypes/chatQuestionCarouselData.js'; import { IChatRequestModel } from '../../model/chatModel.js'; +import { ChatPermissionLevel } from '../../constants.js'; import { StopWatch } from '../../../../../../base/common/stopwatch.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; @@ -22,6 +23,12 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { raceCancellation } from '../../../../../../base/common/async.js'; import { URI } from '../../../../../../base/common/uri.js'; +/** + * Response returned to the model when the user is not available (autopilot mode). + */ +export const AUTOPILOT_ASK_USER_RESPONSE = + 'The user is not available to respond and will review your work later. Work autonomously and make good decisions.'; + // Use a distinct id to avoid clashing with extension-provided tools export const AskQuestionsToolId = 'vscode_askQuestions'; @@ -187,6 +194,17 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { return this.createSkippedResult(questions); } + // In autopilot mode, the user is not available — auto-respond instead of blocking. + // Still append a completed carousel so the user can see the auto-selected answers. + if (request.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + this.logService.info('[AskQuestionsTool] Autopilot mode: auto-responding to questions'); + const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions); + carousel.data = this.buildAutopilotCarouselAnswers(questions, carousel, idToHeaderMap); + carousel.isUsed = true; + this.chatService.appendProgress(request, carousel); + return this.createAutopilotResult(questions); + } + const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions); this.chatService.appendProgress(request, carousel); @@ -477,6 +495,63 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { }; } + private createAutopilotResult(questions: IQuestion[]): IToolResult { + const answers: Record = {}; + for (const question of questions) { + // Pick the recommended option if available, otherwise pick the first option + const recommended = question.options?.find(opt => opt.recommended); + const firstOption = question.options?.[0]; + const selected = recommended?.label ?? firstOption?.label; + answers[question.header] = { + selected: selected ? [selected] : [], + freeText: selected ? null : AUTOPILOT_ASK_USER_RESPONSE, + skipped: false, + }; + } + return { + content: [{ kind: 'text', value: JSON.stringify({ answers } satisfies IAnswerResult) }] + }; + } + + /** + * Build carousel answer data keyed by carousel question IDs for rendering + * the completed summary in the UI during autopilot mode. + */ + private buildAutopilotCarouselAnswers(questions: IQuestion[], carousel: ChatQuestionCarouselData, idToHeaderMap: Map): Record { + const data: Record = {}; + // Build reverse map: original header -> internal carousel question ID + const headerToIdMap = new Map(); + for (const [internalId, originalHeader] of idToHeaderMap) { + headerToIdMap.set(originalHeader, internalId); + } + + for (const question of questions) { + const internalId = headerToIdMap.get(question.header); + if (!internalId) { + continue; + } + + const chatQuestion = carousel.questions.find(q => q.id === internalId); + if (!chatQuestion) { + continue; + } + + const recommended = question.options?.find(opt => opt.recommended); + const firstOption = question.options?.[0]; + const selectedLabel = recommended?.label ?? firstOption?.label; + + if (chatQuestion.type === 'text' || !selectedLabel) { + data[internalId] = AUTOPILOT_ASK_USER_RESPONSE; + } else if (chatQuestion.type === 'multiSelect') { + data[internalId] = { selectedValues: [selectedLabel] }; + } else { + data[internalId] = { selectedValue: selectedLabel }; + } + } + + return data; + } + private sendTelemetry(requestId: string | undefined, questionCount: number, answeredCount: number, skippedCount: number, freeTextCount: number, recommendedAvailableCount: number, recommendedSelectedCount: number, duration: number): void { this.telemetryService.publicLog2('askQuestionsToolInvoked', { requestId, diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts new file mode 100644 index 00000000000..74879d30d21 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress, CountTokensCallback } from '../languageModelToolsService.js'; + +export const TaskCompleteToolId = 'task_complete'; + +/** + * Message sent to the agent when the session goes idle without task completion. + */ +export const AUTOPILOT_CONTINUATION_MESSAGE = + 'You have not yet marked the task as complete using the task_complete tool. ' + + 'You MUST call task_complete when done — whether the task involved code changes, answering a question, or any other interaction.\n\n' + + 'Do NOT repeat or restate your previous response. Pick up where you left off.\n\n' + + 'If you were planning, stop planning and start implementing. ' + + 'You are not done until you have fully completed the task.\n\n' + + 'IMPORTANT: Do NOT call task_complete if:\n' + + '- You have open questions or ambiguities — make good decisions and keep working\n' + + '- You encountered an error — try to resolve it or find an alternative approach\n' + + '- There are remaining steps — complete them first\n\n' + + 'Keep working autonomously until the task is truly finished, then call task_complete.'; + +export const TaskCompleteToolData: IToolData = { + id: TaskCompleteToolId, + displayName: 'Task Complete', + modelDescription: + 'Signal that the user\'s task is fully done. You MUST call this tool when your work is complete — ' + + 'whether you made code changes, answered a question, or completed any other kind of task. ' + + 'Provide a brief summary of what was accomplished. If the summary is trivial (e.g. answering a question), omit it. ' + + 'Do not restate the summary in your message text — it is shown to the user directly.\n\n' + + 'When to call:\n' + + '- After answering the user\'s question or completing a conversational request\n' + + '- After you have completed ALL requested changes\n' + + '- After verifying results: tests pass, terminal commands succeeded, tool calls returned expected output\n\n' + + 'When NOT to call:\n' + + '- If a terminal command failed or produced unexpected output\n' + + '- If an MCP or external tool call returned an error\n' + + '- If you encountered errors you have not resolved\n' + + '- If there are remaining steps to complete\n' + + '- If you have not verified your changes work', + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + summary: { + type: 'string', + description: 'Brief summary of what was accomplished. Omit for trivial interactions.', + }, + }, + }, +}; + +export class TaskCompleteTool implements IToolImpl { + async prepareToolInvocation(_context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + return { + presentation: ToolInvocationPresentation.Hidden, + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const params = invocation.parameters as { summary?: string }; + const summary = params?.summary ?? 'All done!'; + return { + content: [{ + kind: 'text', + value: summary, + }], + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 619e63406dd..74f61e8b613 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -13,6 +13,7 @@ import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; import { ResolveDebugEventDetailsTool, ResolveDebugEventDetailsToolData } from './resolveDebugEventDetailsTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; +import { TaskCompleteTool, TaskCompleteToolData } from './taskCompleteTool.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -35,15 +36,19 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool)); this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); - // Register the confirmation tool const confirmationTool = instantiationService.createInstance(ConfirmationTool); this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); + + const taskCompleteTool = instantiationService.createInstance(TaskCompleteTool); + this._register(toolsService.registerTool(TaskCompleteToolData, taskCompleteTool)); + const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData)); + const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); let runSubagentRegistration: IDisposable | undefined; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 16c8ff488a0..839499ef55f 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 14 +// version: 15 declare module 'vscode' { @@ -116,6 +116,13 @@ declare module 'vscode' { */ readonly parentRequestId?: string; + /** + * The permission level for tool auto-approval in this request. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. + */ + readonly permissionLevel?: string; + /** * Whether any hooks are enabled for this request. */ From 9495e313c0eaec600ce20cd800648c99fabb52fd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 20:45:50 +0100 Subject: [PATCH 180/448] sessions - clarify instructions around compiling and testing (#299255) --- .github/copilot-instructions.md | 2 +- .github/skills/sessions/SKILL.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index be8c26eeadd..8d56465c45a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,7 +58,7 @@ MANDATORY: Always check for compilation errors before running any tests or valid ### TypeScript compilation steps - If the `#runTasks/getTaskOutput` tool is available, check the `VS Code - Build` watch task output for compilation errors. This task runs `Core - Build` and `Ext - Build` to incrementally compile VS Code TypeScript sources and built-in extensions. Start the task if it's not already running in the background. - If the tool is not available (e.g. in CLI environments) and you only changed code under `src/`, run `npm run compile-check-ts-native` after making changes to type-check the main VS Code sources (it validates `./src/tsconfig.json`). -- If you changed built-in extensions under `extensions/` and the tool is not available, run the corresponding gulp task `gulp compile-extensions` instead so that TypeScript errors in extensions are also reported. +- If you changed built-in extensions under `extensions/` and the tool is not available, run the corresponding gulp task `npm run gulp compile-extensions` instead so that TypeScript errors in extensions are also reported. - For TypeScript changes in the `build` folder, you can simply run `npm run typecheck` in the `build` folder. ### TypeScript validation steps diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index d82e03178ff..e39957e5f66 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -272,7 +272,9 @@ Views and contributions that should only appear in the agent sessions window (no 1. Run `npm run compile-check-ts-native` to run a repo-wide TypeScript compilation check (including `src/vs/sessions/`). This is a fast way to catch TypeScript errors introduced by your changes. 2. Run `npm run valid-layers-check` to verify layering rules are not violated. -3. Run tests under `src/vs/sessions/test/` to confirm nothing is broken. +3. Use `scripts/test.sh` (or `scripts\test.bat` on Windows) for unit tests (add `--grep ` to filter tests) + +**Important** do not run `tsc` to check for TypeScript errors always use above methods to validate TypeScript changes in `src/vs/**`. ### 10.3 Layout Changes From fdceca105cf1b9583c869e67f70197220017dd3a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 21:20:48 +0100 Subject: [PATCH 181/448] Scope EditorTabsVisibleContext to editor parts for correct action resolution (#299261) feat - add EditorTabsVisibleContext handling --- src/vs/workbench/browser/parts/editor/editorPart.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index ca2d962ea0d..c172d537754 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -34,7 +34,7 @@ import { IBoundarySashes } from '../../../../base/browser/ui/sash/sash.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext } from '../../../common/contextkeys.js'; +import { EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext, EditorTabsVisibleContext } from '../../../common/contextkeys.js'; import { mainWindow } from '../../../../base/browser/window.js'; export interface IEditorPartUIState { @@ -1039,6 +1039,7 @@ export class EditorPart extends Part implements IEditorPart, protected handleContextKeys(): void { const multipleEditorGroupsContext = EditorPartMultipleEditorGroupsContext.bindTo(this.scopedContextKeyService); const maximizedEditorGroupContext = EditorPartMaximizedEditorGroupContext.bindTo(this.scopedContextKeyService); + const editorTabsVisibleContext = EditorTabsVisibleContext.bindTo(this.scopedContextKeyService); const updateContextKeys = () => { const groupCount = this.count; @@ -1055,11 +1056,17 @@ export class EditorPart extends Part implements IEditorPart, } }; + const updateEditorTabsVisibleContext = () => { + editorTabsVisibleContext.set(this.partOptions.showTabs === 'multiple'); + }; + updateContextKeys(); + updateEditorTabsVisibleContext(); this._register(this.onDidAddGroup(() => updateContextKeys())); this._register(this.onDidRemoveGroup(() => updateContextKeys())); this._register(this.onDidChangeGroupMaximized(() => updateContextKeys())); + this._register(this.onDidChangeEditorPartOptions(() => updateEditorTabsVisibleContext())); } private setupDragAndDropSupport(parent: HTMLElement, container: HTMLElement): void { From b3ad9079ba93efcd470e89f022d51b1d8b96bce9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 12:34:53 -0800 Subject: [PATCH 182/448] plugins: add Plugins section to Chat Customizations (#299265) * plugins: show in customizations view * plugins: address PR review comments --- .../browser/aiCustomizationOverviewView.ts | 15 +- .../aiCustomizationWorkspaceService.ts | 7 +- .../browser/aiCustomizationShortcutsWidget.ts | 4 +- .../sessions/browser/customizationCounts.ts | 5 +- .../customizationsToolbar.contribution.ts | 19 +- .../aiCustomizationShortcutsWidget.fixture.ts | 5 + .../aiCustomizationManagementEditor.ts | 125 ++- .../aiCustomizationWorkspaceService.ts | 1 + .../media/aiCustomizationManagement.css | 13 + .../aiCustomization/pluginListWidget.ts | 859 ++++++++++++++++++ .../common/aiCustomizationWorkspaceService.ts | 1 + 11 files changed, 1042 insertions(+), 12 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index d644c5122b5..30d034ce089 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -24,11 +24,12 @@ import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyn import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; -import { agentIcon, instructionsIcon, mcpServerIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; const $ = DOM.$; @@ -69,6 +70,7 @@ export class AICustomizationOverviewView extends ViewPane { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IMcpService private readonly mcpService: IMcpService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -79,6 +81,7 @@ export class AICustomizationOverviewView extends ViewPane { { id: AICustomizationManagementSection.Instructions, label: localize('instructions', "Instructions"), icon: instructionsIcon, count: 0 }, { id: AICustomizationManagementSection.Prompts, label: localize('prompts', "Prompts"), icon: promptIcon, count: 0 }, { id: AICustomizationManagementSection.McpServers, label: localize('mcpServers', "MCP Servers"), icon: mcpServerIcon, count: 0 }, + { id: AICustomizationManagementSection.Plugins, label: localize('plugins', "Plugins"), icon: pluginIcon, count: 0 }, ); // Listen to changes @@ -186,6 +189,16 @@ export class AICustomizationOverviewView extends ViewPane { })); } + // Update plugin count reactively + const pluginSection = this.sections.find(s => s.id === AICustomizationManagementSection.Plugins); + if (pluginSection) { + this._register(autorun(reader => { + const plugins = this.agentPluginService.allPlugins.read(reader); + pluginSection.count = plugins.length; + this.updateCountElements(); + })); + } + this.updateCountElements(); } diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 47292441bdb..c055e550706 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -69,7 +69,7 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization joinPath(userHome, '.agents'), ]; this._cliUserFilter = { - sources: [PromptsStorage.local, PromptsStorage.user], + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin], includedUserFileRoots: this._cliUserRoots, }; @@ -113,14 +113,15 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization AICustomizationManagementSection.Prompts, AICustomizationManagementSection.Hooks, AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Plugins, ]; private static readonly _hooksFilter: IStorageSourceFilter = { - sources: [PromptsStorage.local], + sources: [PromptsStorage.local, PromptsStorage.plugin], }; private static readonly _allUserRootsFilter: IStorageSourceFilter = { - sources: [PromptsStorage.local, PromptsStorage.user], + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin], }; getStorageSourceFilter(type: PromptsType): IStorageSourceFilter { diff --git a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts index c4e89d70e17..58c814528b3 100644 --- a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts @@ -22,6 +22,7 @@ import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.j import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { Menus } from '../../../browser/menus.js'; import { getCustomizationTotalCount } from './customizationCounts.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; const $ = DOM.$; @@ -42,6 +43,7 @@ export class AICustomizationShortcutsWidget extends Disposable { @IMcpService private readonly mcpService: IMcpService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, ) { super(); @@ -93,7 +95,7 @@ export class AICustomizationShortcutsWidget extends Disposable { let updateCountRequestId = 0; const updateHeaderTotalCount = async () => { const requestId = ++updateCountRequestId; - const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService); + const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService, this.agentPluginService); if (requestId !== updateCountRequestId) { return; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index 682c73edc6b..ad30f5c04ad 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -13,6 +13,7 @@ import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/c import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { parseHooksFromFile } from '../../../../workbench/contrib/chat/common/promptSyntax/hookCompatibility.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; import { parse as parseJSONC } from '../../../../base/common/jsonc.js'; export interface ISourceCounts { @@ -136,6 +137,7 @@ export async function getCustomizationTotalCount( mcpService: IMcpService, workspaceService: IAICustomizationWorkspaceService, workspaceContextService: IWorkspaceContextService, + agentPluginService?: IAgentPluginService, ): Promise { const types: PromptsType[] = [PromptsType.agent, PromptsType.skill, PromptsType.instructions, PromptsType.prompt, PromptsType.hook]; const results = await Promise.all(types.map(type => { @@ -143,5 +145,6 @@ export async function getCustomizationTotalCount( return getSourceCounts(promptsService, type, filter, workspaceContextService, workspaceService) .then(counts => getSourceCountsTotal(counts, filter)); })); - return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length; + const pluginCount = agentPluginService?.allPlugins.get().length ?? 0; + return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length + pluginCount; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index ba97537a9b8..350a9fa19b8 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -20,7 +20,7 @@ import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyn import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { Menus } from '../../../browser/menus.js'; -import { agentIcon, instructionsIcon, mcpServerIcon, promptIcon, skillIcon, hookIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon, hookIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../base/common/actions.js'; import { $, append } from '../../../../base/browser/dom.js'; @@ -33,6 +33,7 @@ import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultS import { getSourceCounts, getSourceCountsTotal } from './customizationCounts.js'; import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; export interface ICustomizationItemConfig { readonly id: string; @@ -41,6 +42,7 @@ export interface ICustomizationItemConfig { readonly section: AICustomizationManagementSection; readonly promptType?: PromptsType; readonly isMcp?: boolean; + readonly isPlugins?: boolean; } export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ @@ -86,6 +88,13 @@ export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ section: AICustomizationManagementSection.McpServers, isMcp: true, }, + { + id: 'sessions.customization.plugins', + label: localize('plugins', "Plugins"), + icon: pluginIcon, + section: AICustomizationManagementSection.Plugins, + isPlugins: true, + }, ]; /** @@ -109,6 +118,7 @@ export class CustomizationLinkViewItem extends ActionViewItem { @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, @IAICustomizationWorkspaceService private readonly _workspaceService: IAICustomizationWorkspaceService, @IFileService private readonly _fileService: IFileService, + @IAgentPluginService private readonly _agentPluginService: IAgentPluginService, ) { super(undefined, action, { ...options, icon: false, label: false }); this._viewItemDisposables = this._register(new DisposableStore()); @@ -152,6 +162,10 @@ export class CustomizationLinkViewItem extends ActionViewItem { this._mcpService.servers.read(reader); this._updateCounts(); })); + this._viewItemDisposables.add(autorun(reader => { + this._agentPluginService.allPlugins.read(reader); + this._updateCounts(); + })); this._viewItemDisposables.add(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateCounts())); this._viewItemDisposables.add(autorun(reader => { this._activeSessionService.activeSession.read(reader); @@ -183,6 +197,9 @@ export class CustomizationLinkViewItem extends ActionViewItem { } else if (this._config.isMcp) { const total = this._mcpService.servers.get().length; this._renderTotalCount(this._countContainer, total); + } else if (this._config.isPlugins) { + const total = this._agentPluginService.allPlugins.get().length; + this._renderTotalCount(this._countContainer, total); } } diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts index 71d5b630d11..6fc39f22725 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts @@ -19,6 +19,7 @@ import { PromptsType } from '../../../../../workbench/contrib/chat/common/prompt import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpServer, IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAgentPluginService } from '../../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem } from '../../browser/customizationsToolbar.contribution.js'; @@ -191,6 +192,10 @@ function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: reg.defineInstance(IMcpService, createMockMcpService(options?.mcpServerCount ?? 0)); reg.defineInstance(IAICustomizationWorkspaceService, createMockWorkspaceService()); reg.defineInstance(IWorkspaceContextService, createMockWorkspaceContextService()); + reg.defineInstance(IAgentPluginService, new class extends mock() { + override readonly plugins = observableValue('mockPlugins', []); + override readonly allPlugins = observableValue('mockAllPlugins', []); + }()); // Additional services needed by CustomizationLinkViewItem reg.defineInstance(ILanguageModelsService, new class extends mock() { override readonly onDidChangeLanguageModels = Event.None; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index e3b8f65df50..6dced7449d2 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -33,6 +33,7 @@ import { PANEL_BORDER } from '../../../../common/theme.js'; import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js'; import { AICustomizationListWidget } from './aiCustomizationListWidget.js'; import { McpListWidget } from './mcpListWidget.js'; +import { PluginListWidget } from './pluginListWidget.js'; import { AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID, AI_CUSTOMIZATION_MANAGEMENT_SIDEBAR_WIDTH_KEY, @@ -45,7 +46,7 @@ import { SIDEBAR_MAX_WIDTH, CONTENT_MIN_WIDTH, } from './aiCustomizationManagement.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from './aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, pluginIcon } from './aiCustomizationIcons.js'; import { ChatModelsWidget } from '../chatManagement/chatModelsWidget.js'; import { PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; @@ -66,6 +67,9 @@ import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput. import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js'; +import { AgentPluginEditor } from '../agentPluginEditor/agentPluginEditor.js'; +import { AgentPluginEditorInput } from '../agentPluginEditor/agentPluginEditorInput.js'; +import { IAgentPluginItem } from '../agentPluginEditor/agentPluginItems.js'; const $ = DOM.$; @@ -137,9 +141,11 @@ export class AICustomizationManagementEditor extends EditorPane { private contentContainer!: HTMLElement; private listWidget!: AICustomizationListWidget; private mcpListWidget: McpListWidget | undefined; + private pluginListWidget: PluginListWidget | undefined; private modelsWidget: ChatModelsWidget | undefined; private promptsContentContainer!: HTMLElement; private mcpContentContainer: HTMLElement | undefined; + private pluginContentContainer: HTMLElement | undefined; private modelsContentContainer: HTMLElement | undefined; private modelsFooterElement: HTMLElement | undefined; @@ -153,13 +159,18 @@ export class AICustomizationManagementEditor extends EditorPane { private currentEditingUri: URI | undefined; private currentEditingProjectRoot: URI | undefined; private currentModelRef: IReference | undefined; - private viewMode: 'list' | 'editor' | 'mcpDetail' = 'list'; + private viewMode: 'list' | 'editor' | 'mcpDetail' | 'pluginDetail' = 'list'; // Embedded MCP server detail view private mcpDetailContainer: HTMLElement | undefined; private embeddedMcpEditor: McpServerEditor | undefined; private readonly mcpDetailDisposables = this._register(new DisposableStore()); + // Embedded plugin detail view + private pluginDetailContainer: HTMLElement | undefined; + private embeddedPluginEditor: AgentPluginEditor | undefined; + private readonly pluginDetailDisposables = this._register(new DisposableStore()); + private dimension: DOM.Dimension | undefined; private readonly sections: ISectionItem[] = []; private selectedSection: AICustomizationManagementSection = AICustomizationManagementSection.Agents; @@ -218,6 +229,7 @@ export class AICustomizationManagementEditor extends EditorPane { [AICustomizationManagementSection.Prompts]: { label: localize('prompts', "Prompts"), icon: promptIcon }, [AICustomizationManagementSection.Hooks]: { label: localize('hooks', "Hooks"), icon: hookIcon }, [AICustomizationManagementSection.McpServers]: { label: localize('mcpServers', "MCP Servers"), icon: Codicon.server }, + [AICustomizationManagementSection.Plugins]: { label: localize('plugins', "Plugins"), icon: pluginIcon }, [AICustomizationManagementSection.Models]: { label: localize('models', "Models"), icon: Codicon.vm }, }; for (const id of this.workspaceService.managementSections) { @@ -287,6 +299,7 @@ export class AICustomizationManagementEditor extends EditorPane { if (height !== undefined) { this.listWidget.layout(height - 16, width - 24); this.mcpListWidget?.layout(height - 16, width - 24); + this.pluginListWidget?.layout(height - 16, width - 24); const modelsFooterHeight = this.modelsFooterElement?.offsetHeight || 80; this.modelsWidget?.layout(height - 16 - modelsFooterHeight, width); if (this.viewMode === 'editor' && this.embeddedEditor) { @@ -298,6 +311,10 @@ export class AICustomizationManagementEditor extends EditorPane { const backHeaderHeight = 40; this.embeddedMcpEditor.layout(new DOM.Dimension(width, Math.max(0, height - backHeaderHeight))); } + if (this.viewMode === 'pluginDetail' && this.embeddedPluginEditor) { + const backHeaderHeight = 40; + this.embeddedPluginEditor.layout(new DOM.Dimension(width, Math.max(0, height - backHeaderHeight))); + } } }, }, Sizing.Distribute, undefined, true); @@ -483,6 +500,21 @@ export class AICustomizationManagementEditor extends EditorPane { })); } + // Container for Plugins content + if (hasSections.has(AICustomizationManagementSection.Plugins)) { + this.pluginContentContainer = DOM.append(contentInner, $('.plugin-content-container')); + this.pluginListWidget = this.editorDisposables.add(this.instantiationService.createInstance(PluginListWidget)); + this.pluginContentContainer.appendChild(this.pluginListWidget.element); + + // Embedded plugin detail view + this.pluginDetailContainer = DOM.append(contentInner, $('.plugin-detail-container')); + this.createEmbeddedPluginDetail(); + + this.editorDisposables.add(this.pluginListWidget.onDidSelectPlugin(item => { + this.showEmbeddedPluginDetail(item); + })); + } + // Embedded editor container this.editorContentContainer = DOM.append(contentInner, $('.editor-content-container')); this.createEmbeddedEditor(); @@ -515,6 +547,9 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + if (this.viewMode === 'pluginDetail') { + this.goBackFromPluginDetail(); + } this.selectedSection = section; this.sectionContextKey.set(section); @@ -534,20 +569,29 @@ export class AICustomizationManagementEditor extends EditorPane { private updateContentVisibility(): void { const isEditorMode = this.viewMode === 'editor'; const isMcpDetailMode = this.viewMode === 'mcpDetail'; + const isPluginDetailMode = this.viewMode === 'pluginDetail'; + const isDetailMode = isMcpDetailMode || isPluginDetailMode; const isPromptsSection = this.isPromptsSection(this.selectedSection); const isModelsSection = this.selectedSection === AICustomizationManagementSection.Models; const isMcpSection = this.selectedSection === AICustomizationManagementSection.McpServers; + const isPluginsSection = this.selectedSection === AICustomizationManagementSection.Plugins; - this.promptsContentContainer.style.display = !isEditorMode && !isMcpDetailMode && isPromptsSection ? '' : 'none'; + this.promptsContentContainer.style.display = !isEditorMode && !isDetailMode && isPromptsSection ? '' : 'none'; if (this.modelsContentContainer) { - this.modelsContentContainer.style.display = !isEditorMode && !isMcpDetailMode && isModelsSection ? '' : 'none'; + this.modelsContentContainer.style.display = !isEditorMode && !isDetailMode && isModelsSection ? '' : 'none'; } if (this.mcpContentContainer) { - this.mcpContentContainer.style.display = !isEditorMode && !isMcpDetailMode && isMcpSection ? '' : 'none'; + this.mcpContentContainer.style.display = !isEditorMode && !isDetailMode && isMcpSection ? '' : 'none'; } if (this.mcpDetailContainer) { this.mcpDetailContainer.style.display = isMcpDetailMode ? '' : 'none'; } + if (this.pluginContentContainer) { + this.pluginContentContainer.style.display = !isEditorMode && !isDetailMode && isPluginsSection ? '' : 'none'; + } + if (this.pluginDetailContainer) { + this.pluginDetailContainer.style.display = isPluginDetailMode ? '' : 'none'; + } if (this.editorContentContainer) { this.editorContentContainer.style.display = isEditorMode ? '' : 'none'; } @@ -654,6 +698,9 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + if (this.viewMode === 'pluginDetail') { + this.goBackFromPluginDetail(); + } // Clear transient folder override on close this.workspaceService.clearOverrideProjectRoot(); super.clearInput(); @@ -676,6 +723,8 @@ export class AICustomizationManagementEditor extends EditorPane { } if (this.selectedSection === AICustomizationManagementSection.McpServers) { this.mcpListWidget?.focusSearch(); + } else if (this.selectedSection === AICustomizationManagementSection.Plugins) { + this.pluginListWidget?.focusSearch(); } else if (this.selectedSection === AICustomizationManagementSection.Models) { this.modelsWidget?.focusSearch(); } else { @@ -698,6 +747,9 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + if (this.viewMode === 'pluginDetail') { + this.goBackFromPluginDetail(); + } this.selectedSection = sectionId; this.sectionContextKey.set(sectionId); this.storageService.store(AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, sectionId, StorageScope.PROFILE, StorageTarget.USER); @@ -916,4 +968,67 @@ export class AICustomizationManagementEditor extends EditorPane { } //#endregion + + //#region Embedded Plugin Detail + + private createEmbeddedPluginDetail(): void { + if (!this.pluginDetailContainer) { + return; + } + + // Back button header + const detailHeader = DOM.append(this.pluginDetailContainer, $('.editor-header')); + const backButton = DOM.append(detailHeader, $('button.editor-back-button')); + backButton.setAttribute('aria-label', localize('backToPluginList', "Back to plugins")); + const backIconEl = DOM.append(backButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}`)); + backIconEl.setAttribute('aria-hidden', 'true'); + this.editorDisposables.add(DOM.addDisposableListener(backButton, 'click', () => { + this.goBackFromPluginDetail(); + })); + + // Container for the plugin editor + const editorContainer = DOM.append(this.pluginDetailContainer, $('.plugin-detail-editor-container')); + + // Create the embedded plugin editor pane + this.embeddedPluginEditor = this.editorDisposables.add(this.instantiationService.createInstance(AgentPluginEditor, this.group)); + this.embeddedPluginEditor.create(editorContainer); + } + + private async showEmbeddedPluginDetail(item: IAgentPluginItem): Promise { + if (!this.embeddedPluginEditor) { + return; + } + + this.viewMode = 'pluginDetail'; + this.updateContentVisibility(); + + const input = new AgentPluginEditorInput(item); + this.pluginDetailDisposables.clear(); + this.pluginDetailDisposables.add(input); + + try { + await this.embeddedPluginEditor.setInput(input, undefined, {}, CancellationToken.None); + } catch { + this.goBackFromPluginDetail(); + return; + } + + if (this.dimension) { + this.layout(this.dimension); + } + } + + private goBackFromPluginDetail(): void { + this.pluginDetailDisposables.clear(); + this.embeddedPluginEditor?.clearInput(); + this.viewMode = 'list'; + this.updateContentVisibility(); + + if (this.dimension) { + this.layout(this.dimension); + } + this.pluginListWidget?.focusSearch(); + } + + //#endregion } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 3ebfcea382f..dea000c2086 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -53,6 +53,7 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic AICustomizationManagementSection.Prompts, AICustomizationManagementSection.Hooks, AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Plugins, ]; private static readonly _defaultFilter: IStorageSourceFilter = { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index dd08e6fb380..d9781fd7664 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -556,6 +556,7 @@ /* Content container visibility */ .ai-customization-management-editor .prompts-content-container, .ai-customization-management-editor .mcp-content-container, +.ai-customization-management-editor .plugin-content-container, .ai-customization-management-editor .models-content-container { height: 100%; display: flex; @@ -574,6 +575,18 @@ overflow: hidden; } +/* Embedded plugin detail view */ +.ai-customization-management-editor .plugin-detail-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.ai-customization-management-editor .plugin-detail-editor-container { + flex: 1; + overflow: hidden; +} + /* Models section footer */ .ai-customization-management-editor .models-content-container .section-footer { flex-shrink: 0; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts new file mode 100644 index 00000000000..52e409f290b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -0,0 +1,859 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/aiCustomizationManagement.css'; +import * as DOM from '../../../../../base/browser/dom.js'; +import { Disposable, DisposableStore, isDisposable } from '../../../../../base/common/lifecycle.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { WorkbenchList } from '../../../../../platform/list/browser/listService.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../../base/browser/ui/list/list.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js'; +import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { Delayer } from '../../../../../base/common/async.js'; +import { IAction, Action, Separator } from '../../../../../base/common/actions.js'; +import { basename, dirname } from '../../../../../base/common/resources.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPluginService.js'; +import { IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plugins/pluginMarketplaceService.js'; +import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js'; +import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from '../agentPluginEditor/agentPluginItems.js'; +import { pluginIcon } from './aiCustomizationIcons.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; + +const $ = DOM.$; + +const PLUGIN_ITEM_HEIGHT = 36; +const PLUGIN_GROUP_HEADER_HEIGHT = 36; +const PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; + +//#region Entry types + +/** + * Represents a collapsible group header in the plugin list. + */ +interface IPluginGroupHeaderEntry { + readonly type: 'group-header'; + readonly id: string; + readonly group: 'enabled' | 'disabled'; + readonly label: string; + readonly icon: ThemeIcon; + readonly count: number; + readonly isFirst: boolean; + readonly description: string; + collapsed: boolean; +} + +/** + * Represents an installed plugin item in the list. + */ +interface IPluginInstalledItemEntry { + readonly type: 'plugin-item'; + readonly item: IInstalledPluginItem; +} + +/** + * Represents a marketplace plugin item in the list (browse mode). + */ +interface IPluginMarketplaceItemEntry { + readonly type: 'marketplace-item'; + readonly item: IMarketplacePluginItem; +} + +type IPluginListEntry = IPluginGroupHeaderEntry | IPluginInstalledItemEntry | IPluginMarketplaceItemEntry; + +//#endregion + +//#region Delegate + +class PluginItemDelegate implements IListVirtualDelegate { + getHeight(element: IPluginListEntry): number { + if (element.type === 'group-header') { + return element.isFirst ? PLUGIN_GROUP_HEADER_HEIGHT : PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; + } + return PLUGIN_ITEM_HEIGHT; + } + + getTemplateId(element: IPluginListEntry): string { + if (element.type === 'group-header') { + return 'pluginGroupHeader'; + } + if (element.type === 'marketplace-item') { + return 'pluginMarketplaceItem'; + } + return 'pluginInstalledItem'; + } +} + +//#endregion + +//#region Group Header Renderer (reuses .ai-customization-group-header CSS) + +interface IPluginGroupHeaderTemplateData { + readonly container: HTMLElement; + readonly chevron: HTMLElement; + readonly icon: HTMLElement; + readonly label: HTMLElement; + readonly count: HTMLElement; + readonly infoIcon: HTMLElement; + readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; +} + +class PluginGroupHeaderRenderer implements IListRenderer { + readonly templateId = 'pluginGroupHeader'; + + constructor( + private readonly hoverService: IHoverService, + ) { } + + renderTemplate(container: HTMLElement): IPluginGroupHeaderTemplateData { + const disposables = new DisposableStore(); + const elementDisposables = new DisposableStore(); + container.classList.add('ai-customization-group-header'); + + const chevron = DOM.append(container, $('.group-chevron')); + const icon = DOM.append(container, $('.group-icon')); + const labelGroup = DOM.append(container, $('.group-label-group')); + const label = DOM.append(labelGroup, $('.group-label')); + const infoIcon = DOM.append(labelGroup, $('.group-info')); + infoIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); + const count = DOM.append(container, $('.group-count')); + + return { container, chevron, icon, label, count, infoIcon, disposables, elementDisposables }; + } + + renderElement(element: IPluginGroupHeaderEntry, _index: number, templateData: IPluginGroupHeaderTemplateData): void { + templateData.elementDisposables.clear(); + + templateData.chevron.className = 'group-chevron'; + templateData.chevron.classList.add(...ThemeIcon.asClassNameArray(element.collapsed ? Codicon.chevronRight : Codicon.chevronDown)); + + templateData.icon.className = 'group-icon'; + templateData.icon.classList.add(...ThemeIcon.asClassNameArray(element.icon)); + + templateData.label.textContent = element.label; + templateData.count.textContent = `${element.count}`; + + templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.infoIcon, () => ({ + content: element.description, + appearance: { + compact: true, + skipFadeInAnimation: true, + } + }))); + + templateData.container.classList.toggle('collapsed', element.collapsed); + templateData.container.classList.toggle('has-previous-group', !element.isFirst); + } + + disposeTemplate(templateData: IPluginGroupHeaderTemplateData): void { + templateData.elementDisposables.dispose(); + templateData.disposables.dispose(); + } +} + +//#endregion + +//#region Installed Plugin Renderer (reuses .mcp-server-item CSS) + +interface IPluginInstalledItemTemplateData { + readonly container: HTMLElement; + readonly name: HTMLElement; + readonly description: HTMLElement; + readonly status: HTMLElement; + readonly disposables: DisposableStore; +} + +class PluginInstalledItemRenderer implements IListRenderer { + readonly templateId = 'pluginInstalledItem'; + + renderTemplate(container: HTMLElement): IPluginInstalledItemTemplateData { + container.classList.add('mcp-server-item'); + + const details = DOM.append(container, $('.mcp-server-details')); + const name = DOM.append(details, $('.mcp-server-name')); + const description = DOM.append(details, $('.mcp-server-description')); + const status = DOM.append(container, $('.mcp-server-status')); + + return { container, name, description, status, disposables: new DisposableStore() }; + } + + renderElement(element: IPluginInstalledItemEntry, _index: number, templateData: IPluginInstalledItemTemplateData): void { + templateData.disposables.clear(); + + templateData.name.textContent = element.item.name; + + if (element.item.description) { + templateData.description.textContent = element.item.description; + templateData.description.style.display = ''; + } else { + templateData.description.style.display = 'none'; + } + + // Show enabled/disabled status + templateData.disposables.add(autorun(reader => { + const enabled = element.item.plugin.enabled.read(reader); + templateData.status.className = 'mcp-server-status'; + if (enabled) { + templateData.status.textContent = localize('enabled', "Enabled"); + templateData.status.classList.add('running'); + } else { + templateData.status.textContent = localize('disabled', "Disabled"); + templateData.status.classList.add('stopped'); + } + })); + } + + disposeTemplate(templateData: IPluginInstalledItemTemplateData): void { + templateData.disposables.dispose(); + } +} + +//#endregion + +//#region Marketplace Plugin Renderer (reuses .mcp-gallery-item CSS) + +interface IPluginMarketplaceItemTemplateData { + readonly container: HTMLElement; + readonly name: HTMLElement; + readonly publisher: HTMLElement; + readonly description: HTMLElement; + readonly installButton: Button; + readonly elementDisposables: DisposableStore; + readonly templateDisposables: DisposableStore; +} + +class PluginMarketplaceItemRenderer implements IListRenderer { + readonly templateId = 'pluginMarketplaceItem'; + + constructor( + private readonly pluginInstallService: IPluginInstallService, + ) { } + + renderTemplate(container: HTMLElement): IPluginMarketplaceItemTemplateData { + container.classList.add('mcp-server-item', 'mcp-gallery-item'); + + const details = DOM.append(container, $('.mcp-server-details')); + const nameRow = DOM.append(details, $('.mcp-gallery-name-row')); + const name = DOM.append(nameRow, $('.mcp-server-name')); + const publisher = DOM.append(nameRow, $('.mcp-gallery-publisher')); + const description = DOM.append(details, $('.mcp-server-description')); + + const actionContainer = DOM.append(container, $('.mcp-gallery-action')); + const installButton = new Button(actionContainer, { ...defaultButtonStyles, supportIcons: true }); + installButton.element.classList.add('mcp-gallery-install-button'); + + const templateDisposables = new DisposableStore(); + templateDisposables.add(installButton); + + return { container, name, publisher, description, installButton, elementDisposables: new DisposableStore(), templateDisposables }; + } + + renderElement(element: IPluginMarketplaceItemEntry, _index: number, templateData: IPluginMarketplaceItemTemplateData): void { + templateData.elementDisposables.clear(); + + templateData.name.textContent = element.item.name; + templateData.publisher.textContent = element.item.marketplace ? localize('byPublisher', "by {0}", element.item.marketplace) : ''; + templateData.description.textContent = element.item.description || ''; + + templateData.installButton.label = localize('install', "Install"); + templateData.installButton.enabled = true; + + templateData.elementDisposables.add(templateData.installButton.onDidClick(async () => { + templateData.installButton.label = localize('installing', "Installing..."); + templateData.installButton.enabled = false; + try { + await this.pluginInstallService.installPlugin({ + name: element.item.name, + description: element.item.description, + version: '', + sourceDescriptor: element.item.sourceDescriptor, + source: element.item.source, + marketplace: element.item.marketplace, + marketplaceReference: element.item.marketplaceReference, + marketplaceType: element.item.marketplaceType, + readmeUri: element.item.readmeUri, + }); + templateData.installButton.label = localize('installed', "Installed"); + } catch (_e) { + templateData.installButton.label = localize('install', "Install"); + templateData.installButton.enabled = true; + } + })); + } + + disposeTemplate(templateData: IPluginMarketplaceItemTemplateData): void { + templateData.elementDisposables.dispose(); + templateData.templateDisposables.dispose(); + } +} + +//#endregion + +//#region Plugin context menu actions + +function getInstalledPluginContextMenuActions(plugin: IAgentPlugin, instantiationService: IInstantiationService): IAction[][] { + const groups: IAction[][] = []; + if (plugin.enabled.get()) { + groups.push([instantiationService.createInstance(DisablePluginAction, plugin)]); + } else { + groups.push([instantiationService.createInstance(EnablePluginAction, plugin)]); + } + groups.push([ + instantiationService.createInstance(OpenPluginFolderAction, plugin), + ]); + if (plugin.fromMarketplace) { + groups.push([new UninstallPluginAction(plugin)]); + } + return groups; +} + +class EnablePluginAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + ) { + super('pluginListWidget.enable', localize('enable', "Enable")); + } + + override async run(): Promise { + this.agentPluginService.setPluginEnabled(this.plugin.uri, true); + } +} + +class DisablePluginAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + ) { + super('pluginListWidget.disable', localize('disable', "Disable")); + } + + override async run(): Promise { + this.agentPluginService.setPluginEnabled(this.plugin.uri, false); + } +} + +class OpenPluginFolderAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + @ICommandService private readonly commandService: ICommandService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super('pluginListWidget.openFolder', localize('openPluginFolder', "Open Plugin Folder")); + } + + override async run(): Promise { + try { + await this.commandService.executeCommand('revealFileInOS', this.plugin.uri); + } catch { + await this.openerService.open(dirname(this.plugin.uri)); + } + } +} + +class UninstallPluginAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + ) { + super('pluginListWidget.uninstall', localize('uninstall', "Uninstall")); + } + + override async run(): Promise { + this.plugin.remove(); + } +} + +//#endregion + +//#region Helpers + +function installedPluginToItem(plugin: IAgentPlugin, labelService: ILabelService): IInstalledPluginItem { + const name = plugin.label ?? basename(plugin.uri); + const description = plugin.fromMarketplace?.description ?? labelService.getUriLabel(dirname(plugin.uri), { relative: true }); + const marketplace = plugin.fromMarketplace?.marketplace; + return { kind: AgentPluginItemKind.Installed, name, description, marketplace, plugin }; +} + +function marketplacePluginToItem(plugin: IMarketplacePlugin): IMarketplacePluginItem { + return { + kind: AgentPluginItemKind.Marketplace, + name: plugin.name, + description: plugin.description, + source: plugin.source, + sourceDescriptor: plugin.sourceDescriptor, + marketplace: plugin.marketplace, + marketplaceReference: plugin.marketplaceReference, + marketplaceType: plugin.marketplaceType, + readmeUri: plugin.readmeUri, + }; +} + +//#endregion + +/** + * Widget that displays a list of agent plugins with marketplace browsing. + * Follows the same patterns as {@link McpListWidget}. + */ +export class PluginListWidget extends Disposable { + + readonly element: HTMLElement; + + private readonly _onDidSelectPlugin = this._register(new Emitter()); + readonly onDidSelectPlugin = this._onDidSelectPlugin.event; + + private sectionHeader!: HTMLElement; + private sectionDescription!: HTMLElement; + private sectionLink!: HTMLAnchorElement; + private searchAndButtonContainer!: HTMLElement; + private searchInput!: InputBox; + private listContainer!: HTMLElement; + private list!: WorkbenchList; + private emptyContainer!: HTMLElement; + private emptyText!: HTMLElement; + private emptySubtext!: HTMLElement; + private browseButton!: Button; + private backLink!: HTMLElement; + + private installedItems: IInstalledPluginItem[] = []; + private displayEntries: IPluginListEntry[] = []; + private marketplaceItems: IMarketplacePluginItem[] = []; + private searchQuery: string = ''; + private browseMode: boolean = false; + private readonly collapsedGroups = new Set(); + private marketplaceCts: CancellationTokenSource | undefined; + private readonly delayedFilter = new Delayer(200); + private readonly delayedMarketplaceSearch = new Delayer(400); + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + @IPluginMarketplaceService private readonly pluginMarketplaceService: IPluginMarketplaceService, + @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, + @IOpenerService private readonly openerService: IOpenerService, + @IContextViewService private readonly contextViewService: IContextViewService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IHoverService private readonly hoverService: IHoverService, + @ILabelService private readonly labelService: ILabelService, + ) { + super(); + this.element = $('.mcp-list-widget'); // reuse MCP list widget CSS + this.create(); + this._register({ + dispose: () => { + this.marketplaceCts?.dispose(); + } + }); + } + + private create(): void { + // Search and button container + this.searchAndButtonContainer = DOM.append(this.element, $('.list-search-and-button-container')); + + // Search container + const searchContainer = DOM.append(this.searchAndButtonContainer, $('.list-search-container')); + this.searchInput = this._register(new InputBox(searchContainer, this.contextViewService, { + placeholder: localize('searchPluginsPlaceholder', "Type to search..."), + inputBoxStyles: defaultInputBoxStyles, + })); + + this._register(this.searchInput.onDidChange(() => { + this.searchQuery = this.searchInput.value; + if (this.browseMode) { + this.delayedMarketplaceSearch.trigger(() => this.queryMarketplace()); + } else { + this.delayedFilter.trigger(() => this.filterPlugins()); + } + })); + + // Button container (Browse Marketplace) + const buttonContainer = DOM.append(this.searchAndButtonContainer, $('.list-button-group')); + + const browseButtonContainer = DOM.append(buttonContainer, $('.list-add-button-container')); + this.browseButton = this._register(new Button(browseButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + this.browseButton.label = `$(${Codicon.library.id}) ${localize('browseMarketplace', "Browse Marketplace")}`; + this.browseButton.element.classList.add('list-add-button'); + this._register(this.browseButton.onDidClick(() => { + this.toggleBrowseMode(!this.browseMode); + })); + + // Back to installed link (shown only in browse mode) + this.backLink = DOM.append(this.element, $('.mcp-back-link')); + this.backLink.setAttribute('role', 'button'); + this.backLink.tabIndex = 0; + this.backLink.setAttribute('aria-label', localize('backToInstalledPluginsAriaLabel', "Back to installed plugins")); + const backIcon = DOM.append(this.backLink, $('span')); + backIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.arrowLeft)); + const backText = DOM.append(this.backLink, $('span')); + backText.textContent = localize('backToInstalledPlugins', "Back to installed plugins"); + this._register(DOM.addDisposableListener(this.backLink, 'click', () => { + this.toggleBrowseMode(false); + })); + this._register(DOM.addDisposableListener(this.backLink, 'keydown', (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.toggleBrowseMode(false); + } + })); + this.backLink.style.display = 'none'; + + // Empty state + this.emptyContainer = DOM.append(this.element, $('.mcp-empty-state')); + const emptyIcon = DOM.append(this.emptyContainer, $('.empty-icon')); + emptyIcon.classList.add(...ThemeIcon.asClassNameArray(pluginIcon)); + this.emptyText = DOM.append(this.emptyContainer, $('.empty-text')); + this.emptySubtext = DOM.append(this.emptyContainer, $('.empty-subtext')); + + // List container + this.listContainer = DOM.append(this.element, $('.mcp-list-container')); + + // Section footer + this.sectionHeader = DOM.append(this.element, $('.section-footer')); + this.sectionDescription = DOM.append(this.sectionHeader, $('p.section-footer-description')); + this.sectionDescription.textContent = localize('pluginsDescription', "Extend your AI agent with plugins that add commands, skills, agents, hooks, and MCP servers from reusable packages."); + this.sectionLink = DOM.append(this.sectionHeader, $('a.section-footer-link')) as HTMLAnchorElement; + this.sectionLink.textContent = localize('learnMorePlugins', "Learn more about agent plugins"); + this.sectionLink.href = 'https://code.visualstudio.com/docs/copilot/chat/agent-plugins'; + this._register(DOM.addDisposableListener(this.sectionLink, 'click', (e) => { + e.preventDefault(); + const href = this.sectionLink.href; + if (href) { + this.openerService.open(URI.parse(href)); + } + })); + + // Create list + const delegate = new PluginItemDelegate(); + const groupHeaderRenderer = new PluginGroupHeaderRenderer(this.hoverService); + const installedRenderer = new PluginInstalledItemRenderer(); + const marketplaceRenderer = new PluginMarketplaceItemRenderer(this.pluginInstallService); + + this.list = this._register(this.instantiationService.createInstance( + WorkbenchList, + 'PluginManagementList', + this.listContainer, + delegate, + [groupHeaderRenderer, installedRenderer, marketplaceRenderer], + { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(element: IPluginListEntry) { + if (element.type === 'group-header') { + return localize('pluginGroupAriaLabel', "{0}, {1} items, {2}", element.label, element.count, element.collapsed ? localize('collapsed', "collapsed") : localize('expanded', "expanded")); + } + if (element.type === 'marketplace-item') { + return element.item.name; + } + return element.item.name; + }, + getWidgetAriaLabel() { + return localize('pluginsListAriaLabel', "Plugins"); + } + }, + openOnSingleClick: true, + identityProvider: { + getId(element: IPluginListEntry) { + if (element.type === 'group-header') { + return element.id; + } + if (element.type === 'marketplace-item') { + return `marketplace-${element.item.marketplaceReference.canonicalId}/${element.item.source}`; + } + return element.item.plugin.uri.toString(); + } + } + } + )); + + this._register(this.list.onDidOpen(e => { + if (e.element) { + if (e.element.type === 'group-header') { + this.toggleGroup(e.element); + } else if (e.element.type === 'plugin-item') { + this._onDidSelectPlugin.fire(e.element.item); + } else if (e.element.type === 'marketplace-item') { + this._onDidSelectPlugin.fire(e.element.item); + } + } + })); + + // Handle context menu + this._register(this.list.onContextMenu(e => this.onContextMenu(e as IListContextMenuEvent))); + + // Listen to plugin service changes + this._register(autorun(reader => { + this.agentPluginService.allPlugins.read(reader); + if (!this.browseMode) { + this.refresh(); + } + })); + this._register(this.pluginMarketplaceService.onDidChangeMarketplaces(() => { + if (!this.browseMode) { + this.refresh(); + } + })); + + // Initial refresh + void this.refresh(); + } + + private async refresh(): Promise { + if (this.browseMode) { + await this.queryMarketplace(); + } else { + this.filterPlugins(); + } + } + + private toggleBrowseMode(browse: boolean): void { + this.browseMode = browse; + this.searchInput.value = ''; + this.searchQuery = ''; + + this.backLink.style.display = browse ? '' : 'none'; + this.browseButton.element.parentElement!.style.display = browse ? 'none' : ''; + + this.searchInput.setPlaceHolder(browse + ? localize('searchMarketplacePlaceholder', "Search plugin marketplace...") + : localize('searchPluginsPlaceholder', "Type to search...") + ); + + if (browse) { + void this.queryMarketplace(); + } else { + this.marketplaceCts?.dispose(true); + this.marketplaceItems = []; + this.filterPlugins(); + } + } + + private async queryMarketplace(): Promise { + this.marketplaceCts?.dispose(true); + const cts = this.marketplaceCts = new CancellationTokenSource(); + + // Show loading state + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + this.emptyText.textContent = localize('loadingMarketplace', "Loading marketplace..."); + this.emptySubtext.textContent = ''; + + try { + const plugins = await this.pluginMarketplaceService.fetchMarketplacePlugins(cts.token); + + if (cts.token.isCancellationRequested) { + return; + } + + const query = this.searchQuery.toLowerCase().trim(); + const filtered = query + ? plugins.filter(p => p.name.toLowerCase().includes(query) || p.description.toLowerCase().includes(query)) + : plugins; + + // Filter out already-installed plugins + const installedUris = new Set(this.agentPluginService.allPlugins.get().map(p => p.uri.toString())); + this.marketplaceItems = filtered + .filter(p => { + const expectedUri = this.pluginInstallService.getPluginInstallUri(p); + return !installedUris.has(expectedUri.toString()); + }) + .map(marketplacePluginToItem); + + this.updateMarketplaceList(); + } catch { + if (!cts.token.isCancellationRequested) { + this.marketplaceItems = []; + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + this.emptyText.textContent = localize('marketplaceError', "Unable to load marketplace"); + this.emptySubtext.textContent = localize('tryAgainLater', "Check your connection and try again"); + } + } + } + + private updateMarketplaceList(): void { + if (this.marketplaceItems.length === 0) { + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + if (this.searchQuery.trim()) { + this.emptyText.textContent = localize('noMarketplaceResults', "No plugins match '{0}'", this.searchQuery); + this.emptySubtext.textContent = localize('tryDifferentSearch', "Try a different search term"); + } else { + this.emptyText.textContent = localize('emptyMarketplace', "No plugins available"); + this.emptySubtext.textContent = ''; + } + } else { + this.emptyContainer.style.display = 'none'; + this.listContainer.style.display = ''; + } + + const entries: IPluginListEntry[] = this.marketplaceItems.map(item => ({ type: 'marketplace-item' as const, item })); + this.list.splice(0, this.list.length, entries); + } + + private filterPlugins(): void { + const query = this.searchQuery.toLowerCase().trim(); + const allPlugins = this.agentPluginService.allPlugins.get(); + + this.installedItems = allPlugins + .map(p => installedPluginToItem(p, this.labelService)) + .filter(item => !query || + item.name.toLowerCase().includes(query) || + item.description.toLowerCase().includes(query) + ); + + if (this.installedItems.length === 0) { + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + + if (this.searchQuery.trim()) { + this.emptyText.textContent = localize('noMatchingPlugins', "No plugins match '{0}'", this.searchQuery); + this.emptySubtext.textContent = localize('tryDifferentSearch', "Try a different search term"); + } else { + this.emptyText.textContent = localize('noPlugins', "No plugins installed"); + this.emptySubtext.textContent = localize('browseToAdd', "Browse the marketplace to discover and install plugins"); + } + } else { + this.emptyContainer.style.display = 'none'; + this.listContainer.style.display = ''; + } + + // Group plugins: enabled vs disabled + const enabledPlugins = this.installedItems.filter(item => item.plugin.enabled.get()); + const disabledPlugins = this.installedItems.filter(item => !item.plugin.enabled.get()); + + const entries: IPluginListEntry[] = []; + let isFirst = true; + + if (enabledPlugins.length > 0) { + const collapsed = this.collapsedGroups.has('enabled'); + entries.push({ + type: 'group-header', + id: 'plugin-group-enabled', + group: 'enabled', + label: localize('enabledGroup', "Enabled"), + icon: pluginIcon, + count: enabledPlugins.length, + isFirst, + description: localize('enabledGroupDescription', "Plugins that are currently active and providing commands, skills, agents, and other capabilities."), + collapsed, + }); + if (!collapsed) { + for (const item of enabledPlugins) { + entries.push({ type: 'plugin-item', item }); + } + } + isFirst = false; + } + + if (disabledPlugins.length > 0) { + const collapsed = this.collapsedGroups.has('disabled'); + entries.push({ + type: 'group-header', + id: 'plugin-group-disabled', + group: 'disabled', + label: localize('disabledGroup', "Disabled"), + icon: pluginIcon, + count: disabledPlugins.length, + isFirst, + description: localize('disabledGroupDescription', "Plugins that are installed but currently disabled. Enable them to use their capabilities."), + collapsed, + }); + if (!collapsed) { + for (const item of disabledPlugins) { + entries.push({ type: 'plugin-item', item }); + } + } + } + + this.displayEntries = entries; + this.list.splice(0, this.list.length, this.displayEntries); + } + + private toggleGroup(entry: IPluginGroupHeaderEntry): void { + if (this.collapsedGroups.has(entry.group)) { + this.collapsedGroups.delete(entry.group); + } else { + this.collapsedGroups.add(entry.group); + } + this.filterPlugins(); + } + + layout(height: number, width: number): void { + const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; + const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight; + + this.listContainer.style.height = `${Math.max(0, listHeight)}px`; + this.list.layout(Math.max(0, listHeight), width); + + if (sectionFooterHeight === 0) { + DOM.getWindow(this.listContainer).requestAnimationFrame(() => { + if (this._store.isDisposed) { + return; + } + const actualFooterHeight = this.sectionHeader.offsetHeight; + if (actualFooterHeight > 0) { + const correctedHeight = height - actualFooterHeight - searchBarHeight - backLinkHeight; + this.listContainer.style.height = `${Math.max(0, correctedHeight)}px`; + this.list.layout(Math.max(0, correctedHeight), width); + } + }); + } + } + + focusSearch(): void { + this.searchInput.focus(); + } + + focus(): void { + this.list.domFocus(); + if (this.list.length > 0) { + this.list.setFocus([0]); + } + } + + private onContextMenu(e: IListContextMenuEvent): void { + if (!e.element || e.element.type !== 'plugin-item') { + return; + } + + const entry = e.element; + const disposables = new DisposableStore(); + const groups: IAction[][] = getInstalledPluginContextMenuActions(entry.item.plugin, this.instantiationService); + const actions: IAction[] = []; + for (const menuActions of groups) { + for (const menuAction of menuActions) { + actions.push(menuAction); + if (isDisposable(menuAction)) { + disposables.add(menuAction); + } + } + actions.push(new Separator()); + } + if (actions.length > 0 && actions[actions.length - 1] instanceof Separator) { + actions.pop(); + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + onHide: () => disposables.dispose() + }); + } +} diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index de4108ff5fb..286257580e2 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -23,6 +23,7 @@ export const AICustomizationManagementSection = { Prompts: 'prompts', Hooks: 'hooks', McpServers: 'mcpServers', + Plugins: 'plugins', Models: 'models', } as const; From 87cec2bf5b9c309aba8f3ac0882adfb2adb10d4b Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Wed, 4 Mar 2026 21:37:19 +0100 Subject: [PATCH 183/448] Add `.ronn` extension to markdown --- extensions/markdown-basics/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index b24c8594cc2..434c7e7c667 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -29,6 +29,7 @@ ".mdtxt", ".mdtext", ".ron", + ".ronn", ".workbook" ], "filenamePatterns": [ From d405135f7148777b16c838f2e81fc9c16f722b25 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 4 Mar 2026 15:41:25 -0500 Subject: [PATCH 184/448] add experiment to elevate AI terminal profiles (#299270) --- src/vs/platform/terminal/common/terminal.ts | 1 + .../terminal/browser/terminalEditor.ts | 6 +- .../contrib/terminal/browser/terminalMenus.ts | 149 ++++++++++++++---- .../contrib/terminal/browser/terminalView.ts | 11 +- .../terminal/common/terminalConfiguration.ts | 9 ++ 5 files changed, 139 insertions(+), 37 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index dc7c596c346..44e53e09a64 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -122,6 +122,7 @@ export const enum TerminalSettingId { FontLigaturesFallbackLigatures = 'terminal.integrated.fontLigatures.fallbackLigatures', EnableKittyKeyboardProtocol = 'terminal.integrated.enableKittyKeyboardProtocol', EnableWin32InputMode = 'terminal.integrated.enableWin32InputMode', + ExperimentalAiProfileGrouping = 'terminal.integrated.experimental.aiProfileGrouping', AllowInUntrustedWorkspace = 'terminal.integrated.allowInUntrustedWorkspace', // Developer/debug settings diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 8b40d469375..b158684ae0e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -13,6 +13,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -60,6 +61,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -156,7 +158,7 @@ export class TerminalEditor extends EditorPane { private _updateTabActionBar(profiles: ITerminalProfile[]): void { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -180,7 +182,7 @@ export class TerminalEditor extends EditorPane { if (action instanceof MenuItemAction) { const location = { viewColumn: ACTIVE_GROUP }; this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate }); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); return this._newDropdown.value; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index b0860ecc69c..281d624fcec 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -8,6 +8,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Schemas } from '../../../../base/common/network.js'; import { localize, localize2 } from '../../../../nls.js'; import { IMenu, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionTerminalProfile, ITerminalProfile, TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { ResourceContextKey } from '../../../common/contextkeys.js'; @@ -781,12 +782,20 @@ export function setupTerminalMenus(): void { } } -export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore): { +export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore, configurationService: IConfigurationService): { dropdownAction: IAction; dropdownMenuActions: IAction[]; className: string; dropdownIcon?: string; } { + const shouldElevateAiProfiles = configurationService.getValue(TerminalSettingId.ExperimentalAiProfileGrouping); + profiles = profiles.filter(e => !e.isAutoDetected); + const [aiProfiles, otherProfiles] = shouldElevateAiProfiles + ? splitProfiles(profiles) + : [[], profiles]; + const [aiContributedProfiles, otherContributedProfiles] = shouldElevateAiProfiles + ? splitContributedProfiles(contributedProfiles) + : [[], contributedProfiles]; const dropdownActions: IAction[] = []; const submenuActions: IAction[] = []; const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && hasKey(location, { viewColumn: true }) && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; @@ -806,40 +815,22 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro location: splitLocation })))); dropdownActions.push(new Separator()); - - profiles = profiles.filter(e => !e.isAutoDetected); - for (const p of profiles) { - const isDefault = p.profileName === defaultProfileName; - const options: ICreateTerminalOptions = { config: p, location }; - const splitOptions: ICreateTerminalOptions = { config: p, location: splitLocation }; - const sanitizedProfileName = p.profileName.replace(/[\n\r\t]/g, ''); - dropdownActions.push(disposableStore.add(new Action(TerminalCommandId.NewWithProfile, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { - await terminalService.createAndFocusTerminal(options); - }))); - submenuActions.push(disposableStore.add(new Action(TerminalCommandId.Split, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { - await terminalService.createAndFocusTerminal(splitOptions); - }))); + for (const p of aiProfiles) { + addProfileActions(p, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); + } + for (const contributed of aiContributedProfiles) { + addContributedProfileActions(contributed, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); + } + if ((aiProfiles.length > 0 || aiContributedProfiles.length > 0) && (otherProfiles.length > 0 || otherContributedProfiles.length > 0)) { + dropdownActions.push(new Separator()); } - for (const contributed of contributedProfiles) { - const isDefault = contributed.title === defaultProfileName; - const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, ''); - dropdownActions.push(disposableStore.add(new Action('contributed', title, undefined, true, () => terminalService.createAndFocusTerminal({ - config: { - extensionIdentifier: contributed.extensionIdentifier, - id: contributed.id, - title - }, - location - })))); - submenuActions.push(disposableStore.add(new Action('contributed-split', title, undefined, true, () => terminalService.createAndFocusTerminal({ - config: { - extensionIdentifier: contributed.extensionIdentifier, - id: contributed.id, - title - }, - location: splitLocation - })))); + for (const p of otherProfiles) { + addProfileActions(p, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); + } + + for (const contributed of otherContributedProfiles) { + addContributedProfileActions(contributed, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); } if (dropdownActions.length > 0) { @@ -852,3 +843,95 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro const dropdownAction = disposableStore.add(new Action('refresh profiles', localize('launchProfile', 'Launch Profile...'), 'codicon-chevron-down', true)); return { dropdownAction, dropdownMenuActions: dropdownActions, className: `terminal-tab-actions-${terminalService.resolveLocation(location)}` }; } + +function splitProfiles(profiles: readonly ITerminalProfile[]): [ITerminalProfile[], ITerminalProfile[]] { + const aiProfiles: ITerminalProfile[] = []; + const otherProfiles: ITerminalProfile[] = []; + for (const profile of profiles) { + if (isAiProfileName(profile.profileName)) { + aiProfiles.push(profile); + } else { + otherProfiles.push(profile); + } + } + return [aiProfiles, otherProfiles]; +} + +function splitContributedProfiles(contributedProfiles: readonly IExtensionTerminalProfile[]): [IExtensionTerminalProfile[], IExtensionTerminalProfile[]] { + const aiContributedProfiles: IExtensionTerminalProfile[] = []; + const otherContributedProfiles: IExtensionTerminalProfile[] = []; + for (const profile of contributedProfiles) { + if (isAiContributedProfile(profile)) { + aiContributedProfiles.push(profile); + } else { + otherContributedProfiles.push(profile); + } + } + return [aiContributedProfiles, otherContributedProfiles]; +} + +function isAiContributedProfile(profile: IExtensionTerminalProfile): boolean { + const extensionIdentifier = profile.extensionIdentifier.toLowerCase(); + if (extensionIdentifier === 'github.copilot-chat' || extensionIdentifier === 'anthropic.claude-code') { + return true; + } + + return isAiProfileName(profile.title); +} + +function isAiProfileName(name: string): boolean { + const lowerCaseName = name.toLowerCase(); + return lowerCaseName.includes('copilot') || lowerCaseName.includes('claude'); +} + +function addProfileActions( + profile: ITerminalProfile, + defaultProfileName: string, + location: ITerminalLocationOptions, + splitLocation: ITerminalLocationOptions, + terminalService: ITerminalService, + dropdownActions: IAction[], + submenuActions: IAction[], + disposableStore: DisposableStore +): void { + const isDefault = profile.profileName === defaultProfileName; + const options: ICreateTerminalOptions = { config: profile, location }; + const splitOptions: ICreateTerminalOptions = { config: profile, location: splitLocation }; + const sanitizedProfileName = profile.profileName.replace(/[\n\r\t]/g, ''); + dropdownActions.push(disposableStore.add(new Action(TerminalCommandId.NewWithProfile, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { + await terminalService.createAndFocusTerminal(options); + }))); + submenuActions.push(disposableStore.add(new Action(TerminalCommandId.Split, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { + await terminalService.createAndFocusTerminal(splitOptions); + }))); +} + +function addContributedProfileActions( + contributed: IExtensionTerminalProfile, + defaultProfileName: string, + location: ITerminalLocationOptions, + splitLocation: ITerminalLocationOptions, + terminalService: ITerminalService, + dropdownActions: IAction[], + submenuActions: IAction[], + disposableStore: DisposableStore +): void { + const isDefault = contributed.title === defaultProfileName; + const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, ''); + dropdownActions.push(disposableStore.add(new Action('contributed', title, undefined, true, () => terminalService.createAndFocusTerminal({ + config: { + extensionIdentifier: contributed.extensionIdentifier, + id: contributed.id, + title + }, + location + })))); + submenuActions.push(disposableStore.add(new Action('contributed-split', title, undefined, true, () => terminalService.createAndFocusTerminal({ + config: { + extensionIdentifier: contributed.extensionIdentifier, + id: contributed.id, + title + }, + location: splitLocation + })))); +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 9a745727415..01632bee153 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -289,7 +289,7 @@ export class TerminalViewPane extends ViewPane { case TerminalCommandId.New: { if (action instanceof MenuItemAction) { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate, getKeyBinding: (action: IAction) => this._keybindingService.lookupKeybinding(action.id, this._contextKeyService) @@ -318,8 +318,15 @@ export class TerminalViewPane extends ViewPane { private _updateTabActionBar(profiles: ITerminalProfile[]): void { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); + + this._disposableStore.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.ExperimentalAiProfileGrouping)) { + const updatedActions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); + this._newDropdown.value?.update(updatedActions.dropdownAction, updatedActions.dropdownMenuActions); + } + })); } override focus() { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 632938e0d5d..132db6d2982 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -604,6 +604,15 @@ const terminalConfiguration: IStringDictionary = { mode: 'auto' } }, + [TerminalSettingId.ExperimentalAiProfileGrouping]: { + markdownDescription: localize('terminal.integrated.experimental.aiProfileGrouping', "Whether to elevate AI-contributed terminal profiles (for example Copilot CLI and Claude Agent) in the new terminal dropdown."), + type: 'boolean', + default: false, + tags: ['experimental'], + experiment: { + mode: 'auto' + } + }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh, git bash\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", '`#terminal.integrated.shellIntegration.decorationsEnabled#`', '`#editor.accessibilitySupport#`'), From 49b4ea3cf6e00b9a5ce1f6377d42e7e8defbee38 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:44:18 -0800 Subject: [PATCH 185/448] don't allow expanding until hitting max height in thinking (#299249) * don't allow expanding until hitting max height in thinking * Address comments --- .../chatContentParts/chatThinkingContentPart.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index bbab1de37aa..d695b515e88 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -500,6 +500,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen height: viewportHeight, scrollHeight: contentHeight }); + + // Re-evaluate hover feedback as content grows past the max height, + // reusing the already-measured contentHeight to avoid an extra layout read. + this.updateDropdownClickability(contentHeight); } private scrollToBottom(): void { @@ -651,8 +655,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen return !(!strippedContent || strippedContent === titleToCompare); } - private updateDropdownClickability(): void { - const allowExpansion = this.shouldAllowExpansion(); + private updateDropdownClickability(knownContentHeight?: number): void { + let allowExpansion = this.shouldAllowExpansion(); + + // don't allow feedback on fixed scrolling before reaching max height. + if (allowExpansion && this.fixedScrollingMode && !this.streamingCompleted && !this.element.isComplete && this.wrapper) { + const contentHeight = knownContentHeight ?? this.wrapper.scrollHeight; + if (contentHeight <= THINKING_SCROLL_MAX_HEIGHT) { + allowExpansion = false; + } + } + if (!allowExpansion && this.isExpanded()) { this.setExpanded(false); } From 1b44525f0e01ea615c547cdeea3ce4417c5a4723 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:45:34 +0100 Subject: [PATCH 186/448] =?UTF-8?q?Clear=20previous=20sync=20action=20befo?= =?UTF-8?q?re=20registering=20a=20new=20one=20in=20GitSyncCon=E2=80=A6=20(?= =?UTF-8?q?#298898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clear previous sync action before registering a new one in GitSyncContribution --- src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts index 2aed42bfd4c..9aaaac7acb3 100644 --- a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts +++ b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts @@ -73,6 +73,7 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { const behind = head.behind ?? 0; const hasSyncChanges = ahead > 0 || behind > 0; contextKey.set(hasSyncChanges); + this._syncActionDisposable.clear(); this._syncActionDisposable.value = registerSyncAction(behind, ahead, isSyncing, (syncing) => { this._isSyncing.set(syncing, undefined); }); From ffe529eced87d41e5e206f0c3f6089d45a77c581 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 4 Mar 2026 13:50:19 -0800 Subject: [PATCH 187/448] Add support for agent-scoped hooks (#299029) --- .../aiCustomizationListWidget.ts | 67 +++- .../chat/browser/promptSyntax/hookActions.ts | 71 +++- .../chat/browser/promptSyntax/hookUtils.ts | 87 +++++ .../common/chatService/chatServiceImpl.ts | 16 +- .../common/promptSyntax/hookClaudeCompat.ts | 60 +-- .../chat/common/promptSyntax/hookSchema.ts | 218 ++++++++++- .../languageProviders/promptFileAttributes.ts | 4 + .../promptHeaderAutocompletion.ts | 332 +++++++++++++++- .../languageProviders/promptHovers.ts | 62 ++- .../languageProviders/promptValidator.ts | 119 +++++- .../common/promptSyntax/promptFileParser.ts | 15 + .../promptSyntax/service/promptsService.ts | 5 + .../service/promptsServiceImpl.ts | 20 +- .../tools/builtinTools/runSubagentTool.ts | 17 +- .../browser/promptSyntax/hookUtils.test.ts | 230 +++++++++++- .../promptHeaderAutocompletion.test.ts | 244 ++++++++++++ .../languageProviders/promptValidator.test.ts | 354 +++++++++++++++++- .../common/promptSyntax/hookSchema.test.ts | 162 +++++++- .../service/promptsService.test.ts | 14 + 19 files changed, 1982 insertions(+), 115 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 474df4e970b..547f42e190a 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -43,7 +43,7 @@ import { IPathService } from '../../../../services/path/common/pathService.js'; import { generateCustomizationDebugReport } from './aiCustomizationDebugPanel.js'; import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js'; import { formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js'; -import { HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'; +import { HookType, HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Schemas } from '../../../../../base/common/network.js'; import { OS } from '../../../../../base/common/platform.js'; @@ -65,6 +65,8 @@ export interface IAICustomizationListItem { readonly description?: string; readonly storage: PromptsStorage; readonly promptType: PromptsType; + /** When set, overrides `storage` for display grouping purposes. */ + readonly groupKey?: string; nameMatches?: IMatch[]; descriptionMatches?: IMatch[]; } @@ -75,7 +77,7 @@ export interface IAICustomizationListItem { interface IGroupHeaderEntry { readonly type: 'group-header'; readonly id: string; - readonly storage: PromptsStorage; + readonly groupKey: string; readonly label: string; readonly icon: ThemeIcon; readonly count: number; @@ -311,7 +313,7 @@ export class AICustomizationListWidget extends Disposable { private allItems: IAICustomizationListItem[] = []; private displayEntries: IListEntry[] = []; private searchQuery: string = ''; - private readonly collapsedGroups = new Set(); + private readonly collapsedGroups = new Set(); private readonly dropdownActionDisposables = this._register(new DisposableStore()); private readonly delayedFilter = new Delayer(200); @@ -827,6 +829,37 @@ export class AICustomizationListWidget extends Disposable { }); } } + + // Also include hooks defined in agent frontmatter (not in sessions window) + // TODO: add this back when Copilot CLI supports this + const agents = !this.workspaceService.isSessionsWindow ? await this.promptsService.getCustomAgents(CancellationToken.None) : []; + for (const agent of agents) { + if (!agent.hooks) { + continue; + } + for (const hookType of Object.values(HookType)) { + const hookCommands = agent.hooks[hookType]; + if (!hookCommands || hookCommands.length === 0) { + continue; + } + const hookMeta = HOOK_METADATA[hookType]; + for (let i = 0; i < hookCommands.length; i++) { + const hook = hookCommands[i]; + const cmdLabel = formatHookCommandLabel(hook, OS); + const truncatedCmd = cmdLabel.length > 60 ? cmdLabel.substring(0, 57) + '...' : cmdLabel; + items.push({ + id: `${agent.uri.toString()}#hook:${hookType}[${i}]`, + uri: agent.uri, + name: hookMeta?.label ?? hookType, + filename: basename(agent.uri), + description: `${agent.name}: ${truncatedCmd || localize('hookUnset', "(unset)")}`, + storage: agent.source.storage, + groupKey: 'agents', + promptType, + }); + } + } + } } else { // For instructions, fetch prompt files and group by storage const promptFiles = await this.promptsService.listPromptFiles(promptType, CancellationToken.None); @@ -940,15 +973,17 @@ export class AICustomizationListWidget extends Disposable { // Group items by storage const promptType = sectionToPromptType(this.currentSection); const visibleSources = new Set(this.workspaceService.getStorageSourceFilter(promptType).sources); - const groups: { storage: PromptsStorage; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ - { storage: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, - { storage: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, - { storage: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, - { storage: PromptsStorage.plugin, label: localize('pluginGroup', "Plugins"), icon: pluginIcon, description: localize('pluginGroupDescription', "Read-only customizations provided by installed plugins."), items: [] }, - ].filter(g => visibleSources.has(g.storage)); + const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ + { groupKey: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, + { groupKey: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, + { groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, + { groupKey: PromptsStorage.plugin, label: localize('pluginGroup', "Plugins"), icon: pluginIcon, description: localize('pluginGroupDescription', "Read-only customizations provided by installed plugins."), items: [] }, + { groupKey: 'agents', label: localize('agentsGroup', "Agents"), icon: agentIcon, description: localize('agentsGroupDescription', "Hooks defined in agent files."), items: [] }, + ].filter(g => visibleSources.has(g.groupKey as PromptsStorage) || g.groupKey === 'agents'); for (const item of matchedItems) { - const group = groups.find(g => g.storage === item.storage); + const key = item.groupKey ?? item.storage; + const group = groups.find(g => g.groupKey === key); if (group) { group.items.push(item); } @@ -967,12 +1002,12 @@ export class AICustomizationListWidget extends Disposable { continue; } - const collapsed = this.collapsedGroups.has(group.storage); + const collapsed = this.collapsedGroups.has(group.groupKey); this.displayEntries.push({ type: 'group-header', - id: `group-${group.storage}`, - storage: group.storage, + id: `group-${group.groupKey}`, + groupKey: group.groupKey, label: group.label, icon: group.icon, count: group.items.length, @@ -997,10 +1032,10 @@ export class AICustomizationListWidget extends Disposable { * Toggles the collapsed state of a group. */ private toggleGroup(entry: IGroupHeaderEntry): void { - if (this.collapsedGroups.has(entry.storage)) { - this.collapsedGroups.delete(entry.storage); + if (this.collapsedGroups.has(entry.groupKey)) { + this.collapsedGroups.delete(entry.groupKey); } else { - this.collapsedGroups.add(entry.storage); + this.collapsedGroups.add(entry.groupKey); } this.filterItems(); } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index 805c977bf06..244bf9498ef 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -24,14 +24,14 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { HOOK_METADATA, HOOKS_BY_TARGET, HookType, IHookTypeMeta } from '../../common/promptSyntax/hookTypes.js'; -import { getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js'; +import { formatHookCommandLabel, getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js'; import { getCopilotCliHookTypeName, resolveCopilotCliHookType } from '../../common/promptSyntax/hookCopilotCliCompat.js'; import { getHookSourceFormat, HookSourceFormat, buildNewHookEntry } from '../../common/promptSyntax/hookCompatibility.js'; import { getClaudeHookTypeName, resolveClaudeHookType } from '../../common/promptSyntax/hookClaudeCompat.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js'; -import { findHookCommandSelection, parseAllHookFiles, IParsedHook } from './hookUtils.js'; +import { findHookCommandSelection, findHookCommandInYaml, parseAllHookFiles, IParsedHook } from './hookUtils.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../services/path/common/pathService.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; @@ -348,7 +348,8 @@ export async function showConfigureHooksQuickPick( workspaceRootUri, userHome, targetOS, - CancellationToken.None + CancellationToken.None, + { includeAgentHooks: true } ); // Count hooks per type @@ -445,6 +446,10 @@ export async function showConfigureHooksQuickPick( // Filter hooks by the selected type const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType!.hookType); + // Separate hooks by source + const fileHooks = hooksOfType.filter(h => !h.agentName); + const agentHooks = hooksOfType.filter(h => h.agentName); + // Step 2: Show "Add new hook" + existing hooks of this type const hookItems: (IHookQuickPickItem | IQuickPickSeparator)[] = []; @@ -455,14 +460,14 @@ export async function showConfigureHooksQuickPick( alwaysShow: true }); - // Add existing hooks - if (hooksOfType.length > 0) { + // Add existing file-based hooks + if (fileHooks.length > 0) { hookItems.push({ type: 'separator', label: localize('existingHooks', "Existing Hooks") }); - for (const entry of hooksOfType) { + for (const entry of fileHooks) { const description = labelService.getUriLabel(entry.fileUri, { relative: true }); hookItems.push({ label: entry.commandLabel, @@ -472,6 +477,26 @@ export async function showConfigureHooksQuickPick( } } + // Add agent-defined hooks grouped by agent name + if (agentHooks.length > 0) { + const agentNames = [...new Set(agentHooks.map(h => h.agentName!))]; + for (const agentName of agentNames) { + hookItems.push({ + type: 'separator', + label: localize('agentHooks', "Agent: {0}", agentName) + }); + + for (const entry of agentHooks.filter(h => h.agentName === agentName)) { + const description = labelService.getUriLabel(entry.fileUri, { relative: true }); + hookItems.push({ + label: entry.commandLabel, + description, + hookEntry: entry + }); + } + } + } + // Auto-execute if only "Add new hook" is available (no existing hooks) if (hooksOfType.length === 0) { selectedHook = hookItems[0] as IHookQuickPickItem; @@ -500,22 +525,34 @@ export async function showConfigureHooksQuickPick( const entry = selectedHook.hookEntry; let selection: ITextEditorSelection | undefined; - // Determine the command field name to highlight based on target platform - const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS); - - // Try to find the command field to highlight - if (commandFieldName) { + if (entry.agentName) { + // Agent hook: search the YAML frontmatter for the command try { const content = await fileService.readFile(entry.fileUri); - selection = findHookCommandSelection( - content.value.toString(), - entry.originalHookTypeId, - entry.index, - commandFieldName - ); + const commandText = formatHookCommandLabel(entry.command, targetOS); + if (commandText) { + selection = findHookCommandInYaml(content.value.toString(), commandText); + } } catch { // Ignore errors and just open without selection } + } else { + // File hook: use JSON-based selection finder + const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS); + + if (commandFieldName) { + try { + const content = await fileService.readFile(entry.fileUri); + selection = findHookCommandSelection( + content.value.toString(), + entry.originalHookTypeId, + entry.index, + commandFieldName + ); + } catch { + // Ignore errors and just open without selection + } + } } if (options?.openEditor) { diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts index e5eac07cdb7..e019aecb93f 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts @@ -114,6 +114,54 @@ export function findHookCommandSelection(content: string, hookType: string, inde }; } +/** + * Finds the selection range for a hook command string in a YAML/Markdown file + * (e.g., an agent `.md` file with YAML frontmatter). + * + * Searches for the command text within command field lines and selects the value. + * Supports all hook command field keys: command, windows, linux, osx, bash, powershell. + * + * @param content The full file content + * @param commandText The command string to locate + * @returns The selection range, or undefined if not found + */ +export function findHookCommandInYaml(content: string, commandText: string): ITextEditorSelection | undefined { + const commandFieldKeys = ['command', 'windows', 'linux', 'osx', 'bash', 'powershell']; + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trimStart(); + + // Only match lines whose YAML key is a known command field + const matchedKey = commandFieldKeys.find(key => + trimmed.startsWith(`${key}:`) || trimmed.startsWith(`- ${key}:`) + ); + if (!matchedKey) { + continue; + } + + // Search after the colon to avoid matching within the key name itself + const colonIdx = line.indexOf(':'); + const idx = line.indexOf(commandText, colonIdx + 1); + if (idx !== -1) { + // Verify this is a full match (not a substring of a longer command) + const afterIdx = idx + commandText.length; + const charAfter = afterIdx < line.length ? line.charCodeAt(afterIdx) : -1; + // Accept if what follows is end of line, a quote, or whitespace + if (charAfter === -1 || charAfter === 34 /* " */ || charAfter === 39 /* ' */ || charAfter === 32 /* space */ || charAfter === 9 /* tab */) { + return { + startLineNumber: i + 1, + startColumn: idx + 1, + endLineNumber: i + 1, + endColumn: idx + 1 + commandText.length + }; + } + } + } + + return undefined; +} + /** * Parsed hook information. */ @@ -129,11 +177,15 @@ export interface IParsedHook { originalHookTypeId: string; /** If true, this hook is disabled via `disableAllHooks: true` in its file */ disabled?: boolean; + /** If set, this hook came from a custom agent's frontmatter */ + agentName?: string; } export interface IParseAllHookFilesOptions { /** Additional file URIs to parse (e.g., files skipped due to disableAllHooks) */ additionalDisabledFileUris?: readonly URI[]; + /** If true, also collect hooks from custom agent frontmatter */ + includeAgentHooks?: boolean; } /** @@ -227,5 +279,40 @@ export async function parseAllHookFiles( } } + // Collect hooks from custom agents' frontmatter + if (options?.includeAgentHooks) { + const agents = await promptsService.getCustomAgents(token); + for (const agent of agents) { + if (!agent.hooks) { + continue; + } + for (const hookTypeValue of Object.values(HookType)) { + const commands = agent.hooks[hookTypeValue]; + if (!commands || commands.length === 0) { + continue; + } + const hookTypeMeta = HOOK_METADATA[hookTypeValue]; + if (!hookTypeMeta) { + continue; + } + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + const commandLabel = formatHookCommandLabel(command, os) || nls.localize('commands.hook.emptyCommand', '(empty command)'); + parsedHooks.push({ + hookType: hookTypeValue, + hookTypeLabel: hookTypeMeta.label, + command, + commandLabel, + fileUri: agent.uri, + filePath: labelService.getUriLabel(agent.uri, { relative: true }), + index: i, + originalHookTypeId: hookTypeValue, + agentName: agent.name, + }); + } + } + } + } + return parsedHooks; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 498a1ef40da..a1e1d22d80c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -53,7 +53,7 @@ import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../langua import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; import { IPromptsService } from '../promptSyntax/service/promptsService.js'; -import { ChatRequestHooks } from '../promptSyntax/hookSchema.js'; +import { ChatRequestHooks, mergeHooks } from '../promptSyntax/hookSchema.js'; const serializedChatKey = 'interactive.sessions'; @@ -959,6 +959,20 @@ export class ChatService extends Disposable implements IChatService { this.logService.warn('[ChatService] Failed to collect hooks:', error); } + // Merge hooks from the selected custom agent's frontmatter (if any) + const agentName = options?.modeInfo?.modeInstructions?.name; + if (agentName) { + try { + const agents = await this.promptsService.getCustomAgents(token, model.sessionResource); + const customAgent = agents.find(a => a.name === agentName); + if (customAgent?.hooks) { + collectedHooks = mergeHooks(collectedHooks, customAgent.hooks); + } + } catch (error) { + this.logService.warn('[ChatService] Failed to collect agent hooks:', error); + } + } + const stopWatch = new StopWatch(false); store.add(token.onCancellationRequested(() => { this.trace('sendRequest', `Request for session ${model.sessionResource} was cancelled`); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts index 6776c6a2e28..9311488d61c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../base/common/uri.js'; -import { toHookType, resolveHookCommand, IHookCommand } from './hookSchema.js'; +import { toHookType, IHookCommand, extractHookCommandsFromItem } from './hookSchema.js'; import { HOOKS_BY_TARGET, HookType } from './hookTypes.js'; import { Target } from './promptTypes.js'; +export { extractHookCommandsFromItem }; + /** * Cached inverse mapping from HookType to Claude hook type name. * Lazily computed on first access. @@ -132,60 +134,4 @@ export function parseClaudeHooks( return { hooks: result, disabledAllHooks: false }; } -/** - * Helper to extract hook commands from an item that could be: - * 1. A direct command object: { type: 'command', command: '...' } - * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] } - * - * This allows Copilot format to handle Claude-style entries if pasted. - * Also handles Claude's leniency where 'type' field can be omitted. - */ -export function extractHookCommandsFromItem( - item: unknown, - workspaceRootUri: URI | undefined, - userHome: string -): IHookCommand[] { - if (!item || typeof item !== 'object') { - return []; - } - const itemObj = item as Record; - const commands: IHookCommand[] = []; - - // Check for nested hooks with matcher (Claude style): { matcher: "...", hooks: [...] } - const nestedHooks = itemObj.hooks; - if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { - for (const nestedHook of nestedHooks) { - if (!nestedHook || typeof nestedHook !== 'object') { - continue; - } - const normalized = normalizeForResolve(nestedHook as Record); - const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } - } else { - // Direct command object - const normalized = normalizeForResolve(itemObj); - const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } - - return commands; -} - -/** - * Normalizes a hook command object for resolving. - * Claude format allows omitting the 'type' field, treating it as 'command'. - * This ensures compatibility when Claude-style hooks are pasted into Copilot format. - */ -function normalizeForResolve(raw: Record): Record { - // If type is missing or already 'command', ensure it's set to 'command' - if (raw.type === undefined || raw.type === 'command') { - return { ...raw, type: 'command' }; - } - return raw; -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index 0d69dce53bf..8025b3ea675 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts @@ -12,6 +12,7 @@ import { untildify } from '../../../../../base/common/labels.js'; import { OperatingSystem } from '../../../../../base/common/platform.js'; import { HookType, HOOKS_BY_TARGET, HOOK_METADATA } from './hookTypes.js'; import { Target } from './promptTypes.js'; +import { IValue, IMapValue } from './promptFileParser.js'; /** * A single hook command configuration. @@ -46,6 +47,43 @@ export type ChatRequestHooks = { readonly [K in HookType]?: readonly IHookCommand[]; }; +/** + * Merges two sets of hooks by concatenating the command arrays for each hook type. + * Additional hooks are appended after the base hooks. + */ +export function mergeHooks(base: ChatRequestHooks | undefined, additional: ChatRequestHooks): ChatRequestHooks { + if (!base) { + return additional; + } + + const result: Partial> = { ...base }; + for (const hookType of Object.values(HookType)) { + const baseArr = base[hookType]; + const additionalArr = additional[hookType]; + if (additionalArr && additionalArr.length > 0) { + result[hookType] = baseArr ? [...baseArr, ...additionalArr] : additionalArr; + } + } + return result as ChatRequestHooks; +} + +/** + * Descriptions for hook command fields, used by both the JSON schema and the hover provider. + */ +export const HOOK_COMMAND_FIELD_DESCRIPTIONS: Record = { + type: nls.localize('hook.type', 'Must be "command".'), + command: nls.localize('hook.command', 'The command to execute. This is the default cross-platform command.'), + windows: nls.localize('hook.windows', 'Windows-specific command. If specified and running on Windows, this overrides the "command" field.'), + linux: nls.localize('hook.linux', 'Linux-specific command. If specified and running on Linux, this overrides the "command" field.'), + osx: nls.localize('hook.osx', 'macOS-specific command. If specified and running on macOS, this overrides the "command" field.'), + bash: nls.localize('hook.bash', 'Bash command for Linux and macOS.'), + powershell: nls.localize('hook.powershell', 'PowerShell command for Windows.'), + cwd: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).'), + env: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.'), + timeout: nls.localize('hook.timeout', 'Maximum execution time in seconds (default: 30).'), + timeoutSec: nls.localize('hook.timeoutSec', 'Maximum execution time in seconds (default: 10).'), +}; + /** * JSON Schema for GitHub Copilot hook configuration files. * Hooks enable executing custom shell commands at strategic points in an agent's workflow. @@ -67,37 +105,37 @@ const vscodeHookCommandSchema: IJSONSchema = { type: { type: 'string', enum: ['command'], - description: nls.localize('hook.type', 'Must be "command".') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.type }, command: { type: 'string', - description: nls.localize('hook.command', 'The command to execute. This is the default cross-platform command.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.command }, windows: { type: 'string', - description: nls.localize('hook.windows', 'Windows-specific command. If specified and running on Windows, this overrides the "command" field.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.windows }, linux: { type: 'string', - description: nls.localize('hook.linux', 'Linux-specific command. If specified and running on Linux, this overrides the "command" field.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.linux }, osx: { type: 'string', - description: nls.localize('hook.osx', 'macOS-specific command. If specified and running on macOS, this overrides the "command" field.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.osx }, cwd: { type: 'string', - description: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.cwd }, env: { type: 'object', additionalProperties: { type: 'string' }, - description: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.env }, timeout: { type: 'number', default: 30, - description: nls.localize('hook.timeout', 'Maximum execution time in seconds (default: 30).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.timeout } } }; @@ -142,29 +180,29 @@ const copilotCliHookCommandSchema: IJSONSchema = { type: { type: 'string', enum: ['command'], - description: nls.localize('hook.type', 'Must be "command".') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.type }, bash: { type: 'string', - description: nls.localize('hook.bash', 'Bash command for Linux and macOS.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.bash }, powershell: { type: 'string', - description: nls.localize('hook.powershell', 'PowerShell command for Windows.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.powershell }, cwd: { type: 'string', - description: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.cwd }, env: { type: 'object', additionalProperties: { type: 'string' }, - description: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.env }, timeoutSec: { type: 'number', default: 10, - description: nls.localize('hook.timeoutSec', 'Maximum execution time in seconds (default: 10).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.timeoutSec } } }; @@ -444,3 +482,155 @@ export function resolveHookCommand(raw: Record, workspaceRootUr ...(normalized.timeout !== undefined && { timeout: normalized.timeout }), }; } + +/** + * Helper to extract hook commands from an item that could be: + * 1. A direct command object: { type: 'command', command: '...' } + * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] } + * + * This allows Copilot format to handle Claude-style entries if pasted. + * Also handles Claude's leniency where 'type' field can be omitted. + */ +export function extractHookCommandsFromItem( + item: unknown, + workspaceRootUri: URI | undefined, + userHome: string +): IHookCommand[] { + if (!item || typeof item !== 'object') { + return []; + } + + const itemObj = item as Record; + const commands: IHookCommand[] = []; + + // Check for nested hooks with matcher (Claude style): { matcher: "...", hooks: [...] } + const nestedHooks = itemObj.hooks; + if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { + for (const nestedHook of nestedHooks) { + if (!nestedHook || typeof nestedHook !== 'object') { + continue; + } + const normalized = normalizeForResolve(nestedHook as Record); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + } else { + // Direct command object + const normalized = normalizeForResolve(itemObj); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + + return commands; +} + +/** + * Normalizes a hook command object for resolving. + * Claude format allows omitting the 'type' field, treating it as 'command'. + * This ensures compatibility when Claude-style hooks are pasted into Copilot format. + */ +function normalizeForResolve(raw: Record): Record { + // If type is missing or already 'command', ensure it's set to 'command' + if (raw.type === undefined || raw.type === 'command') { + return { ...raw, type: 'command' }; + } + return raw; +} + +/** + * Converts an {@link IValue} YAML AST node into a plain JavaScript value + * (string, array, or object) suitable for passing to hook parsing helpers. + */ +function yamlValueToPlain(value: IValue): unknown { + switch (value.type) { + case 'scalar': + return value.value; + case 'sequence': + return value.items.map(yamlValueToPlain); + case 'map': { + const obj: Record = {}; + for (const prop of value.properties) { + obj[prop.key.value] = yamlValueToPlain(prop.value); + } + return obj; + } + } +} + +/** + * Parses hooks from a subagent's YAML frontmatter `hooks` attribute. + * + * Supports two formats for hook entries: + * + * 1. **Direct command** (our format, without matcher): + * ```yaml + * hooks: + * PreToolUse: + * - type: command + * command: "./scripts/validate.sh" + * ``` + * + * 2. **Nested with matcher** (Claude Code format): + * ```yaml + * hooks: + * PreToolUse: + * - matcher: "Bash" + * hooks: + * - type: command + * command: "./scripts/validate.sh" + * ``` + * + * @param hooksMap The raw YAML map value from the `hooks` frontmatter attribute. + * @param workspaceRootUri Workspace root for resolving relative `cwd` paths. + * @param userHome User home directory path for tilde expansion. + * @param target The agent's target, used to resolve hook type names correctly. + * @returns Resolved hooks organized by hook type, ready for use in {@link ChatRequestHooks}. + */ +export function parseSubagentHooksFromYaml( + hooksMap: IMapValue, + workspaceRootUri: URI | undefined, + userHome: string, + target: Target = Target.Undefined, +): ChatRequestHooks { + const result: Record = {}; + const targetHookMap = HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]; + + for (const prop of hooksMap.properties) { + const hookTypeName = prop.key.value; + + // Resolve hook type name using the target's own map first, then fall back to canonical names + const hookType = targetHookMap[hookTypeName] ?? toHookType(hookTypeName); + if (!hookType) { + continue; + } + + // The value must be a sequence (array of hook entries) + if (prop.value.type !== 'sequence') { + continue; + } + + const commands: IHookCommand[] = []; + + for (const item of prop.value.items) { + // Convert the YAML AST node to a plain object so the existing + // extractHookCommandsFromItem helper can handle both direct + // commands and nested matcher structures. + const plainItem = yamlValueToPlain(item); + const extracted = extractHookCommandsFromItem(plainItem, workspaceRootUri, userHome); + commands.push(...extracted); + } + + if (commands.length > 0) { + if (!result[hookType]) { + result[hookType] = []; + } + result[hookType].push(...commands); + } + } + + return result as ChatRequestHooks; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts index 2e99a13df82..a57216523c9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts @@ -167,6 +167,10 @@ export const customAgentAttributes: Record = { type: 'map', description: localize('promptHeader.agent.github', 'GitHub-specific configuration for the agent, such as token permissions.'), }, + [PromptHeaderAttributes.hooks]: { + type: 'map', + description: localize('promptHeader.agent.hooks', 'Lifecycle hooks scoped to this agent. Define hooks that run only while this agent is active.'), + }, }; // Attribute metadata for skill files (`SKILL.md`). diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 14009b90d44..4884aed9fa8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -15,10 +15,12 @@ import { IChatModeService } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; -import { ISequenceValue, IValue, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { IMapValue, ISequenceValue, IValue, IHeaderAttribute, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { getAttributeDefinition, getTarget, getValidAttributeNames, knownClaudeTools, knownGithubCopilotTools, IValueEntry, ClaudeHeaderAttributes, } from './promptFileAttributes.js'; import { localize } from '../../../../../../nls.js'; import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.js'; +import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; +import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; export class PromptHeaderAutocompletion implements CompletionItemProvider { /** @@ -91,6 +93,33 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { const colonPosition = colonIndex !== -1 ? new Position(position.lineNumber, colonIndex + 1) : undefined; if (!colonPosition || position.isBeforeOrEqual(colonPosition)) { + // Check if the position is inside a multi-line attribute (e.g., hooks map). + // In that case, provide value completions for that attribute instead of attribute name completions. + let containingAttribute = header.attributes.find(({ range }) => + range.startLineNumber < position.lineNumber && position.lineNumber <= range.endLineNumber); + if (!containingAttribute) { + // Handle trailing empty lines after a map-valued attribute: + // The YAML parser's range ends at the last parsed child, but logically + // an empty line before the next attribute still belongs to the map. + for (let i = header.attributes.length - 1; i >= 0; i--) { + const attr = header.attributes[i]; + if (attr.range.endLineNumber < position.lineNumber && attr.value.type === 'map') { + const nextAttr = header.attributes[i + 1]; + const nextStartLine = nextAttr ? nextAttr.range.startLineNumber : headerRange.endLineNumber; + if (position.lineNumber < nextStartLine) { + containingAttribute = attr; + } + break; + } + } + } + if (containingAttribute) { + const attrLineText = model.getLineContent(containingAttribute.range.startLineNumber); + const attrColonIndex = attrLineText.indexOf(':'); + if (attrColonIndex !== -1) { + return this.provideValueCompletions(model, position, header, new Position(containingAttribute.range.startLineNumber, attrColonIndex + 1), promptType, containingAttribute); + } + } return this.provideAttributeNameCompletions(model, position, header, colonPosition, promptType); } else if (colonPosition && colonPosition.isBefore(position)) { return this.provideValueCompletions(model, position, header, colonPosition, promptType); @@ -116,6 +145,11 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { if (colonPosition) { return key; } + // For map-valued attributes, insert a snippet with the nested structure + if (key === PromptHeaderAttributes.hooks && promptType === PromptsType.agent && target !== Target.Claude) { + const hookNames = Object.keys(HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]); + return `${key}:\n \${1|${hookNames.join(',')}|}:\n - type: command\n command: "$2"`; + } const valueSuggestions = this.getValueSuggestions(promptType, key, target); if (valueSuggestions.length > 0) { return `${key}: \${0:${valueSuggestions[0].name}}`; @@ -146,10 +180,11 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { header: PromptHeader, colonPosition: Position, promptType: PromptsType, + preFoundAttribute?: IHeaderAttribute, ): Promise { const suggestions: CompletionItem[] = []; const posLineNumber = position.lineNumber; - const attribute = header.attributes.find(({ range }) => range.startLineNumber <= posLineNumber && posLineNumber <= range.endLineNumber); + const attribute = preFoundAttribute ?? header.attributes.find(({ range }) => range.startLineNumber <= posLineNumber && posLineNumber <= range.endLineNumber); if (!attribute) { return undefined; } @@ -200,6 +235,18 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { }); } } + if (attribute.key === PromptHeaderAttributes.hooks) { + if (attribute.value.type === 'map') { + // Inside the hooks map — suggest hook event type names as sub-keys + return this.provideHookEventCompletions(model, position, attribute.value, target); + } + // When hooks value is not yet a map (e.g., user is mid-edit on a nested line), + // still provide hook event completions with no existing keys. + if (position.lineNumber !== attribute.range.startLineNumber) { + const emptyMap: IMapValue = { type: 'map', properties: [], range: attribute.value.range }; + return this.provideHookEventCompletions(model, position, emptyMap, target); + } + } const lineContent = model.getLineContent(attribute.range.startLineNumber); const whilespaceAfterColon = (lineContent.substring(colonPosition.column).match(/^\s*/)?.[0].length) ?? 0; const entries = this.getValueSuggestions(promptType, attribute.key, target); @@ -229,9 +276,290 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { }; suggestions.push(item); } + if (attribute.key === PromptHeaderAttributes.hooks && promptType === PromptsType.agent) { + const hookSnippet = [ + '', + ' ${1|' + Object.keys(HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]).join(',') + '|}:', + ' - type: command', + ' command: "$2"' + ].join('\n'); + const item: CompletionItem = { + label: localize('promptHeaderAutocompletion.newHook', "New Hook"), + kind: CompletionItemKind.Snippet, + insertText: whilespaceAfterColon === 0 ? ` ${hookSnippet}` : hookSnippet, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, colonPosition.column + whilespaceAfterColon + 1, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }; + suggestions.push(item); + } return { suggestions }; } + /** + * Provides completions inside the `hooks:` map. + * Determines what to suggest based on nesting depth: + * - At hook event level: suggest event names (SessionStart, PreToolUse, etc.) + * - Inside a command object: suggest command fields (type, command, timeout, etc.) + */ + private provideHookEventCompletions( + model: ITextModel, + position: Position, + hooksMap: IMapValue, + target: Target, + ): CompletionList | undefined { + // Check if the cursor is on the value side of an existing hook event key (e.g., "SessionEnd:|") + // In that case, offer a command entry snippet instead of event name completions. + const hookEventOnLine = hooksMap.properties.find(p => p.key.range.startLineNumber === position.lineNumber); + if (hookEventOnLine) { + const lineText = model.getLineContent(position.lineNumber); + const colonIdx = lineText.indexOf(':'); + if (colonIdx !== -1 && position.column > colonIdx + 1) { + const whilespaceAfterColon = (lineText.substring(colonIdx + 1).match(/^\s*/)?.[0].length) ?? 0; + const commandSnippet = [ + '', + ' - type: command', + ' command: "$1"', + ].join('\n'); + return { + suggestions: [{ + label: localize('promptHeaderAutocompletion.newCommand', "New Command"), + documentation: localize('promptHeaderAutocompletion.newCommand.description', "Add a new command entry to this hook."), + kind: CompletionItemKind.Snippet, + insertText: whilespaceAfterColon === 0 ? ` ${commandSnippet}` : commandSnippet, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, colonIdx + 1 + whilespaceAfterColon + 1, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }] + }; + } + } + + // Try to provide command field completions if cursor is inside a command object + const commandFieldCompletions = this.provideHookCommandFieldCompletions(model, position, hooksMap, target); + if (commandFieldCompletions) { + return commandFieldCompletions; + } + + // Otherwise provide hook event name completions + const suggestions: CompletionItem[] = []; + const hooksByTarget = HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]; + + const lineText = model.getLineContent(position.lineNumber); + const firstNonWhitespace = lineText.search(/\S/); + const isEmptyLine = firstNonWhitespace === -1; + // Start the range after leading whitespace so VS Code's completion + // filtering matches the hook name prefix the user has typed. + const rangeStartColumn = isEmptyLine ? position.column : firstNonWhitespace + 1; + + // Exclude hook keys on the current line so the user sees all options while editing a key + const existingKeys = new Set( + hooksMap.properties + .filter(p => p.key.range.startLineNumber !== position.lineNumber) + .map(p => p.key.value) + ); + + // Supplement with text-based scanning: when incomplete YAML causes the + // parser to drop subsequent keys, scan the model for lines that look + // like hook event entries (e.g., " UserPromptSubmit:") at the expected + // indentation. + const expectedIndent = hooksMap.properties.length > 0 + ? hooksMap.properties[0].key.range.startColumn - 1 + : -1; + if (expectedIndent >= 0) { + const scanEnd = model.getLineCount(); + for (let lineNum = hooksMap.range.endLineNumber + 1; lineNum <= scanEnd; lineNum++) { + if (lineNum === position.lineNumber) { + continue; + } + const lt = model.getLineContent(lineNum); + const lineIndent = lt.search(/\S/); + if (lineIndent === -1) { + continue; + } + if (lineIndent < expectedIndent) { + break; // Left the hooks map scope + } + if (lineIndent === expectedIndent) { + const match = lt.match(/^\s+(\S+)\s*:/); + if (match) { + existingKeys.add(match[1]); + } + } + } + } + + // Check whether the current line already has a colon (editing an existing key) + const lineHasColon = lineText.indexOf(':') !== -1; + + for (const [hookName, hookType] of Object.entries(hooksByTarget)) { + if (existingKeys.has(hookName)) { + continue; + } + const meta = HOOK_METADATA[hookType]; + let insertText: string; + if (isEmptyLine) { + // On empty lines, insert a full hook snippet with command placeholder + insertText = [ + `${hookName}:`, + ` - type: command`, + ` command: "$1"`, + ].join('\n'); + } else if (lineHasColon) { + // On existing key lines, only replace the key name to preserve nested content + insertText = `${hookName}:`; + } else { + // Typing a new event name — omit the colon so the user can + // trigger the next completion (e.g., New Command snippet) by typing ':' + insertText = hookName; + } + suggestions.push({ + label: hookName, + documentation: meta?.description, + kind: CompletionItemKind.Property, + insertText, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, rangeStartColumn, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }); + } + + return { suggestions }; + } + + /** + * Provides completions for hook command fields (type, command, windows, etc.) + * when the cursor is inside a command object within the hooks map. + * Detects nesting by checking if the position falls within a sequence item + * of a hook event's value. + */ + private provideHookCommandFieldCompletions( + model: ITextModel, + position: Position, + hooksMap: IMapValue, + target: Target, + ): CompletionList | undefined { + // Find which hook event's command list the cursor is in + const containingCommandMap = this.findContainingCommandMap(model, position, hooksMap); + if (!containingCommandMap) { + return undefined; + } + + const isCopilotCli = target === Target.GitHubCopilot; + const validFields = isCopilotCli + ? ['type', 'bash', 'powershell', 'cwd', 'env', 'timeoutSec'] + : ['type', 'command', 'windows', 'linux', 'osx', 'bash', 'powershell', 'cwd', 'env', 'timeout']; + + const existingFields = new Set( + containingCommandMap.properties + .filter(p => p.key.range.startLineNumber !== position.lineNumber) + .map(p => p.key.value) + ); + + const lineText = model.getLineContent(position.lineNumber); + const firstNonWhitespace = lineText.search(/\S/); + const isEmptyLine = firstNonWhitespace === -1; + // Skip past the YAML sequence indicator `- ` so the range starts at the + // actual field name; otherwise VS Code's completion filter would see the + // `- ` prefix and reject valid field names. + const dashPrefixMatch = lineText.match(/^(\s*-\s+)/); + const fieldStart = dashPrefixMatch ? dashPrefixMatch[1].length : firstNonWhitespace; + const rangeStartColumn = isEmptyLine ? position.column : fieldStart + 1; + const colonIndex = lineText.indexOf(':'); + + const suggestions: CompletionItem[] = []; + for (const fieldName of validFields) { + if (existingFields.has(fieldName)) { + continue; + } + const desc = HOOK_COMMAND_FIELD_DESCRIPTIONS[fieldName]; + const insertText = colonIndex !== -1 ? fieldName : `${fieldName}: $0`; + suggestions.push({ + label: fieldName, + documentation: desc, + kind: CompletionItemKind.Property, + insertText, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, rangeStartColumn, position.lineNumber, colonIndex !== -1 ? colonIndex + 1 : model.getLineMaxColumn(position.lineNumber)), + }); + } + + return suggestions.length > 0 ? { suggestions } : undefined; + } + + /** + * Walks the hooks map AST to find the command map object containing the position. + * Handles both direct command objects and nested matcher format. + * Also handles trailing lines after the last parsed property of a command map. + */ + private findContainingCommandMap(model: ITextModel, position: Position, hooksMap: IMapValue): IMapValue | undefined { + for (let i = 0; i < hooksMap.properties.length; i++) { + const prop = hooksMap.properties[i]; + if (prop.value.type !== 'sequence') { + continue; + } + // Check if cursor is within the sequence's range, or on a trailing line after it + const seqRange = prop.value.range; + const nextProp = hooksMap.properties[i + 1]; + const isInSeq = seqRange.containsPosition(position); + const isTrailingSeq = !isInSeq + && seqRange.endLineNumber < position.lineNumber + && (!nextProp || nextProp.key.range.startLineNumber > position.lineNumber); + + if (isInSeq || isTrailingSeq) { + // For trailing lines, verify the cursor is indented deeper than + // the hook event key — otherwise it belongs to the parent map. + if (isTrailingSeq) { + const lineText = model.getLineContent(position.lineNumber); + const firstNonWs = lineText.search(/\S/); + const effectiveIndent = firstNonWs === -1 ? position.column - 1 : firstNonWs; + const hookKeyIndent = prop.key.range.startColumn - 1; + if (effectiveIndent <= hookKeyIndent) { + continue; + } + } + const result = this.findCommandMapInSequence(position, prop.value); + if (result) { + return result; + } + } + } + return undefined; + } + + private findCommandMapInSequence(position: Position, sequence: ISequenceValue): IMapValue | undefined { + for (let i = 0; i < sequence.items.length; i++) { + const item = sequence.items[i]; + if (item.type !== 'map') { + // Handle partial typing: a scalar on the cursor line means the user + // is starting to type a command entry (e.g., "- t"). + if (item.type === 'scalar' && item.range.startLineNumber === position.lineNumber) { + return { type: 'map', properties: [], range: item.range }; + } + continue; + } + + // Check if position is within or just after this map item's parsed range. + // The parser's range may not include a trailing line being typed. + const isInRange = item.range.containsPosition(position); + const isTrailing = !isInRange + && item.range.endLineNumber < position.lineNumber + && (i + 1 >= sequence.items.length || sequence.items[i + 1].range.startLineNumber > position.lineNumber); + + if (!isInRange && !isTrailing) { + continue; + } + + // Check for nested matcher format: { hooks: [...] } + const nestedHooks = item.properties.find(p => p.key.value === 'hooks'); + if (nestedHooks?.value.type === 'sequence') { + const result = this.findCommandMapInSequence(position, nestedHooks.value); + if (result) { + return result; + } + } + return item; + } + return undefined; + } + private getValueSuggestions(promptType: PromptsType, attribute: string, target: Target): readonly IValueEntry[] { const attributeDesc = getAttributeDefinition(attribute, promptType, target); if (attributeDesc?.enums) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 273ceef3be0..00065d1b40c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -15,8 +15,10 @@ import { ILanguageModelToolsService, isToolSet, IToolSet } from '../../tools/lan import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { IHeaderAttribute, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { IHeaderAttribute, ISequenceValue, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { ClaudeHeaderAttributes, getAttributeDefinition, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptFileAttributes.js'; +import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; +import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -86,6 +88,8 @@ export class PromptHoverProvider implements HoverProvider { return this.getAgentHover(attribute, position, description); case PromptHeaderAttributes.handOffs: return this.getHandsOffHover(attribute, position, target); + case PromptHeaderAttributes.hooks: + return this.getHooksHover(attribute, position, description, target); case PromptHeaderAttributes.infer: return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', 'Deprecated: Use `user-invocable` and `disable-model-invocation` instead.'), attribute.range); default: @@ -232,6 +236,62 @@ export class PromptHoverProvider implements HoverProvider { return this.createHover(lines.join('\n'), agentAttribute.range); } + private getHooksHover(attribute: IHeaderAttribute, position: Position, baseMessage: string, target: Target): Hover | undefined { + const value = attribute.value; + if (value.type === 'map') { + const hooksByTarget = HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]; + for (const prop of value.properties) { + // Hover on a hook event name key (e.g., SessionStart, PreToolUse) + if (prop.key.range.containsPosition(position)) { + const hookType = hooksByTarget[prop.key.value]; + if (hookType) { + const meta = HOOK_METADATA[hookType]; + return this.createHover(`**${meta.label}**\n\n${meta.description}`, prop.key.range); + } + } + // Hover inside hook command entries + if (prop.value.type === 'sequence') { + const hover = this.getHookCommandItemHover(prop.value, position); + if (hover) { + return hover; + } + } + } + } + return this.createHover(baseMessage, attribute.range); + } + + /** + * Recursively searches hook command items for hover information. + * Handles both direct command objects and nested matcher format + * (e.g., `{ matcher: "...", hooks: [{ type: command, ... }] }`). + */ + private getHookCommandItemHover(sequence: ISequenceValue, position: Position): Hover | undefined { + for (const item of sequence.items) { + if (item.type !== 'map' || !item.range.containsPosition(position)) { + continue; + } + // Check for nested matcher format: { hooks: [...] } + const nestedHooks = item.properties.find(p => p.key.value === 'hooks'); + if (nestedHooks && nestedHooks.value.type === 'sequence') { + const hover = this.getHookCommandItemHover(nestedHooks.value, position); + if (hover) { + return hover; + } + } + // Check fields of the command object itself + for (const field of item.properties) { + if (field.key.range.containsPosition(position) || field.value.range.containsPosition(position)) { + const desc = HOOK_COMMAND_FIELD_DESCRIPTIONS[field.key.value]; + if (desc) { + return this.createHover(desc, field.key.range); + } + } + } + } + return undefined; + } + private getHandsOffHover(attribute: IHeaderAttribute, position: Position, target: Target): Hover | undefined { const handoffsBaseMessage = getAttributeDefinition(PromptHeaderAttributes.handOffs, PromptsType.agent, target)?.description!; if (!isVSCodeOrDefaultTarget(target)) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 569e024d3c8..d6e71491d01 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -28,6 +28,7 @@ import { Lazy } from '../../../../../../base/common/lazy.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { HOOKS_BY_TARGET } from '../hookTypes.js'; import { GithubPromptHeaderAttributes } from './promptFileAttributes.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -191,6 +192,7 @@ export class PromptValidator { this.validateUserInvokable(attributes, report); this.validateDisableModelInvocation(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, target, report); + this.validateHooks(attributes, target, report); if (isVSCodeOrDefaultTarget(target)) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); @@ -545,6 +547,119 @@ export class PromptValidator { } } + private validateHooks(attributes: IHeaderAttribute[], target: Target, report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.hooks); + if (!attribute) { + return; + } + if (attribute.value.type !== 'map') { + report(toMarker(localize('promptValidator.hooksMustBeMap', "The 'hooks' attribute must be a map of hook event types to command arrays."), attribute.value.range, MarkerSeverity.Error)); + return; + } + const validHookNames = new Set(Object.keys(HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined])); + for (const prop of attribute.value.properties) { + if (!validHookNames.has(prop.key.value)) { + report(toMarker(localize('promptValidator.unknownHookType', "Unknown hook event type '{0}'. Supported: {1}.", prop.key.value, Array.from(validHookNames).join(', ')), prop.key.range, MarkerSeverity.Warning)); + } + if (prop.value.type !== 'sequence') { + report(toMarker(localize('promptValidator.hookValueMustBeArray', "Hook event '{0}' must have an array of command objects as its value.", prop.key.value), prop.value.range, MarkerSeverity.Error)); + continue; + } + for (const item of prop.value.items) { + this.validateHookCommand(item, target, report); + } + } + } + + private validateHookCommand(item: IValue, target: Target, report: (markers: IMarkerData) => void): void { + if (item.type !== 'map') { + report(toMarker(localize('promptValidator.hookCommandMustBeObject', "Each hook command must be an object."), item.range, MarkerSeverity.Error)); + return; + } + + // Detect nested matcher format: { matcher?: "...", hooks: [{ type: 'command', command: '...' }] } + const hooksProperty = item.properties.find(p => p.key.value === 'hooks'); + if (hooksProperty) { + // Validate that only known matcher properties are present + for (const prop of item.properties) { + if (prop.key.value !== 'hooks' && prop.key.value !== 'matcher') { + report(toMarker(localize('promptValidator.unknownMatcherProperty', "Unknown property '{0}' in hook matcher.", prop.key.value), prop.key.range, MarkerSeverity.Warning)); + } + } + if (hooksProperty.value.type !== 'sequence') { + report(toMarker(localize('promptValidator.nestedHooksMustBeArray', "The 'hooks' property in a matcher must be an array of command objects."), hooksProperty.value.range, MarkerSeverity.Error)); + return; + } + for (const nestedItem of hooksProperty.value.items) { + this.validateHookCommand(nestedItem, target, report); + } + return; + } + + const isCopilotCli = target === Target.GitHubCopilot; + + // Determine valid and command-providing properties based on target + const validCommandFields = isCopilotCli + ? new Set(['bash', 'powershell']) + : new Set(['command', 'windows', 'linux', 'osx', 'bash', 'powershell']); + + const validProperties = isCopilotCli + ? new Set(['type', 'bash', 'powershell', 'cwd', 'env', 'timeoutSec']) + : new Set(['type', 'command', 'windows', 'linux', 'osx', 'bash', 'powershell', 'cwd', 'env', 'timeout']); + + let hasType = false; + let hasCommandField = false; + + for (const prop of item.properties) { + const key = prop.key.value; + + if (!validProperties.has(key)) { + report(toMarker(localize('promptValidator.unknownHookProperty', "Unknown property '{0}' in hook command.", key), prop.key.range, MarkerSeverity.Warning)); + } + + if (key === 'type') { + hasType = true; + if (prop.value.type !== 'scalar' || prop.value.value !== 'command') { + report(toMarker(localize('promptValidator.hookTypeMustBeCommand', "The 'type' property in a hook command must be 'command'."), prop.value.range, MarkerSeverity.Error)); + } + } else if (validCommandFields.has(key)) { + hasCommandField = true; + if (prop.value.type !== 'scalar' || prop.value.value.trim().length === 0) { + report(toMarker(localize('promptValidator.hookCommandFieldMustBeNonEmptyString', "The '{0}' property in a hook command must be a non-empty string.", key), prop.value.range, MarkerSeverity.Error)); + } + } else if (key === 'cwd') { + if (prop.value.type !== 'scalar') { + report(toMarker(localize('promptValidator.hookCwdMustBeString', "The 'cwd' property in a hook command must be a string."), prop.value.range, MarkerSeverity.Error)); + } + } else if (key === 'env') { + if (prop.value.type !== 'map') { + report(toMarker(localize('promptValidator.hookEnvMustBeMap', "The 'env' property in a hook command must be a map of string values."), prop.value.range, MarkerSeverity.Error)); + } else { + for (const envProp of prop.value.properties) { + if (envProp.value.type !== 'scalar') { + report(toMarker(localize('promptValidator.hookEnvValueMustBeString', "Environment variable '{0}' must have a string value.", envProp.key.value), envProp.value.range, MarkerSeverity.Error)); + } + } + } + } else if (key === 'timeout' || key === 'timeoutSec') { + if (prop.value.type !== 'scalar' || isNaN(Number(prop.value.value))) { + report(toMarker(localize('promptValidator.hookTimeoutMustBeNumber', "The '{0}' property in a hook command must be a number.", key), prop.value.range, MarkerSeverity.Error)); + } + } + } + + if (!hasType) { + report(toMarker(localize('promptValidator.hookMissingType', "Hook command is missing required property 'type'."), item.range, MarkerSeverity.Error)); + } + if (!hasCommandField) { + if (isCopilotCli) { + report(toMarker(localize('promptValidator.hookMissingCopilotCommand', "Hook command must specify at least one of 'bash' or 'powershell'."), item.range, MarkerSeverity.Error)); + } else { + report(toMarker(localize('promptValidator.hookMissingCommand', "Hook command must specify at least one of 'command', 'windows', 'linux', or 'osx'."), item.range, MarkerSeverity.Error)); + } + } + } + private validateHandoffs(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.handOffs); if (!attribute) { @@ -761,7 +876,7 @@ function isTrueOrFalse(value: IValue): boolean { const allAttributeNames: Record = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], - [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github], + [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.hooks, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github], [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation], [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter }; @@ -846,6 +961,8 @@ export function getAttributeDescription(attributeName: string, promptType: Promp return localize('promptHeader.agent.infer', 'Controls visibility of the agent.'); case PromptHeaderAttributes.agents: return localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'); + case PromptHeaderAttributes.hooks: + return localize('promptHeader.agent.hooks', 'Lifecycle hooks scoped to this agent. Define hooks that run only while this agent is active.'); case PromptHeaderAttributes.userInvocable: return localize('promptHeader.agent.userInvocable', 'Whether the agent can be selected and invoked by users in the UI.'); case PromptHeaderAttributes.disableModelInvocation: diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 3d04a7cfec3..240265152cf 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -84,6 +84,7 @@ export namespace PromptHeaderAttributes { export const userInvokable = 'user-invokable'; export const userInvocable = 'user-invocable'; export const disableModelInvocation = 'disable-model-invocation'; + export const hooks = 'hooks'; } export class PromptHeader { @@ -317,6 +318,20 @@ export class PromptHeader { return this.getBooleanAttribute(PromptHeaderAttributes.disableModelInvocation); } + /** + * Gets the raw 'hooks' attribute value from the header. + * Returns the YAML map value if present, or undefined. The caller is + * responsible for converting this to `ChatRequestHooks` via + * {@link parseSubagentHooksFromYaml}. + */ + public get hooksRaw(): IMapValue | undefined { + const attr = this._parsedHeader.attributes.find(a => a.key === PromptHeaderAttributes.hooks); + if (attr?.value.type === 'map') { + return attr.value; + } + return undefined; + } + private getBooleanAttribute(key: string): boolean | undefined { const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); if (attribute?.value.type === 'scalar') { 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 b4de6bbc2e4..3e04c2fed58 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -225,6 +225,11 @@ export interface ICustomAgent { */ readonly agents?: readonly string[]; + /** + * Lifecycle hooks scoped to this subagent. + */ + readonly hooks?: ChatRequestHooks; + /** * Where the agent was loaded from. */ 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 c628f10fcf7..688ad88d715 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -36,7 +36,7 @@ import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../p import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPluginPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, IPromptSourceFolderResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, IPromptDiscoveryLogEntry } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { ChatRequestHooks, IHookCommand } from '../hookSchema.js'; +import { ChatRequestHooks, IHookCommand, parseSubagentHooksFromYaml } from '../hookSchema.js'; import { HookType } from '../hookTypes.js'; import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; @@ -678,6 +678,12 @@ export class PromptsService extends Disposable implements IPromptsService { let agentFiles = await this.listPromptFiles(PromptsType.agent, token); const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent); agentFiles = agentFiles.filter(promptPath => !disabledAgents.has(promptPath.uri)); + + // Get user home for tilde expansion in hook cwd paths + const userHomeUri = await this.pathService.userHome(); + const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path; + const defaultFolder = this.workspaceService.getWorkspace().folders[0]; + const customAgentsResults = await Promise.allSettled( agentFiles.map(async (promptPath): Promise => { const uri = promptPath.uri; @@ -733,7 +739,17 @@ export class PromptsService extends Disposable implements IPromptsService { if (target === Target.Claude && tools) { tools = mapClaudeTools(tools); } - return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source }; + + // Parse hooks from the frontmatter if present + let hooks: ChatRequestHooks | undefined; + const hooksRaw = ast.header.hooksRaw; + if (hooksRaw) { + const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder; + const workspaceRootUri = hookWorkspaceFolder?.uri; + hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target); + } + + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, hooks, agentInstructions, source }; }) ); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index e5fd0a7a691..c5931a7d732 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -23,7 +23,8 @@ import { ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; -import { ChatRequestHooks } from '../../promptSyntax/hookSchema.js'; +import { ChatRequestHooks, mergeHooks } from '../../promptSyntax/hookSchema.js'; +import { HookType } from '../../promptSyntax/hookTypes.js'; import { ICustomAgent, IPromptsService } from '../../promptSyntax/service/promptsService.js'; import { isBuiltinAgent } from '../../promptSyntax/utils/promptsServiceUtils.js'; import { @@ -260,6 +261,20 @@ export class RunSubagentTool extends Disposable implements IToolImpl { this.logService.warn('[ChatService] Failed to collect hooks:', error); } + // Merge subagent-level hooks (from the agent's frontmatter) with global hooks. + // Remap Stop hooks to SubagentStop since the agent is running as a subagent. + if (subagent?.hooks) { + const remapped: ChatRequestHooks = { ...subagent.hooks }; + if (remapped[HookType.Stop]) { + const stopHooks = remapped[HookType.Stop]; + (remapped as Record)[HookType.SubagentStop] = remapped[HookType.SubagentStop] + ? [...remapped[HookType.SubagentStop], ...stopHooks] + : stopHooks; + (remapped as Record)[HookType.Stop] = undefined; + } + collectedHooks = mergeHooks(collectedHooks, remapped); + } + // Build the agent request const agentRequest: IChatAgentRequest = { sessionResource: invocation.context.sessionResource, diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts index d75ce8adbb8..33cbbd85c1a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { findHookCommandSelection } from '../../../browser/promptSyntax/hookUtils.js'; +import { findHookCommandInYaml, findHookCommandSelection } from '../../../browser/promptSyntax/hookUtils.js'; import { ITextEditorSelection } from '../../../../../../platform/editor/common/editor.js'; import { buildNewHookEntry, HookSourceFormat } from '../../../common/promptSyntax/hookCompatibility.js'; @@ -722,4 +722,232 @@ suite('hookUtils', () => { }); }); }); + + suite('findHookCommandInYaml', () => { + + test('finds unquoted command value', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + assert.deepStrictEqual(result, { + startLineNumber: 4, + startColumn: 16, + endLineNumber: 4, + endColumn: 26 + }); + }); + + test('finds double-quoted command value', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: "echo hello"', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + }); + + test('finds single-quoted command value', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ` - command: 'echo hello'`, + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + }); + + test('finds command without list prefix', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' command: run-lint', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'run-lint'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'run-lint'); + }); + + test('does not match substring of a longer command', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello-world', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when command is not found', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo goodbye'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when no command lines exist', () => { + const content = [ + '---', + 'name: my-agent', + 'description: An agent', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for empty content', () => { + const result = findHookCommandInYaml('', 'echo hello'); + assert.strictEqual(result, undefined); + }); + + test('finds first matching command when multiple exist', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + ' userPromptSubmit:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(result.startLineNumber, 4); + }); + + test('ignores lines that are not command fields', () => { + const content = [ + '---', + 'description: run command echo hello', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(result.startLineNumber, 5); + }); + + test('handles command with special characters', () => { + const content = [ + '---', + 'hooks:', + ' preToolUse:', + ' - command: echo "foo" > /tmp/out.txt', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo "foo" > /tmp/out.txt'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo "foo" > /tmp/out.txt'); + }); + + test('matches command followed by trailing whitespace', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello ', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + }); + + test('finds short command that is a substring of the key name', () => { + const content = [ + 'hooks:', + ' Stop:', + ' - timeout: 10', + ' command: "a"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'a'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'a'); + assert.strictEqual(result.startLineNumber, 4); + }); + + test('finds short command in bash field that is a substring of the key name', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - bash: "a"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'a'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'a'); + assert.strictEqual(result.startLineNumber, 3); + }); + + test('finds command in powershell field', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - powershell: "echo hello"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + assert.strictEqual(result.startLineNumber, 3); + }); + + test('finds command in windows field', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - windows: "dir"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'dir'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'dir'); + assert.strictEqual(result.startLineNumber, 3); + }); + + test('finds command in linux and osx fields', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - linux: "ls"', + ' osx: "ls -G"', + ' type: command', + ].join('\n'); + const linuxResult = findHookCommandInYaml(content, 'ls'); + assert.ok(linuxResult); + assert.strictEqual(getSelectedText(content, linuxResult), 'ls'); + assert.strictEqual(linuxResult.startLineNumber, 3); + + const osxResult = findHookCommandInYaml(content, 'ls -G'); + assert.ok(osxResult); + assert.strictEqual(getSelectedText(content, osxResult), 'ls -G'); + assert.strictEqual(osxResult.startLineNumber, 4); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index 1daf164ddcd..4ba131c8950 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -143,6 +143,7 @@ suite('PromptHeaderAutocompletion', () => { { label: 'disable-model-invocation', result: 'disable-model-invocation: ${0:true}' }, { label: 'github', result: 'github: $0' }, { label: 'handoffs', result: 'handoffs: $0' }, + { label: 'hooks', result: 'hooks:\n ${1|SessionStart,SessionEnd,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop,ErrorOccurred|}:\n - type: command\n command: "$2"' }, { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, { label: 'name', result: 'name: $0' }, { label: 'target', result: 'target: ${0:vscode}' }, @@ -390,6 +391,249 @@ suite('PromptHeaderAutocompletion', () => { const labels = actual.map(a => a.label); assert.ok(!labels.includes('BG Agent Model (copilot)'), 'Models with targetChatSessionType should be excluded from agent model array completions'); }); + + test('complete hooks value with New Hook snippet', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual, [ + { + label: 'New Hook', + result: 'hooks: \n ${1|SessionStart,SessionEnd,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop,ErrorOccurred|}:\n - type: command\n command: "$2"' + }, + ]); + }); + + test('complete hooks value with New Hook snippet for vscode target', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'hooks: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual, [ + { + label: 'New Hook', + result: 'hooks: \n ${1|SessionStart,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop|}:\n - type: command\n command: "$2"' + }, + ]); + }); + + test('complete hook event names inside hooks map', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: "echo hi"', + ' |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + // SessionStart should be excluded since it already exists + assert.ok(!labels.includes('SessionStart'), 'SessionStart should not be suggested when already present'); + assert.ok(labels.includes('SessionEnd'), 'SessionEnd should be suggested'); + assert.ok(labels.includes('PreToolUse'), 'PreToolUse should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + }); + + test('complete hook event names for vscode target excludes existing hooks', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: "echo hi"', + ' PreToolUse:', + ' - type: command', + ' command: "lint"', + ' |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + assert.ok(!labels.includes('SessionStart'), 'SessionStart should not be suggested when already present'); + assert.ok(!labels.includes('PreToolUse'), 'PreToolUse should not be suggested when already present'); + assert.ok(labels.includes('UserPromptSubmit'), 'UserPromptSubmit should be suggested'); + assert.ok(labels.includes('PostToolUse'), 'PostToolUse should be suggested'); + // SessionEnd is not available for vscode target + assert.ok(!labels.includes('SessionEnd'), 'SessionEnd should not be available for vscode target'); + }); + + test('complete hook event names on empty line before existing hooks', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' |', + ' SessionStart:', + ' - type: command', + ' command: "echo hi"', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + assert.ok(!labels.includes('SessionStart'), 'SessionStart should not be suggested when already present'); + assert.ok(labels.includes('SessionEnd'), 'SessionEnd should be suggested'); + assert.ok(labels.includes('PreToolUse'), 'PreToolUse should be suggested'); + }); + + test('complete hook event names while editing existing key name', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' S|:', + ' - type: command', + ' command: "echo hi"', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + assert.ok(labels.includes('SessionStart'), 'SessionStart should be suggested'); + assert.ok(labels.includes('SubagentStart'), 'SubagentStart should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + // Verify insertText only replaces the key (no full snippet) + const sessionStartItem = actual.find(a => a.label === 'SessionStart'); + assert.ok(sessionStartItem); + assert.strictEqual(sessionStartItem.result, ' SessionStart:'); + }); + + test('hooks: cursor right after colon triggers New Hook snippet', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('New Hook'), 'New Hook snippet should be suggested'); + }); + + test('hooks: typing event name on next line triggers hook events', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' S|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('SessionStart'), 'SessionStart should be suggested'); + assert.ok(labels.includes('SessionEnd'), 'SessionEnd should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + }); + + test('typing field name in first command entry triggers command fields', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - t|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('type'), 'type should be suggested'); + assert.ok(labels.includes('command'), 'command should be suggested'); + assert.ok(labels.includes('timeout'), 'timeout should be suggested'); + }); + + test('typing field name after existing field triggers remaining command fields', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - type: command', + ' c|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('command'), 'command should be suggested'); + assert.ok(labels.includes('cwd'), 'cwd should be suggested'); + assert.ok(!labels.includes('type'), 'type should not be suggested when already present'); + }); + + test('typing event name after existing hook triggers hook events', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - type: command', + ' command: echo "Session ended."', + ' U|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('UserPromptSubmit'), 'UserPromptSubmit should be suggested'); + assert.ok(!labels.includes('SessionEnd'), 'SessionEnd should not be suggested when already present'); + }); + + test('typing event name between existing hooks triggers hook events', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - type: command', + ' command: echo "Session ended."', + ' S|', + ' UserPromptSubmit:', + ' - type: command', + ' command: echo "User submitted."', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('SessionStart'), 'SessionStart should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + assert.ok(!labels.includes('SessionEnd'), 'SessionEnd should not be suggested when already present'); + assert.ok(!labels.includes('UserPromptSubmit'), 'UserPromptSubmit should not be suggested when already present'); + }); + + test('cursor after hook event colon triggers New Command snippet', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('New Command'), 'New Command snippet should be suggested'); + assert.strictEqual(actual.length, 1, 'Only one suggestion should be returned'); + }); }); suite('claude agent header completions', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index cd985b7ba74..e1ad97bb791 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -551,7 +551,7 @@ suite('PromptValidator', () => { assert.deepStrictEqual( markers.map(m => ({ severity: m.severity, message: m.message })), [ - { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, model, name, target, tools, user-invocable.` }, + { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, hooks, model, name, target, tools, user-invocable.` }, ] ); }); @@ -1416,6 +1416,358 @@ suite('PromptValidator', () => { assert.strictEqual(markers[0].message, `The 'disable-model-invocation' attribute must be 'true' or 'false'.`); } }); + + test('hooks - valid hook commands', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' PreToolUse:', + ' - type: command', + ' command: ./validate.sh', + ' cwd: scripts', + ' timeout: 30', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - must be a map', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'hooks' attribute must be a map of hook event types to command arrays.` }, + ] + ); + }); + + test('hooks - unknown hook event type', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' UnknownEvent:', + ' - type: command', + ' command: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: `Unknown hook event type 'UnknownEvent'. Supported: SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, SubagentStart, SubagentStop, Stop, ErrorOccurred.` }, + ] + ); + }); + + test('hooks - hook value must be array', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Hook event 'SessionStart' must have an array of command objects as its value.` }, + ] + ); + }); + + test('hooks - command item must be object', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - just a string', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Each hook command must be an object.` }, + ] + ); + }); + + test('hooks - missing type property', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Hook command is missing required property 'type'.` }, + ] + ); + }); + + test('hooks - type must be command', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: script', + ' command: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'type' property in a hook command must be 'command'.` }, + ] + ); + }); + + test('hooks - missing command field', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Hook command must specify at least one of 'command', 'windows', 'linux', or 'osx'.` }, + ] + ); + }); + + test('hooks - empty command string', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: ""', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'command' property in a hook command must be a non-empty string.` }, + ] + ); + }); + + test('hooks - platform-specific commands are valid', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' windows: echo hello', + ' linux: echo hello', + ' osx: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - env must be a map with string values', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' env: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'env' property in a hook command must be a map of string values.` }, + ] + ); + }); + + test('hooks - valid env map', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' env:', + ' NODE_ENV: production', + ' DEBUG: "true"', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - unknown property warns', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' unknownProp: value', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: `Unknown property 'unknownProp' in hook command.` }, + ] + ); + }); + + test('hooks - timeout must be number', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' timeout: not-a-number', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'timeout' property in a hook command must be a number.` }, + ] + ); + }); + + test('hooks - cwd must be string', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' cwd:', + ' - array', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'cwd' property in a hook command must be a string.` }, + ] + ); + }); + + test('hooks - multiple errors in one command', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: script', + ' unknownProp: value', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'type' property in a hook command must be 'command'.` }, + { severity: MarkerSeverity.Warning, message: `Unknown property 'unknownProp' in hook command.` }, + { severity: MarkerSeverity.Error, message: `Hook command must specify at least one of 'command', 'windows', 'linux', or 'osx'.` }, + ] + ); + }); + + test('hooks - nested matcher format is valid', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' UserPromptSubmit:', + ' - hooks:', + ' - type: command', + ' command: "echo foo"', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - nested matcher validates inner commands', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' PreToolUse:', + ' - matcher: Bash', + ' hooks:', + ' - type: script', + ' command: "echo foo"', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'type' property in a hook command must be 'command'.` }, + ] + ); + }); + + test('hooks - nested hooks must be array', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' PreToolUse:', + ' - hooks: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'hooks' property in a matcher must be an array of command objects.` }, + ] + ); + }); }); suite('instructions', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts index 56cf17fafc8..ee30e3f60dc 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts @@ -5,9 +5,11 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { resolveHookCommand, resolveEffectiveCommand, formatHookCommandLabel, IHookCommand } from '../../../common/promptSyntax/hookSchema.js'; +import { resolveHookCommand, resolveEffectiveCommand, formatHookCommandLabel, IHookCommand, parseSubagentHooksFromYaml } from '../../../common/promptSyntax/hookSchema.js'; import { URI } from '../../../../../../base/common/uri.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { HookType } from '../../../common/promptSyntax/hookTypes.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; suite('HookSchema', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -485,4 +487,162 @@ suite('HookSchema', () => { assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), 'default-command'); }); }); + + suite('parseSubagentHooksFromYaml', () => { + + const workspaceRoot = URI.file('/workspace'); + const userHome = '/home/user'; + + const dummyRange = new Range(1, 1, 1, 1); + + function makeScalar(value: string): import('../../../common/promptSyntax/promptFileParser.js').IScalarValue { + return { type: 'scalar', value, range: dummyRange, format: 'none' }; + } + + function makeMap(entries: Record): import('../../../common/promptSyntax/promptFileParser.js').IMapValue { + const properties = Object.entries(entries).map(([key, value]) => ({ + key: makeScalar(key), + value, + })); + return { type: 'map', properties, range: dummyRange }; + } + + function makeSequence(items: import('../../../common/promptSyntax/promptFileParser.js').IValue[]): import('../../../common/promptSyntax/promptFileParser.js').ISequenceValue { + return { type: 'sequence', items, range: dummyRange }; + } + + test('parses direct command format (without matcher)', () => { + // hooks: + // PreToolUse: + // - type: command + // command: "./scripts/validate.sh" + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/validate.sh'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/validate.sh'); + }); + + test('parses Claude format (with matcher)', () => { + // hooks: + // PreToolUse: + // - matcher: "Bash" + // hooks: + // - type: command + // command: "./scripts/validate-readonly.sh" + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'matcher': makeScalar('Bash'), + 'hooks': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/validate-readonly.sh'), + }), + ]), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/validate-readonly.sh'); + }); + + test('parses multiple hook types', () => { + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/pre.sh'), + }), + ]), + 'PostToolUse': makeSequence([ + makeMap({ + 'matcher': makeScalar('Edit|Write'), + 'hooks': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/lint.sh'), + }), + ]), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/pre.sh'); + assert.strictEqual(result[HookType.PostToolUse]?.length, 1); + assert.strictEqual(result[HookType.PostToolUse]![0].command, './scripts/lint.sh'); + }); + + test('skips unknown hook types', () => { + const hooksMap = makeMap({ + 'UnknownHook': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('echo "ignored"'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse], undefined); + assert.strictEqual(result[HookType.PostToolUse], undefined); + }); + + test('handles command without type field', () => { + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'command': makeScalar('./scripts/validate.sh'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/validate.sh'); + }); + + test('resolves cwd relative to workspace', () => { + const hooksMap = makeMap({ + 'SessionStart': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('echo "start"'), + 'cwd': makeScalar('src'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.SessionStart]?.length, 1); + assert.deepStrictEqual(result[HookType.SessionStart]![0].cwd, URI.file('/workspace/src')); + }); + + test('skips non-sequence hook values', () => { + const hooksMap = makeMap({ + 'PreToolUse': makeScalar('not-a-sequence'), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse], undefined); + }); + }); }); 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 7d51bb6782f..a5e0efdd72b 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 @@ -794,6 +794,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -850,6 +851,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local }, }, @@ -925,6 +927,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -943,6 +946,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1013,6 +1017,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1031,6 +1036,7 @@ suite('PromptsService', () => { tools: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1049,6 +1055,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1126,6 +1133,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/copilot-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1146,6 +1154,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent.md'), source: { storage: PromptsStorage.local } }, @@ -1165,6 +1174,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent2.md'), source: { storage: PromptsStorage.local } }, @@ -1221,6 +1231,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'), source: { storage: PromptsStorage.local } } @@ -1291,6 +1302,7 @@ suite('PromptsService', () => { argumentHint: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/restricted-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1309,6 +1321,7 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/no-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1327,6 +1340,7 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/full-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, From a1bbcb5581c479b500bfa8e3308249660d6dd890 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:52:19 -0800 Subject: [PATCH 188/448] Question carousel UI polish (#299272) * Polish question carousel: simplify title bar, footer nav, and layout * Question carousel UI polish - Border radius matches chat input (cornerRadius-large) - Background uses panel background - Remove colon prefix from option descriptions - Option list items use cornerRadius-medium - Footer padding: 8px left, 16px right - 12px gap between number and labels - Freeform row aligned with preset options - Close button vertically centered in titlebar - Checkboxes center-aligned in list items - has-description class for title+description items - Number elements use consistent width - Focus outline consistent across all list items - Tighter gap between presets and custom answer - Summary Q/A always on separate rows - Hide submit icon when carousel is open (show stop only) - Show submit when user types to steer * Add close button to single-question carousel title row * Add submit footer for single-question multi-select carousels * Align single-question submit footer to the right with hint * Fix failing carousel unit tests Update test selectors and structure to match current DOM: - Remove .chat-question-carousel-nav assertion (element no longer exists) - Update markdown/message tests to use .chat-question-title - Fix nav button tests to use multi-question carousels with .chat-question-nav-arrow - Fix submit button test to use multi-question carousel * Fix chat question carousel navigation and summary test regressions --- .../browser/actions/chatExecuteActions.ts | 10 +- .../chatQuestionCarouselPart.ts | 359 ++++++++++-------- .../media/chatQuestionCarousel.css | 269 ++++++------- .../chatQuestionCarouselPart.test.ts | 37 +- 4 files changed, 348 insertions(+), 327 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 58043423887..b1d898bbcb1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -192,7 +192,11 @@ const requestInProgressWithoutInput = ContextKeyExpr.and( ); const pendingToolCall = ContextKeyExpr.or( ChatContextKeys.Editing.hasToolConfirmation, - ChatContextKeys.Editing.hasQuestionCarousel, + ContextKeyExpr.and(ChatContextKeys.Editing.hasQuestionCarousel, ChatContextKeys.inputHasText.negate()), +); +const noQuestionCarouselOrHasInput = ContextKeyExpr.or( + ChatContextKeys.Editing.hasQuestionCarousel.negate(), + ChatContextKeys.inputHasText, ); const whenNotInProgress = ChatContextKeys.requestInProgress.negate(); @@ -235,6 +239,7 @@ export class ChatSubmitAction extends SubmitAction { whenNotInProgress, menuCondition, ChatContextKeys.withinEditSessionDiff.negate(), + noQuestionCarouselOrHasInput, ), group: 'navigation', alt: { @@ -762,7 +767,8 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { order: 4, when: ContextKeyExpr.and( notInProgressOrEditing, - menuCondition), + menuCondition, + noQuestionCarouselOrHasInput), group: 'navigation', alt: { id: 'workbench.action.chat.sendToNewChat', diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 7aea002a1da..753e518fb2d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -10,6 +10,7 @@ import { Emitter, Event } from '../../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { isMacintosh } from '../../../../../../base/common/platform.js'; import { hasKey } from '../../../../../../base/common/types.js'; import { localize } from '../../../../../../nls.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; @@ -53,12 +54,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private _closeButtonContainer: HTMLElement | undefined; private _footerRow: HTMLElement | undefined; private _stepIndicator: HTMLElement | undefined; - private _navigationButtons: HTMLElement | undefined; + private _submitHint: HTMLElement | undefined; + private _submitButton: Button | undefined; private _prevButton: Button | undefined; private _nextButton: Button | undefined; - private readonly _nextButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable()); - private _submitButton: Button | undefined; - private readonly _submitButtonHover: MutableDisposable<{ dispose(): void }> = this._register(new MutableDisposable()); private _skipAllButton: Button | undefined; private _isSkipped = false; @@ -147,69 +146,36 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const skipAllTitle = localize('chat.questionCarousel.skipAllTitle', 'Skip all questions'); const skipAllButton = interactiveStore.add(new Button(this._closeButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); skipAllButton.label = `$(${Codicon.close.id})`; - skipAllButton.element.classList.add('chat-question-nav-arrow', 'chat-question-close'); + skipAllButton.element.classList.add('chat-question-close'); skipAllButton.element.setAttribute('aria-label', skipAllTitle); interactiveStore.add(this._hoverService.setupDelayedHover(skipAllButton.element, { content: skipAllTitle })); this._skipAllButton = skipAllButton; } - // Footer row with step indicator and navigation buttons - this._footerRow = dom.$('.chat-question-footer-row'); - - // Step indicator (e.g., "2/4") on the left - this._stepIndicator = dom.$('.chat-question-step-indicator'); - this._footerRow.appendChild(this._stepIndicator); - - // Navigation controls (< >) - placed in footer row - this._navigationButtons = dom.$('.chat-question-carousel-nav'); - this._navigationButtons.setAttribute('role', 'navigation'); - this._navigationButtons.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); - - // Group prev/next buttons together - const arrowsContainer = dom.$('.chat-question-nav-arrows'); - - const previousLabel = localize('previous', 'Previous'); - const previousLabelWithKeybinding = this.getLabelWithKeybinding(previousLabel, PREVIOUS_QUESTION_ACTION_ID); - const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); - prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev'); - prevButton.label = `$(${Codicon.chevronLeft.id})`; - prevButton.element.setAttribute('aria-label', previousLabelWithKeybinding); - interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabelWithKeybinding })); - this._prevButton = prevButton; - - const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); - nextButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-next'); - nextButton.label = `$(${Codicon.chevronRight.id})`; - this._nextButton = nextButton; - - const submitButton = interactiveStore.add(new Button(this._navigationButtons, { ...defaultButtonStyles })); - submitButton.element.classList.add('chat-question-submit-button'); - submitButton.label = localize('submit', 'Submit'); - this._submitButton = submitButton; - - this._navigationButtons.appendChild(arrowsContainer); - this._footerRow.appendChild(this._navigationButtons); - this.domNode.append(this._footerRow); + const isSingleQuestion = this.carousel.questions.length === 1; + if (!isSingleQuestion && this._closeButtonContainer) { + this.domNode.insertBefore(this._closeButtonContainer, this._questionContainer!); + } // Register event listeners - interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1))); - interactiveStore.add(nextButton.onDidClick(() => this.navigate(1))); - interactiveStore.add(submitButton.onDidClick(() => this.submit())); if (this._skipAllButton) { interactiveStore.add(this._skipAllButton.onDidClick(() => this.ignore())); } - // Register keyboard navigation - handle Enter on text inputs and freeform textareas + // Register keyboard navigation interactiveStore.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.keyCode === KeyCode.Escape && this.carousel.allowSkip) { e.preventDefault(); e.stopPropagation(); this.ignore(); + } else if (event.keyCode === KeyCode.Enter && (event.metaKey || event.ctrlKey)) { + // Cmd/Ctrl+Enter submits immediately from anywhere + e.preventDefault(); + e.stopPropagation(); + this.submit(); } else if (event.keyCode === KeyCode.Enter && !event.shiftKey) { - // Handle Enter key for text inputs and freeform textareas, not radio/checkbox or buttons - // Buttons have their own Enter/Space handling via Button class const target = e.target as HTMLElement; const isTextInput = target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'; const isFreeformTextarea = target.tagName === 'TEXTAREA' && target.classList.contains('chat-question-freeform-textarea'); @@ -262,6 +228,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._currentIndex = newIndex; this.persistDraftState(); this.renderCurrentQuestion(true); + this.domNode.focus(); } } @@ -339,8 +306,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._singleSelectItems.clear(); this._multiSelectCheckboxes.clear(); this._freeformTextareas.clear(); - this._nextButtonHover.value = undefined; - this._submitButtonHover.value = undefined; // Clear references to disposed elements this._prevButton = undefined; @@ -348,10 +313,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._submitButton = undefined; this._skipAllButton = undefined; this._questionContainer = undefined; - this._navigationButtons = undefined; this._closeButtonContainer = undefined; this._footerRow = undefined; this._stepIndicator = undefined; + this._submitHint = undefined; this._inputScrollable = undefined; } @@ -381,12 +346,15 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const availableScrollableHeight = Math.floor(maxContainerHeight - contentVerticalPadding - nonScrollableContentHeight); const constrainedScrollableHeight = Math.max(0, availableScrollableHeight); + const constrainedScrollableHeightPx = `${constrainedScrollableHeight}px`; // Constrain the content element (DomScrollableElement._element) so that // scanDomNode sees clientHeight < scrollHeight and enables scrolling. // The wrapper inherits the same constraint via CSS flex. - scrollableContent.style.height = `${constrainedScrollableHeight}px`; - scrollableContent.style.maxHeight = `${constrainedScrollableHeight}px`; + if (scrollableContent.style.height !== constrainedScrollableHeightPx || scrollableContent.style.maxHeight !== constrainedScrollableHeightPx) { + scrollableContent.style.height = constrainedScrollableHeightPx; + scrollableContent.style.maxHeight = constrainedScrollableHeightPx; + } inputScrollable.scanDomNode(); } @@ -551,7 +519,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } private renderCurrentQuestion(focusContainerForScreenReader: boolean = false): void { - if (!this._questionContainer || !this._prevButton || !this._nextButton || !this._submitButton) { + if (!this._questionContainer) { return; } @@ -574,70 +542,32 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return; } - // Render question header row with title and close button + // Render unified question title (message ?? title) const headerRow = dom.$('.chat-question-header-row'); const titleRow = dom.$('.chat-question-title-row'); - // Render question title (short header) in the header bar as plain text - if (question.title) { + const questionText = question.message ?? question.title; + if (questionText) { const title = dom.$('.chat-question-title'); - const questionText = question.title; const messageContent = this.getQuestionText(questionText); - title.setAttribute('aria-label', messageContent); - if (question.message !== undefined) { - const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); - const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); - title.appendChild(renderedTitle.element); - } else { - // Check for subtitle in parentheses at the end - const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); - if (parenMatch) { - // Main title (bold) - const mainTitle = dom.$('span.chat-question-title-main'); - mainTitle.textContent = parenMatch[1]; - title.appendChild(mainTitle); - - // Subtitle in parentheses (normal weight) - const subtitle = dom.$('span.chat-question-title-subtitle'); - subtitle.textContent = ' ' + parenMatch[2]; - title.appendChild(subtitle); - } else { - title.textContent = messageContent; - } - } + const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); + const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); + title.appendChild(renderedTitle.element); titleRow.appendChild(title); } - // Add close button to header row (if allowSkip is enabled) - if (this._closeButtonContainer) { - titleRow.appendChild(this._closeButtonContainer); - } - headerRow.appendChild(titleRow); - this._questionContainer.appendChild(headerRow); - - // Render full question text below the header row (supports multi-line and markdown) - if (question.message) { - const messageEl = dom.$('.chat-question-message'); - if (isMarkdownString(question.message)) { - const renderedMessage = questionRenderStore.add(this._markdownRendererService.render(MarkdownString.lift(question.message))); - messageEl.appendChild(renderedMessage.element); - } else { - messageEl.textContent = this.getQuestionText(question.message); - } - this._questionContainer.appendChild(messageEl); - } - + // For single-question carousels, add close button inside the title row const isSingleQuestion = this.carousel.questions.length === 1; - // Update step indicator in footer - if (this._stepIndicator) { - this._stepIndicator.textContent = `${this._currentIndex + 1}/${this.carousel.questions.length}`; - this._stepIndicator.style.display = isSingleQuestion ? 'none' : ''; + if (isSingleQuestion && this._closeButtonContainer) { + titleRow.appendChild(this._closeButtonContainer); } + this._questionContainer.appendChild(headerRow); + // Render input based on question type const inputContainer = dom.$('.chat-question-input-container'); this.renderInput(inputContainer, question); @@ -652,10 +582,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollableNode.classList.add('chat-question-input-scrollable'); this._questionContainer.appendChild(inputScrollableNode); - const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => this.layoutInputScrollable(inputScrollable))); + let relayoutScheduled = false; + const relayoutScheduler = questionRenderStore.add(new MutableDisposable()); + const scheduleLayoutInputScrollable = () => { + if (relayoutScheduled) { + return; + } + + relayoutScheduled = true; + relayoutScheduler.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => { + relayoutScheduled = false; + this.layoutInputScrollable(inputScrollable); + }); + }; + + const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => scheduleLayoutInputScrollable())); questionRenderStore.add(inputResizeObserver.observe(inputScrollableNode)); questionRenderStore.add(inputResizeObserver.observe(inputContainer)); - questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => this.layoutInputScrollable(inputScrollable))); + scheduleLayoutInputScrollable(); this.layoutInputScrollable(inputScrollable); questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => { inputContainer.scrollTop = 0; @@ -664,26 +608,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollable.scanDomNode(); })); - // Update navigation button states (prevButton and nextButton are guaranteed non-null from guard above) - this._prevButton!.enabled = this._currentIndex > 0; - this._prevButton!.element.style.display = isSingleQuestion ? 'none' : ''; - - // Keep navigation arrows stable and disable next on the last question - const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1; - const submitLabel = localize('submit', 'Submit'); - const nextLabel = localize('next', 'Next'); - const nextLabelWithKeybinding = this.getLabelWithKeybinding(nextLabel, NEXT_QUESTION_ACTION_ID); - this._nextButton!.label = `$(${Codicon.chevronRight.id})`; - this._nextButton!.enabled = !isLastQuestion; - this._nextButton!.element.setAttribute('aria-label', nextLabelWithKeybinding); - this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabelWithKeybinding }); - - this._submitButton!.enabled = isLastQuestion; - this._submitButton!.element.style.display = isLastQuestion ? '' : 'none'; - this._submitButton!.element.setAttribute('aria-label', submitLabel); - this._submitButtonHover.value = isLastQuestion - ? this._hoverService.setupDelayedHover(this._submitButton!.element, { content: submitLabel }) - : undefined; + // Render footer for multi-question carousels or single-question carousels. + if (!isSingleQuestion) { + this.renderFooter(); + } else { + this.renderSingleQuestionFooter(); + } // Update aria-label to reflect the current question this._updateAriaLabel(); @@ -697,6 +627,138 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._onDidChangeHeight.fire(); } + /** + * Renders or updates the persistent footer with nav arrows, step indicator, and submit button. + */ + private renderFooter(): void { + if (!this._footerRow) { + const interactiveStore = this._interactiveUIStore.value; + if (!interactiveStore) { + return; + } + + this._footerRow = dom.$('.chat-question-footer-row'); + + // Left side: nav arrows + step indicator + const leftControls = dom.$('.chat-question-footer-left.chat-question-carousel-nav'); + leftControls.setAttribute('role', 'navigation'); + leftControls.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); + + const arrowsContainer = dom.$('.chat-question-nav-arrows'); + + const previousLabel = this.getLabelWithKeybinding(localize('previous', 'Previous'), PREVIOUS_QUESTION_ACTION_ID); + const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev'); + prevButton.label = `$(${Codicon.chevronLeft.id})`; + prevButton.element.setAttribute('aria-label', previousLabel); + interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabel })); + interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1))); + this._prevButton = prevButton; + + const nextLabel = this.getLabelWithKeybinding(localize('next', 'Next'), NEXT_QUESTION_ACTION_ID); + const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + nextButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-next'); + nextButton.label = `$(${Codicon.chevronRight.id})`; + nextButton.element.setAttribute('aria-label', nextLabel); + interactiveStore.add(this._hoverService.setupDelayedHover(nextButton.element, { content: nextLabel })); + interactiveStore.add(nextButton.onDidClick(() => this.navigate(1))); + this._nextButton = nextButton; + + leftControls.appendChild(arrowsContainer); + + this._stepIndicator = dom.$('.chat-question-step-indicator'); + leftControls.appendChild(this._stepIndicator); + + this._footerRow.appendChild(leftControls); + + // Right side: hint + submit + const rightControls = dom.$('.chat-question-footer-right'); + + const hint = dom.$('span.chat-question-submit-hint'); + hint.textContent = isMacintosh + ? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit') + : localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit'); + rightControls.appendChild(hint); + this._submitHint = hint; + + const submitButton = interactiveStore.add(new Button(rightControls, { ...defaultButtonStyles })); + submitButton.element.classList.add('chat-question-submit-button'); + submitButton.label = localize('submit', 'Submit'); + interactiveStore.add(submitButton.onDidClick(() => this.submit())); + this._submitButton = submitButton; + + this._footerRow.appendChild(rightControls); + this.domNode.append(this._footerRow); + } + + this.updateFooterState(); + } + + /** + * Updates the footer nav button enabled state and step indicator text. + */ + private updateFooterState(): void { + if (this._prevButton) { + this._prevButton.enabled = this._currentIndex > 0; + } + if (this._nextButton) { + this._nextButton.enabled = this._currentIndex < this.carousel.questions.length - 1; + } + if (this._stepIndicator) { + this._stepIndicator.textContent = localize( + 'chat.questionCarousel.stepIndicator', + '{0}/{1}', + this._currentIndex + 1, + this.carousel.questions.length + ); + } + if (this._submitButton) { + const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1; + this._submitButton.element.style.display = isLastQuestion ? '' : 'none'; + if (this._submitHint) { + this._submitHint.style.display = isLastQuestion ? '' : 'none'; + } + } + } + + /** + * Renders a simplified footer with just a submit button for single-question multi-select carousels. + */ + private renderSingleQuestionFooter(): void { + if (!this._footerRow) { + const interactiveStore = this._interactiveUIStore.value; + if (!interactiveStore) { + return; + } + + this._footerRow = dom.$('.chat-question-footer-row'); + + // Spacer to push controls to the right + const leftControls = dom.$('.chat-question-footer-left.chat-question-carousel-nav'); + leftControls.setAttribute('role', 'navigation'); + leftControls.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); + this._footerRow.appendChild(leftControls); + + const rightControls = dom.$('.chat-question-footer-right'); + + const hint = dom.$('span.chat-question-submit-hint'); + hint.textContent = isMacintosh + ? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit') + : localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit'); + rightControls.appendChild(hint); + this._submitHint = hint; + + const submitButton = interactiveStore.add(new Button(rightControls, { ...defaultButtonStyles })); + submitButton.element.classList.add('chat-question-submit-button'); + submitButton.label = localize('submit', 'Submit'); + interactiveStore.add(submitButton.onDidClick(() => this.submit())); + this._submitButton = submitButton; + + this._footerRow.appendChild(rightControls); + this.domNode.append(this._footerRow); + } + } + private getLabelWithKeybinding(label: string, actionId: string): string { const keybindingLabel = this._keybindingService.lookupKeybinding(actionId, this._contextKeyService)?.getLabel(); return keybindingLabel @@ -837,12 +899,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { + listItem.classList.add('has-description'); const titleSpan = dom.$('span.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); const descSpan = dom.$('span.chat-question-list-label-desc'); - descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3); + descSpan.textContent = option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { label.textContent = option.label; @@ -929,7 +992,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } else if (event.keyCode === KeyCode.UpArrow) { e.preventDefault(); newIndex = Math.max(data.selectedIndex - 1, 0); - } else if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + } else if ((event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) && !event.metaKey && !event.ctrlKey) { // Enter confirms current selection and advances to next question e.preventDefault(); e.stopPropagation(); @@ -1037,12 +1100,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { + listItem.classList.add('has-description'); const titleSpan = dom.$('span.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); const descSpan = dom.$('span.chat-question-list-label-desc'); - descSpan.textContent = ': ' + option.label.substring(separatorIndex + 3); + descSpan.textContent = option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { label.textContent = option.label; @@ -1128,7 +1192,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent e.preventDefault(); focusedIndex = Math.max(focusedIndex - 1, 0); listItems[focusedIndex].focus(); - } else if (event.keyCode === KeyCode.Enter) { + } else if (event.keyCode === KeyCode.Enter && !event.metaKey && !event.ctrlKey) { e.preventDefault(); e.stopPropagation(); this.handleNextOrSubmit(); @@ -1267,40 +1331,25 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent for (const question of this.carousel.questions) { const answer = this._answers.get(question.id); - if (answer === undefined) { - continue; - } const summaryItem = dom.$('.chat-question-summary-item'); - // Category label (use same text as shown in question UI: message ?? title) - const questionLabel = dom.$('span.chat-question-summary-label'); + const questionRow = dom.$('div.chat-question-summary-label'); const questionText = question.message ?? question.title; let labelText = typeof questionText === 'string' ? questionText : questionText.value; - // Remove trailing colons and whitespace to avoid double colons (CSS adds ': ') labelText = labelText.replace(/[:\s]+$/, ''); - questionLabel.textContent = labelText; - summaryItem.appendChild(questionLabel); + questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); + summaryItem.appendChild(questionRow); - // Format answer with title and description parts - const formattedAnswer = this.formatAnswerForSummary(question, answer); - const separatorIndex = formattedAnswer.indexOf(' - '); - - if (separatorIndex !== -1) { - // Answer title (bold) - const answerTitle = dom.$('span.chat-question-summary-answer-title'); - answerTitle.textContent = formattedAnswer.substring(0, separatorIndex); - summaryItem.appendChild(answerTitle); - - // Answer description (normal) - const answerDesc = dom.$('span.chat-question-summary-answer-desc'); - answerDesc.textContent = ' - ' + formattedAnswer.substring(separatorIndex + 3); - summaryItem.appendChild(answerDesc); + if (answer !== undefined) { + const formattedAnswer = this.formatAnswerForSummary(question, answer); + const answerRow = dom.$('div.chat-question-summary-answer-title'); + answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); + summaryItem.appendChild(answerRow); } else { - // Just the answer value (bold) - const answerValue = dom.$('span.chat-question-summary-answer-title'); - answerValue.textContent = formattedAnswer; - summaryItem.appendChild(answerValue); + const unanswered = dom.$('div.chat-question-summary-unanswered'); + unanswered.textContent = localize('chat.questionCarousel.notAnsweredYet', 'Not answered yet'); + summaryItem.appendChild(unanswered); } summaryContainer.appendChild(summaryItem); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 3dd44e42731..59814a560f8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -14,20 +14,21 @@ .interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-container { margin: 0; border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-editor-background); - border-radius: 4px; + background-color: var(--vscode-panel-background); + border-radius: var(--vscode-cornerRadius-large); } /* general questions styling */ .interactive-session .chat-question-carousel-container { margin: 8px 0; border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-large); display: flex; flex-direction: column; overflow: hidden; container-type: inline-size; max-height: min(420px, 45vh); + position: relative; } /* input part wrapper */ @@ -47,7 +48,6 @@ .interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-content, .interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-content { - flex: 1; min-height: 0; } @@ -55,18 +55,13 @@ .interactive-session .chat-question-carousel-container .chat-question-carousel-content { display: flex; flex-direction: column; - flex: 1; min-height: 0; - background: var(--vscode-chat-requestBackground); - padding: 8px 16px 10px 16px; overflow: hidden; .chat-question-header-row { display: flex; flex-direction: column; flex-shrink: 0; - background: var(--vscode-chat-requestBackground); - padding: 0 16px 10px 16px; overflow: hidden; .chat-question-title-row { @@ -75,6 +70,8 @@ align-items: center; gap: 8px; min-width: 0; + padding: 8px 8px 8px 16px; + border-bottom: 1px solid var(--vscode-chat-requestBorder); } .chat-question-title { @@ -85,13 +82,6 @@ font-weight: 500; font-size: var(--vscode-chat-font-size-body-s); margin: 0; - padding-top: 4px; - padding-bottom: 4px; - margin-left: -16px; - margin-right: -16px; - padding-left: 16px; - padding-right: 16px; - border-bottom: 1px solid var(--vscode-chat-requestBorder); .rendered-markdown { a { @@ -107,15 +97,6 @@ margin: 0; } } - - .chat-question-title-main { - font-weight: 500; - } - - .chat-question-title-subtitle { - font-weight: normal; - color: var(--vscode-descriptionForeground); - } } .chat-question-close-container { @@ -126,49 +107,29 @@ width: 22px; height: 22px; padding: 0; - border: none; + border: none !important; + box-shadow: none !important; background: transparent !important; - color: var(--vscode-foreground) !important; + color: var(--vscode-icon-foreground) !important; } .monaco-button.chat-question-close:hover:not(.disabled) { background: var(--vscode-toolbar-hoverBackground) !important; } } - - .chat-question-message { - flex-shrink: 0; - padding-top: 8px; - font-size: var(--vscode-chat-font-size-body-s); - word-wrap: break-word; - overflow-wrap: break-word; - line-height: 1.4; - - .rendered-markdown { - a { - color: var(--vscode-textLink-foreground); - } - - a:hover, - a:active { - color: var(--vscode-textLink-activeForeground); - } - - p { - margin: 0; - } - } - } } } +/* Extra right padding when close button is absolutely positioned (multi-question) */ +.interactive-session .chat-question-carousel-container:has(> .chat-question-close-container) .chat-question-title-row { + padding-right: 36px; +} + /* questions list and freeform area */ .interactive-session .chat-question-carousel-container .chat-question-input-container { display: flex; flex-direction: column; - margin-top: 4px; - padding-right: 14px; - padding-bottom: 12px; + padding: 8px; min-width: 0; &::after { @@ -179,37 +140,24 @@ } /* some hackiness to get the focus looking right */ - .chat-question-list-item:focus:not(.selected), + .chat-question-list-item:focus, .chat-question-list:focus { outline: none; } - .chat-question-list:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; - } - - .chat-question-list:focus-within .chat-question-list-item.selected { - outline-width: 1px; - outline-style: solid; - outline-offset: -1px; - outline-color: var(--vscode-focusBorder); - } - .chat-question-list { display: flex; flex-direction: column; - gap: 3px; outline: none; - padding: 4px 0; + padding: 0; .chat-question-list-item { display: flex; - align-items: flex-start; - gap: 8px; - padding: 3px 8px; + align-items: center; + gap: 12px; + padding: 6px 8px; cursor: pointer; - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-medium); user-select: none; .chat-question-list-indicator { @@ -220,6 +168,8 @@ justify-content: center; flex-shrink: 0; margin-left: auto; + align-self: flex-start; + margin-top: 2px; } .chat-question-list-indicator.codicon-check { @@ -232,11 +182,13 @@ flex: 1; word-wrap: break-word; overflow-wrap: break-word; - padding-top: 2px; + display: flex; + flex-direction: column; } .chat-question-list-label-title { - font-weight: 600; + font-weight: 500; + line-height: 1.4; } .chat-question-list-label-desc { @@ -245,13 +197,28 @@ } } + .chat-question-list-item.has-description { + align-items: flex-start; + + .chat-question-list-number { + line-height: 1.4; + font-size: var(--vscode-chat-font-size-body-s); + font-weight: 500; + } + + .chat-question-list-checkbox { + /* Title line-height is ~17px (1.4 * body-s), checkbox is 16px: 1px offset */ + margin-top: 1px; + } + } + .chat-question-list-item:hover { background-color: var(--vscode-list-hoverBackground); } /* Single-select: highlight entire row when selected */ .chat-question-list-item.selected { - background-color: var(--vscode-list-activeSelectionBackground); + background-color: var(--vscode-list-hoverBackground); color: var(--vscode-list-activeSelectionForeground); .chat-question-label { @@ -268,16 +235,12 @@ } .chat-question-list-number { - background-color: transparent; color: var(--vscode-list-activeSelectionForeground); - border-color: var(--vscode-list-activeSelectionForeground); - border-bottom-color: var(--vscode-list-activeSelectionForeground); - box-shadow: none; } } .chat-question-list-item.selected:hover { - background-color: var(--vscode-list-activeSelectionBackground); + background-color: var(--vscode-list-hoverBackground); } /* Checkbox for multi-select */ @@ -291,11 +254,12 @@ } .chat-question-freeform { - margin-left: 8px; + margin: 0; display: flex; flex-direction: row; align-items: center; - gap: 8px; + padding: 4px 8px; + gap: 12px; .chat-question-freeform-number { height: fit-content; @@ -338,22 +302,11 @@ /* todo: change to use keybinding service so we don't have to recreate this */ .chat-question-list-number, .chat-question-freeform-number { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 14px; - padding: 0px 4px; - border-style: solid; - border-width: 1px; - border-radius: 3px; - font-size: 11px; - font-weight: normal; - background-color: var(--vscode-keybindingLabel-background); - color: var(--vscode-keybindingLabel-foreground); - border-color: var(--vscode-keybindingLabel-border); - border-bottom-color: var(--vscode-keybindingLabel-bottomBorder); - box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow); + font-size: var(--vscode-chat-font-size-body-s); + color: var(--vscode-descriptionForeground); flex-shrink: 0; + min-width: 1ch; + text-align: right; } } @@ -362,31 +315,53 @@ } .interactive-session .chat-question-carousel-container .chat-question-input-scrollable { - flex: 1; + flex: 0 1 auto; min-height: 0; overscroll-behavior: contain; } -/* footer with step indicator and nav buttons */ +/* close button for multi-question carousels (positioned top-right) */ +.interactive-session .chat-question-carousel-container > .chat-question-close-container { + position: absolute; + top: 6px; + right: 8px; + z-index: 1; + + .monaco-button.chat-question-close { + min-width: 22px; + width: 22px; + height: 22px; + padding: 0; + border: none !important; + box-shadow: none !important; + background: transparent !important; + color: var(--vscode-icon-foreground) !important; + } + + .monaco-button.chat-question-close:hover:not(.disabled) { + background: var(--vscode-toolbar-hoverBackground) !important; + } +} + +/* footer with nav arrows, step indicator, and submit */ .interactive-session .chat-question-carousel-container .chat-question-footer-row { display: flex; justify-content: space-between; align-items: center; - padding: 4px 16px; + padding: 4px 8px; border-top: 1px solid var(--vscode-chat-requestBorder); - background: var(--vscode-chat-requestBackground); + flex-shrink: 0; - .chat-question-step-indicator { - font-size: var(--vscode-chat-font-size-body-s); - color: var(--vscode-descriptionForeground); - } - - .chat-question-carousel-nav { + .chat-question-footer-left { display: flex; align-items: center; - gap: 4px; - flex-shrink: 0; - margin-left: auto; + gap: 8px; + } + + .chat-question-footer-right { + display: flex; + align-items: center; + gap: 8px; } .chat-question-nav-arrows { @@ -395,49 +370,48 @@ gap: 4px; } - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow { + .monaco-button.chat-question-nav-arrow { min-width: 22px; width: 22px; height: 22px; padding: 0; - border: none; - } - - /* Secondary buttons (prev, next) use gray secondary background */ - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev, - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next { - background: var(--vscode-button-secondaryBackground) !important; - color: var(--vscode-button-secondaryForeground) !important; - } - - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-prev:hover:not(.disabled), - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-nav-next:hover:not(.disabled) { - background: var(--vscode-button-secondaryHoverBackground) !important; - } - - /* Dedicated submit button uses primary background */ - .chat-question-carousel-nav .monaco-button.chat-question-submit-button { - background: var(--vscode-button-background) !important; - color: var(--vscode-button-foreground) !important; - height: 22px; - min-width: auto; - padding: 0 8px; - } - - .chat-question-carousel-nav .monaco-button.chat-question-submit-button:hover:not(.disabled) { - background: var(--vscode-button-hoverBackground) !important; - } - - /* Close button uses transparent background */ - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close { + border: none !important; + box-shadow: none !important; background: transparent !important; color: var(--vscode-foreground) !important; } - .chat-question-carousel-nav .monaco-button.chat-question-nav-arrow.chat-question-close:hover:not(.disabled) { + .monaco-button.chat-question-nav-arrow:hover:not(.disabled) { background: var(--vscode-toolbar-hoverBackground) !important; } + .monaco-button.chat-question-nav-arrow.disabled { + opacity: 0.4; + } + + .chat-question-step-indicator { + font-size: var(--vscode-chat-font-size-body-s); + color: var(--vscode-descriptionForeground); + } + + .chat-question-submit-hint { + font-size: 11px; + color: var(--vscode-descriptionForeground); + } + + .monaco-button.chat-question-submit-button { + background: var(--vscode-button-background) !important; + color: var(--vscode-button-foreground) !important; + height: 22px; + width: auto; + flex: 0 0 auto; + min-width: auto; + padding: 0 8px; + } + + .monaco-button.chat-question-submit-button:hover:not(.disabled) { + background: var(--vscode-button-hoverBackground) !important; + } } /* summary (after finished) */ @@ -449,9 +423,7 @@ .chat-question-summary-item { display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: baseline; + flex-direction: column; gap: 0; font-size: var(--vscode-chat-font-size-body-s); } @@ -462,11 +434,6 @@ overflow-wrap: break-word; } - .chat-question-summary-label::after { - content: ': '; - white-space: pre; - } - .chat-question-summary-answer-title { color: var(--vscode-foreground); font-weight: 600; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 10045c7dce3..919df6c4893 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -60,7 +60,6 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(widget.domNode.classList.contains('chat-question-carousel-container')); assert.ok(widget.domNode.querySelector('.chat-question-header-row')); assert.ok(widget.domNode.querySelector('.chat-question-carousel-content')); - assert.ok(widget.domNode.querySelector('.chat-question-carousel-nav')); }); test('renders question title', () => { @@ -100,12 +99,7 @@ suite('ChatQuestionCarouselPart', () => { const title = widget.domNode.querySelector('.chat-question-title'); assert.ok(title, 'title element should exist'); - const messageEl = widget.domNode.querySelector('.chat-question-message'); - assert.ok(messageEl, 'message element should exist'); - assert.ok(messageEl?.querySelector('.rendered-markdown'), 'markdown content should be rendered'); - assert.strictEqual(messageEl?.textContent?.includes('**details**'), false, 'markdown syntax should not be shown as raw text'); - const link = messageEl?.querySelector('a') as HTMLAnchorElement | null; - assert.ok(link, 'markdown link should render as anchor'); + assert.ok(title?.querySelector('.rendered-markdown'), 'markdown content should be rendered'); }); test('renders plain string question message as text', () => { @@ -119,10 +113,9 @@ suite('ChatQuestionCarouselPart', () => { ]); createWidget(carousel); - const messageEl = widget.domNode.querySelector('.chat-question-message'); - assert.ok(messageEl, 'message element should exist'); - assert.ok(messageEl?.textContent?.includes('details'), 'plain text content should be rendered'); - assert.strictEqual(messageEl?.querySelector('.rendered-markdown'), null, 'plain string message should not use markdown renderer'); + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title, 'title element should exist'); + assert.ok(title?.textContent?.includes('details'), 'content should be rendered'); }); test('renders progress indicator correctly', () => { @@ -278,34 +271,40 @@ suite('ChatQuestionCarouselPart', () => { ]); createWidget(carousel); - // Use dedicated class selectors for stability - const prevButton = widget.domNode.querySelector('.chat-question-nav-prev') as HTMLButtonElement; + const navArrows = widget.domNode.querySelectorAll('.chat-question-nav-arrow') as NodeListOf; + const prevButton = navArrows[0]; assert.ok(prevButton, 'Previous button should exist'); assert.ok(prevButton.classList.contains('disabled') || prevButton.disabled, 'Previous button should be disabled on first question'); }); test('next button stays as arrow and is disabled on last question', () => { const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Only Question' } + { id: 'q1', type: 'text', title: 'Only Question' }, + { id: 'q2', type: 'text', title: 'Question 2' } ]); createWidget(carousel); - // Use dedicated class selector for stability - const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLButtonElement; + // Navigate to last question + widget.navigateToNextQuestion(); + + const navArrows = widget.domNode.querySelectorAll('.chat-question-nav-arrow') as NodeListOf; + const nextButton = navArrows[1]; assert.ok(nextButton, 'Next button should exist'); - assert.strictEqual(nextButton.getAttribute('aria-label'), 'Next', 'Next button should preserve Next aria-label on last question'); assert.ok(nextButton.classList.contains('disabled') || nextButton.disabled, 'Next button should be disabled on last question'); }); test('submit button is shown on last question', () => { const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Only Question' } + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } ]); createWidget(carousel); + // Navigate to last question + widget.navigateToNextQuestion(); + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; assert.ok(submitButton, 'Submit button should exist'); - assert.strictEqual(submitButton.getAttribute('aria-label'), 'Submit'); assert.notStrictEqual(submitButton.style.display, 'none', 'Submit button should be visible on last question'); }); }); From 37141e3b14e34c7de1688f0f0eb67fac0187907e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Mar 2026 08:56:16 +1100 Subject: [PATCH 189/448] feat: set session options when providing chat session content (#299126) * feat: set session options when providing chat session content * refactor: streamline session option setting in ChatSessionsService --- .../browser/chatSessions/chatSessions.contribution.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 967ae4f9551..a8918b056a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -1052,14 +1052,14 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ options: newSessionOptions ?? {}, dispose: () => { } }; - - for (const [optionId, value] of Object.entries(newSessionOptions ?? {})) { - this.setSessionOption(sessionResource, optionId, value); - } } else { session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token); } + for (const [optionId, value] of Object.entries(session.options ?? {})) { + this.setSessionOption(sessionResource, optionId, value); + } + // Make sure another session wasn't created while we were awaiting the provider { const existingSessionData = this._sessions.get(sessionResource); From a6fb33c23774b95b7f1df1756adb2afe2ad3a7df Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 4 Mar 2026 13:57:23 -0800 Subject: [PATCH 190/448] Improve extension installation wait condition (#299292) --- test/sanity/src/uiTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sanity/src/uiTest.ts b/test/sanity/src/uiTest.ts index 582c4eeab3b..65f8b14a49e 100644 --- a/test/sanity/src/uiTest.ts +++ b/test/sanity/src/uiTest.ts @@ -162,7 +162,7 @@ export class UITest { await installButton.click(); this.context.log('Waiting for extension to be installed'); - await page.locator('.extension-action:not(.disabled)', { hasText: /Uninstall/ }).waitFor({ timeout: 5 * 60_1000 }); + await page.getByRole('button', { name: 'Uninstall' }).first().waitFor({ timeout: 5 * 60_000 }); } /** From 21ebaf37a1b2211c2a8ee81519147b26058f4c99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:58:26 +0000 Subject: [PATCH 191/448] Bump @hono/node-server from 1.19.9 to 1.19.10 in /test/mcp (#299283) Bumps [@hono/node-server](https://github.com/honojs/node-server) from 1.19.9 to 1.19.10. - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](https://github.com/honojs/node-server/compare/v1.19.9...v1.19.10) --- updated-dependencies: - dependency-name: "@hono/node-server" dependency-version: 1.19.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/mcp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 6e7624dc0da..f67a5570734 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -22,9 +22,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.10", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", + "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", "license": "MIT", "engines": { "node": ">=18.14.1" From cd7b7365419733b6833d1b07cb9c01ad663da07b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:02:23 -0800 Subject: [PATCH 192/448] no copy button on request (#299209) no copy button on response --- .../contrib/chat/browser/actions/chatCopyActions.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 23b4712e2a5..42d67d3b3a5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -63,12 +63,6 @@ export function registerChatCopyActions() { when: ChatContextKeys.responseIsFiltered.negate(), group: 'copy', }, - { - id: MenuId.ChatMessageTitle, - group: 'navigation', - order: 5, - when: ChatContextKeys.responseIsFiltered.negate(), - }, { id: MenuId.ChatMessageFooter, group: 'navigation', From 0471f8c218daf9380a590726b0cb0091f5901100 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:02:50 -0800 Subject: [PATCH 193/448] Polish chat input part: picker collapse, padding, and icon sizing (#299293) * Polish chat input part: adjust padding, prevent picker collapse, size add context icon * Increase action widget row gap to 8px --- extensions/theme-2026/themes/2026-dark.json | 1 + .../base/browser/ui/actionbar/actionbar.css | 2 +- .../actionWidget/browser/actionWidget.css | 2 +- .../browser/widget/input/chatModelPicker.ts | 4 +- .../widget/input/modelPickerActionItem.ts | 4 +- .../input/sessionTargetPickerActionItem.ts | 7 +--- .../chat/browser/widget/media/chat.css | 38 +++++++++++-------- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 00e81ee2d10..a346ebba78f 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -60,6 +60,7 @@ "list.hoverBackground": "#262728", "list.hoverForeground": "#bfbfbf", "list.dropBackground": "#3994BC1A", + "toolbar.hoverBackground": "#262728", "list.focusBackground": "#3994BC26", "list.focusForeground": "#bfbfbf", "list.focusOutline": "#3994BCB3", diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 467b1ff6efa..e9e55ad9b26 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -50,7 +50,7 @@ display: flex; font-size: 11px; padding: 3px; - border-radius: 5px; + border-radius: var(--vscode-cornerRadius-medium); } .monaco-action-bar .action-item.disabled .action-label:not(.icon) , diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index f6d3dc6133f..74c89c2c486 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -119,7 +119,7 @@ .action-widget .monaco-list-row.action { display: flex; - gap: 6px; + gap: 8px; align-items: center; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 0257f20252d..2011b0f95e9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -655,9 +655,7 @@ export class ModelPickerWidget extends Disposable { domChildren.push(this._badgeIcon); } - if (!this._hideChevrons?.get()) { - domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); - } + domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(this._domNode, ...domChildren); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 04bfc652890..af61812b3a9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -209,9 +209,7 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem { } domChildren.push(dom.$('span.chat-input-picker-label', undefined, name ?? localize('chat.modelPicker.auto', "Auto"))); - if (!this.pickerOptions.hideChevrons.get()) { - domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); - } + domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(element, ...domChildren); this.setAriaLabelAttributes(element); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 897c927f25f..9ef9fb5eab6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -213,12 +213,9 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local); const labelElements = []; - const collapsed = this.pickerOptions.hideChevrons.get(); labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); - if (!collapsed) { - labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); - labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); - } + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(element, ...labelElements); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 08572fadc85..dd200620d82 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1342,7 +1342,7 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; align-items: center; gap: 6px; - padding: 2px 5px 2px 6px; + padding: 0 4px 0 5px; } .interactive-session .chat-secondary-toolbar:empty { @@ -1412,8 +1412,9 @@ have to be updated for changes to the rules above, or to support more deeply nes } .monaco-workbench .interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label .codicon-chevron-down { - font-size: 12px; - margin-left: 2px; + font-size: 10px; + margin-left: 4px; + opacity: 0.75; } .interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child { @@ -1525,25 +1526,32 @@ have to be updated for changes to the rules above, or to support more deeply nes } } -/* When chevrons are hidden but label is still shown (e.g. model picker), use equal padding */ -.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:has(.chat-input-picker-label), -.interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:has(.chat-input-picker-label), -.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:has(.chat-input-picker-label) { - padding: 3px 7px; -} /* Hide the tools button when the toolbar is in collapsed state */ .interactive-session .chat-input-toolbar:has(.hide-chevrons) .action-item:has(.codicon-settings) { display: none; } -.monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down, -.monaco-workbench .interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down { - font-size: 12px; - margin-left: 2px; +/* Add context button icon sizing */ +.interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .action-label { + display: flex; + align-items: center; + justify-content: center; } -.interactive-session .chat-input-toolbars .monaco-action-bar .actions-container { +.interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .codicon-add { + font-size: 14px; +} + +.monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down, +.monaco-workbench .interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down { + font-size: 10px; + margin-left: 4px; + opacity: 0.75; +} + +.interactive-session .chat-input-toolbars .monaco-action-bar .actions-container, +.interactive-session .chat-secondary-toolbar .monaco-action-bar .actions-container { display: flex; gap: 4px; } @@ -1755,7 +1763,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .interactive-input-part { margin: 0px 12px; - padding: 4px 0 6px 0px; + padding: 4px 0 4px 0px; display: flex; flex-direction: column; gap: 4px; From edb2ef459571eea5567c29ed14fe7b39795b3fce Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:17:04 -0800 Subject: [PATCH 194/448] add logs to troubleshoot contribution availability in evals (#299294) --- .../chat/browser/chatSessions/chatSessions.contribution.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index a8918b056a8..309e48a4472 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -407,7 +407,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } private registerContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable { + this._logService.info(`[ChatSessionsService] registerContribution called for type='${contribution.type}', canDelegate=${contribution.canDelegate}, when='${contribution.when}', extension='${ext.identifier.value}'`); if (this._contributions.has(contribution.type)) { + this._logService.info(`[ChatSessionsService] registerContribution: type='${contribution.type}' already registered, skipping`); return { dispose: () => { } }; } @@ -643,6 +645,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ for (const { contribution, extension } of this._contributions.values()) { const isCurrentlyRegistered = this._contributionDisposables.has(contribution.type); const shouldBeRegistered = this._isContributionAvailable(contribution); + this._logService.trace(`[ChatSessionsService] _evaluateAvailability: type='${contribution.type}', isCurrentlyRegistered=${isCurrentlyRegistered}, shouldBeRegistered=${shouldBeRegistered}, when='${contribution.when}'`); if (isCurrentlyRegistered && !shouldBeRegistered) { // Disable the contribution by disposing its disposable store this._contributionDisposables.deleteAndDispose(contribution.type); @@ -669,6 +672,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void { + this._logService.info(`[ChatSessionsService] _enableContribution: type='${contribution.type}', canDelegate=${contribution.canDelegate}`); const disposableStore = new DisposableStore(); this._contributionDisposables.set(contribution.type, disposableStore); if (contribution.canDelegate) { From c492347a8c971986898c247ec15da7f2b62a96a9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Mar 2026 23:21:05 +0100 Subject: [PATCH 195/448] Fix AI customization section list showing wrong items on fast switching (#299262) * fix - update TypeScript compilation commands in docs * fix - update section handling in AICustomizationListWidget * undo unrelated changes * ccr --- .../aiCustomization/aiCustomizationListWidget.ts | 7 ++++++- .../aiCustomization/aiCustomizationManagementEditor.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 547f42e190a..e890fb82a04 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -725,7 +725,8 @@ export class AICustomizationListWidget extends Disposable { * Loads items for the current section. */ private async loadItems(): Promise { - const promptType = sectionToPromptType(this.currentSection); + const section = this.currentSection; + const promptType = sectionToPromptType(section); const items: IAICustomizationListItem[] = []; @@ -919,6 +920,10 @@ export class AICustomizationListWidget extends Disposable { // Sort items by name items.sort((a, b) => a.name.localeCompare(b.name)); + if (this.currentSection !== section) { + return; // section changed while loading + } + this.allItems = items; this.filterItems(); this._onDidChangeItemCount.fire(items.length); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 6dced7449d2..ed1667fbed9 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -835,6 +835,12 @@ export class AICustomizationManagementEditor extends EditorPane { try { const ref = await this.textModelService.createModelReference(uri); + + if (!isEqual(this.currentEditingUri, uri)) { + ref.dispose(); + return; // another item was selected while loading + } + this.currentModelRef = ref; this.embeddedEditor!.setModel(ref.object.textEditorModel); this.embeddedEditor!.updateOptions({ readOnly: isReadOnly }); @@ -872,7 +878,9 @@ export class AICustomizationManagementEditor extends EditorPane { })); } catch (error) { console.error('Failed to load model for embedded editor:', error); - this.goBackToList(); + if (isEqual(this.currentEditingUri, uri)) { + this.goBackToList(); + } } } From 8d491919b2cdac2fe69fd9ec9b62bc3593641d45 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 23:26:04 +0100 Subject: [PATCH 196/448] don't steal focus for readonly editors --- .../agentFeedbackEditorInputContribution.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts index 707a1090912..dc2434cfb45 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts @@ -342,15 +342,29 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return; } + // Don't capture Escape at this level - let it fall through to the input handler if focused + if (e.keyCode === KeyCode.Escape) { + this._hide(); + this._editor.focus(); + return; + } + + // Ctrl+I / Cmd+I explicitly focuses the feedback input + if ((e.ctrlKey || e.metaKey) && e.keyCode === KeyCode.KeyI) { + e.preventDefault(); + e.stopPropagation(); + widget.inputElement.focus(); + return; + } + // Don't focus if any modifier is held (keyboard shortcuts) if (e.ctrlKey || e.altKey || e.metaKey) { return; } - // Don't capture Escape at this level - let it fall through to the input handler if focused - if (e.keyCode === KeyCode.Escape) { - this._hide(); - this._editor.focus(); + // Only auto-focus the input on typing when the document is readonly; + // when editable the user must click or use Ctrl+I to focus. + if (!this._editor.getOption(EditorOption.readOnly)) { return; } @@ -413,6 +427,12 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements })); } + focusInput(): void { + if (this._visible && this._widget) { + this._widget.inputElement.focus(); + } + } + private _addFeedback(): boolean { if (!this._widget) { return false; From 0cb758d145d60ee942c342bdad9756d746143985 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 23:27:32 +0100 Subject: [PATCH 197/448] maximize editor group on double click --- .../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 e94d1317988..44fb59656b3 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -40,6 +40,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'terminal.integrated.initialHint': false, + 'workbench.editor.doubleClickTabToToggleEditorGroupSizes': 'maximize', 'workbench.editor.restoreEditors': false, 'workbench.startupEditor': 'none', 'workbench.tips.enabled': false, From f48d290224c936a6aefa06bfb22ca1b9574d112a Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 23:37:42 +0100 Subject: [PATCH 198/448] better sessions terminal tracking --- .../browser/sessionsTerminalContribution.ts | 108 +++++++++++++++++- .../sessionsTerminalContribution.test.ts | 76 +++++++++++- 2 files changed, 176 insertions(+), 8 deletions(-) diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index ff375376d00..290fa8b309c 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { isEqualOrParent } from '../../../../base/common/extpath.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; @@ -14,7 +15,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkbenchContribution, getWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalInstance, ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { Menus } from '../../../browser/menus.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; @@ -83,16 +84,17 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben } })); - // When terminals are restored on startup, ensure visibility matches active session + // When terminals are created externally, try to relate them to the active session this._register(this._terminalService.onDidCreateInstance(instance => { if (this._isCreatingTerminal || this._activeKey === undefined) { return; } - // If this instance is not tracked by us, hide it + // If this instance is already tracked by us, nothing to do const activeIds = this._pathToInstanceIds.get(this._activeKey); - if (!activeIds?.has(instance.instanceId)) { - this._terminalService.moveToBackground(instance); + if (activeIds?.has(instance.instanceId)) { + return; } + this._tryAdoptTerminal(instance); })); } @@ -160,6 +162,58 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben ids.add(instanceId); } + /** + * Attempts to associate an externally-created terminal with the active + * session by checking whether its initial cwd falls within the active + * session's worktree or repository. Hides the terminal if it cannot be + * related. + */ + private async _tryAdoptTerminal(instance: ITerminalInstance): Promise { + let cwd: string | undefined; + try { + cwd = await instance.getInitialCwd(); + } catch { + return; + } + + if (instance.isDisposed) { + return; + } + + const activeKey = this._activeKey; + if (!activeKey) { + return; + } + + // Re-check tracking — the terminal may have been adopted while awaiting + const activeIds = this._pathToInstanceIds.get(activeKey); + if (activeIds?.has(instance.instanceId)) { + return; + } + + const session = this._sessionsManagementService.activeSession.get(); + if (cwd && this._isRelatedToSession(cwd, session, activeKey)) { + this._addInstanceToPath(activeKey, instance.instanceId); + this._logService.trace(`[SessionsTerminal] Adopted terminal ${instance.instanceId} with cwd ${cwd}`); + } else { + this._terminalService.moveToBackground(instance); + } + } + + /** + * Returns whether the given cwd falls within the active session's + * worktree, repository, or the current active key (home dir fallback). + */ + private _isRelatedToSession(cwd: string, session: IActiveSessionItem | undefined, activeKey: string): boolean { + if (isEqualOrParent(cwd, activeKey, true)) { + return true; + } + if (session?.providerType === AgentSessionProviders.Background && session.repository) { + return isEqualOrParent(cwd, session.repository.fsPath, true); + } + return false; + } + /** * Hides all foreground terminals that do not belong to the given active key * and shows all background terminals that do belong to it. @@ -199,6 +253,32 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben this._pathToInstanceIds.delete(key); } } + + async dumpTracking(): Promise { + const trackedInstanceIds = new Set(); + + console.log('[SessionsTerminal] === Tracked Terminals ==='); + for (const [key, ids] of this._pathToInstanceIds) { + for (const instanceId of ids) { + trackedInstanceIds.add(instanceId); + const instance = this._terminalService.getInstanceFromId(instanceId); + let cwd = ''; + if (instance) { + try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } + } + console.log(` ${instanceId} - ${cwd} - ${key}`); + } + } + + console.log('[SessionsTerminal] === Untracked Terminals ==='); + for (const instance of this._terminalService.instances) { + if (!trackedInstanceIds.has(instance.instanceId)) { + let cwd = ''; + try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } + console.log(` ${instance.instanceId} - ${cwd}`); + } + } + } } registerWorkbenchContribution2(SessionsTerminalContribution.ID, SessionsTerminalContribution, WorkbenchPhase.AfterRestored); @@ -231,3 +311,21 @@ class OpenSessionInTerminalAction extends Action2 { } registerAction2(OpenSessionInTerminalAction); + +class DumpTerminalTrackingAction extends Action2 { + + constructor() { + super({ + id: 'agentSession.dumpTerminalTracking', + title: localize2('dumpTerminalTracking', "Dump Terminal Tracking"), + f1: true, + }); + } + + override async run(): Promise { + const contribution = getWorkbenchContribution(SessionsTerminalContribution.ID); + await contribution.dumpTracking(); + } +} + +registerAction2(DumpTerminalTrackingAction); diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index c2108711f80..7305d4591b5 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -51,6 +51,14 @@ function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerT } as IActiveSessionItem; } +function makeTerminalInstance(id: number, cwd: string): ITerminalInstance { + return { + instanceId: id, + isDisposed: false, + getInitialCwd: () => Promise.resolve(cwd), + } as unknown as ITerminalInstance; +} + suite('SessionsTerminalContribution', () => { const store = new DisposableStore(); @@ -102,7 +110,9 @@ suite('SessionsTerminalContribution', () => { } override async createTerminal(opts?: any): Promise { const id = nextInstanceId++; - const instance = { instanceId: id } as ITerminalInstance; + const cwdUri: URI | undefined = opts?.config?.cwd; + const cwdStr = cwdUri?.fsPath ?? ''; + const instance = makeTerminalInstance(id, cwdStr); createdTerminals.push({ cwd: opts?.config?.cwd }); terminalInstances.set(id, instance); onDidCreateInstance.fire(instance); @@ -436,9 +446,10 @@ suite('SessionsTerminalContribution', () => { await tick(); // Simulate a terminal being restored (e.g. on startup) that is not tracked - const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + const restoredInstance = makeTerminalInstance(nextInstanceId++, '/some/other/path'); terminalInstances.set(restoredInstance.instanceId, restoredInstance); onDidCreateInstance.fire(restoredInstance); + await tick(); // The restored terminal should be moved to background assert.ok(moveToBackgroundCalls.includes(restoredInstance.instanceId), 'restored terminal should be backgrounded'); @@ -446,9 +457,10 @@ suite('SessionsTerminalContribution', () => { test('does not hide restored terminals before any session is active', async () => { // Simulate a terminal being restored before any session is active - const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + const restoredInstance = makeTerminalInstance(nextInstanceId++, '/some/path'); terminalInstances.set(restoredInstance.instanceId, restoredInstance); onDidCreateInstance.fire(restoredInstance); + await tick(); assert.strictEqual(moveToBackgroundCalls.length, 0, 'should not background before any session is active'); }); @@ -467,6 +479,64 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals.length, 1, 'should not create a new terminal'); assert.ok(showBackgroundCalls.includes(instanceId), 'should show the backgrounded terminal'); }); + + // --- Terminal adoption --- + + test('adopts externally-created terminal whose cwd matches the active worktree', async () => { + const worktree = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, worktree.fsPath); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'should not be hidden'); + // Verify it was adopted — ensureTerminal should reuse it + await contribution.ensureTerminal(worktree, false); + assert.strictEqual(createdTerminals.length, 1, 'should reuse adopted terminal, not create a second'); + }); + + test('adopts externally-created terminal whose cwd is a subdirectory of the active worktree', async () => { + const worktree = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, URI.file('/worktree/subdir').fsPath); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'subdirectory terminal should not be hidden'); + }); + + test('adopts externally-created terminal whose cwd matches the session repository', async () => { + const worktree = URI.file('/worktree'); + const repo = URI.file('/repo'); + activeSessionObs.set(makeAgentSession({ worktree, repository: repo, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, repo.fsPath); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'terminal at repository path should not be hidden'); + }); + + test('hides externally-created terminal whose cwd does not match the active session', async () => { + const worktree = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, '/unrelated/path'); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(moveToBackgroundCalls.includes(externalInstance.instanceId), 'unrelated terminal should be hidden'); + }); }); function tick(): Promise { From d356c797a720444d88f27c9d37beb6f59e6eb57e Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:08:23 -0800 Subject: [PATCH 199/448] Browser: make Playwright per workspace (#299055) * Browser: make Playwright per workspace * Feedback, fixes --- .../sharedProcess/sharedProcessMain.ts | 10 +-- .../browserView/common/browserViewGroup.ts | 3 +- .../platform/browserView/common/cdp/types.ts | 2 +- .../electron-main/browserViewGroup.ts | 5 +- .../browserViewGroupMainService.ts | 4 +- .../electron-main/browserViewMainService.ts | 10 ++- .../node/browserViewGroupRemoteService.ts | 16 ++-- .../browserView/node/playwrightChannel.ts | 82 +++++++++++++++++++ .../browserView/node/playwrightService.ts | 7 +- .../playwrightWorkbenchService.ts | 21 ++++- 10 files changed, 129 insertions(+), 31 deletions(-) create mode 100644 src/vs/platform/browserView/node/playwrightChannel.ts diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index e112b958d73..7f5cd7c26e9 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -134,9 +134,7 @@ import { IMcpGalleryManifestService } from '../../../platform/mcp/common/mcpGall import { McpGalleryManifestIPCService } from '../../../platform/mcp/common/mcpGalleryManifestServiceIpc.js'; import { IMeteredConnectionService } from '../../../platform/meteredConnection/common/meteredConnection.js'; import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js'; -import { IPlaywrightService } from '../../../platform/browserView/common/playwrightService.js'; -import { PlaywrightService } from '../../../platform/browserView/node/playwrightService.js'; -import { IBrowserViewGroupRemoteService, BrowserViewGroupRemoteService } from '../../../platform/browserView/node/browserViewGroupRemoteService.js'; +import { PlaywrightChannel } from '../../../platform/browserView/node/playwrightChannel.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -404,10 +402,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { // Web Content Extractor services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService)); - // Playwright - services.set(IBrowserViewGroupRemoteService, new SyncDescriptor(BrowserViewGroupRemoteService)); - services.set(IPlaywrightService, new SyncDescriptor(PlaywrightService)); - return new InstantiationService(services); } @@ -476,7 +470,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { this.server.registerChannel('sharedWebContentExtractor', webContentExtractorChannel); // Playwright - const playwrightChannel = ProxyChannel.fromService(accessor.get(IPlaywrightService), this._store); + const playwrightChannel = this._register(new PlaywrightChannel(this.server, accessor.get(IMainProcessService), accessor.get(ILogService))); this.server.registerChannel('playwright', playwrightChannel); } diff --git a/src/vs/platform/browserView/common/browserViewGroup.ts b/src/vs/platform/browserView/common/browserViewGroup.ts index 0f43b98c8b0..0c414d9df7b 100644 --- a/src/vs/platform/browserView/common/browserViewGroup.ts +++ b/src/vs/platform/browserView/common/browserViewGroup.ts @@ -51,9 +51,10 @@ export interface IBrowserViewGroupService { /** * Create a new browser view group. + * @param windowId The ID of the primary window the group should be associated with. * @returns The id of the newly created group. */ - createGroup(): Promise; + createGroup(windowId: number): Promise; /** * Destroy a browser view group. diff --git a/src/vs/platform/browserView/common/cdp/types.ts b/src/vs/platform/browserView/common/cdp/types.ts index 603467e3ed2..ca0256478c8 100644 --- a/src/vs/platform/browserView/common/cdp/types.ts +++ b/src/vs/platform/browserView/common/cdp/types.ts @@ -151,7 +151,7 @@ export interface ICDPBrowserTarget extends ICDPTarget { /** Get all available targets */ getTargets(): IterableIterator; /** Create a new target in the specified browser context */ - createTarget(url: string, browserContextId?: string): Promise; + createTarget(url: string, browserContextId?: string, windowId?: number): Promise; /** Activate a target (bring to foreground) */ activateTarget(target: ICDPTarget): Promise; /** Close a target */ diff --git a/src/vs/platform/browserView/electron-main/browserViewGroup.ts b/src/vs/platform/browserView/electron-main/browserViewGroup.ts index d7d59c27018..d68ba74efed 100644 --- a/src/vs/platform/browserView/electron-main/browserViewGroup.ts +++ b/src/vs/platform/browserView/electron-main/browserViewGroup.ts @@ -49,6 +49,7 @@ export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, I constructor( readonly id: string, + private readonly windowId: number, @IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService, @IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer, ) { @@ -127,12 +128,12 @@ export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, I return this.views.values(); } - async createTarget(url: string, browserContextId?: string): Promise { + async createTarget(url: string, browserContextId?: string, windowId = this.windowId): Promise { if (browserContextId && !this.knownContextIds.has(browserContextId)) { throw new Error(`Unknown browser context ${browserContextId}`); } - const target = await this.browserViewMainService.createTarget(url, browserContextId); + const target = await this.browserViewMainService.createTarget(url, browserContextId, windowId); if (target instanceof BrowserView) { await this.addView(target.id); } diff --git a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts index 20dd6331c0e..4cc7aadfe97 100644 --- a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts @@ -33,9 +33,9 @@ export class BrowserViewGroupMainService extends Disposable implements IBrowserV super(); } - async createGroup(): Promise { + async createGroup(windowId: number): Promise { const id = generateUuid(); - const group = this.instantiationService.createInstance(BrowserViewGroup, id); + const group = this.instantiationService.createInstance(BrowserViewGroup, id, windowId); this.groups.set(id, group); // Auto-cleanup when the group disposes itself diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index 3959717fb1d..c2a4e3aeefe 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -19,6 +19,7 @@ import { IProductService } from '../../product/common/productService.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; import { logBrowserOpen } from '../common/browserViewTelemetry.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; export const IBrowserViewMainService = createDecorator('browserViewMainService'); @@ -158,7 +159,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this.browserViews.values(); } - async createTarget(url: string, browserContextId?: string): Promise { + async createTarget(url: string, browserContextId?: string, windowId?: number): Promise { const targetId = generateUuid(); const browserSession = browserContextId && BrowserSession.get(browserContextId) || BrowserSession.getOrCreateEphemeral(targetId); @@ -167,8 +168,13 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa logBrowserOpen(this.telemetryService, 'cdpCreated'); + const window = windowId !== undefined ? this.windowsMainService.getWindowById(windowId) : this.windowsMainService.getFocusedWindow(); + if (!window) { + throw new Error(`Window ${windowId} not found`); + } + // Request the workbench to open the editor - this.windowsMainService.sendToFocused('vscode:runAction', { + window.sendWhenReady('vscode:runAction', CancellationToken.None, { id: '_workbench.open', args: [BrowserViewUri.forUrl(url, targetId), [undefined, { preserveFocus: true }], undefined] }); diff --git a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts index b4aaffb612d..3035a6828df 100644 --- a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts +++ b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts @@ -6,12 +6,9 @@ import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { IBrowserViewGroup, IBrowserViewGroupService, IBrowserViewGroupViewEvent, ipcBrowserViewGroupChannelName } from '../common/browserViewGroup.js'; -export const IBrowserViewGroupRemoteService = createDecorator('browserViewGroupRemoteService'); - /** * Remote-process service for managing browser view groups. * @@ -22,12 +19,11 @@ export const IBrowserViewGroupRemoteService = createDecorator; + createGroup(windowId: number): Promise; } /** @@ -79,20 +75,18 @@ class RemoteBrowserViewGroup extends Disposable implements IBrowserViewGroup { } export class BrowserViewGroupRemoteService implements IBrowserViewGroupRemoteService { - declare readonly _serviceBrand: undefined; - private readonly _groupService: IBrowserViewGroupService; private readonly _groups = new Map(); constructor( - @IMainProcessService mainProcessService: IMainProcessService, + mainProcessService: IMainProcessService, ) { const channel = mainProcessService.getChannel(ipcBrowserViewGroupChannelName); this._groupService = ProxyChannel.toService(channel); } - async createGroup(): Promise { - const id = await this._groupService.createGroup(); + async createGroup(windowId: number): Promise { + const id = await this._groupService.createGroup(windowId); return this._wrap(id); } diff --git a/src/vs/platform/browserView/node/playwrightChannel.ts b/src/vs/platform/browserView/node/playwrightChannel.ts new file mode 100644 index 00000000000..ca3e83c00a2 --- /dev/null +++ b/src/vs/platform/browserView/node/playwrightChannel.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../base/common/event.js'; +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { IPCServer, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { ILogService } from '../../log/common/log.js'; +import { BrowserViewGroupRemoteService } from './browserViewGroupRemoteService.js'; +import { PlaywrightService } from './playwrightService.js'; + +/** + * IPC channel for the Playwright service. + * + * Each connected window gets its own {@link PlaywrightService}, + * keyed by the opaque IPC connection context. The client sends an + * `__initialize` call with its numeric window ID before any other + * method calls, which eagerly creates the instance. When a window + * disconnects the instance is automatically disposed. + */ +export class PlaywrightChannel extends Disposable implements IServerChannel { + + private readonly _instances = this._register(new DisposableMap()); + private readonly browserViewGroupRemoteService: BrowserViewGroupRemoteService; + + constructor( + ipcServer: IPCServer, + mainProcessService: IMainProcessService, + private readonly logService: ILogService, + ) { + super(); + this.browserViewGroupRemoteService = new BrowserViewGroupRemoteService(mainProcessService); + this._register(ipcServer.onDidRemoveConnection(c => { + this._instances.deleteAndDispose(c.ctx); + })); + } + + listen(ctx: string, event: string): Event { + const instance = this._instances.get(ctx); + if (!instance) { + throw new Error(`Window not initialized for context: ${ctx}`); + } + const source = (instance as unknown as Record>)[event]; + if (typeof source !== 'function') { + throw new Error(`Event not found: ${event}`); + } + return source as Event; + } + + call(ctx: string, command: string, arg?: unknown): Promise { + // Handle the one-time initialization call that creates the instance + if (command === '__initialize') { + if (typeof arg !== 'number') { + throw new Error(`Invalid argument for __initialize: expected window ID as number, got ${typeof arg}`); + } + if (!this._instances.has(ctx)) { + const windowId = arg as number; + this._instances.set(ctx, new PlaywrightService(windowId, this.browserViewGroupRemoteService, this.logService)); + } + return Promise.resolve(undefined as T); + } + + const instance = this._instances.get(ctx); + if (!instance) { + throw new Error(`Window not initialized for context: ${ctx}`); + } + + const target = (instance as unknown as Record)[command]; + if (typeof target !== 'function') { + throw new Error(`Method not found: ${command}`); + } + + const methodArgs = Array.isArray(arg) ? arg : []; + let res = target.apply(instance, methodArgs); + if (!(res instanceof Promise)) { + res = Promise.resolve(res); + } + return res; + } +} diff --git a/src/vs/platform/browserView/node/playwrightService.ts b/src/vs/platform/browserView/node/playwrightService.ts index 0a55710ec61..9afb7963d66 100644 --- a/src/vs/platform/browserView/node/playwrightService.ts +++ b/src/vs/platform/browserView/node/playwrightService.ts @@ -32,8 +32,9 @@ export class PlaywrightService extends Disposable implements IPlaywrightService private _initPromise: Promise | undefined; constructor( - @IBrowserViewGroupRemoteService private readonly browserViewGroupRemoteService: IBrowserViewGroupRemoteService, - @ILogService private readonly logService: ILogService, + private readonly windowId: number, + private readonly browserViewGroupRemoteService: IBrowserViewGroupRemoteService, + private readonly logService: ILogService, ) { super(); this._pages = this._register(new PlaywrightPageManager(logService)); @@ -76,7 +77,7 @@ export class PlaywrightService extends Disposable implements IPlaywrightService this._initPromise = (async () => { try { this.logService.debug('[PlaywrightService] Creating browser view group'); - const group = await this.browserViewGroupRemoteService.createGroup(); + const group = await this.browserViewGroupRemoteService.createGroup(this.windowId); this.logService.debug('[PlaywrightService] Connecting to browser via CDP'); const playwright = await import('playwright-core'); diff --git a/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts b/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts index f50672fd913..12dcd6a2b03 100644 --- a/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts +++ b/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts @@ -3,7 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { mainWindow } from '../../../../base/browser/window.js'; +import { IChannel, ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IPlaywrightService } from '../../../../platform/browserView/common/playwrightService.js'; import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; -registerSharedProcessRemoteService(IPlaywrightService, 'playwright'); +class PlaywrightChannelClient { + constructor( + channel: IChannel, + @ILogService logService: ILogService + ) { + /** + * send the current window's ID once via `__initialize`, so the server-side {@link PlaywrightChannel} + * can create a per-window {@link PlaywrightWindowInstance}. All subsequent calls and events are proxied directly. + */ + void channel.call('__initialize', mainWindow.vscodeWindowId).catch((e) => { + logService.error(`Failed to initialize Playwright service`, e); + }); + return ProxyChannel.toService(channel); + } +} + +registerSharedProcessRemoteService(IPlaywrightService, 'playwright', { channelClientCtor: PlaywrightChannelClient }); From e563f3c801ebf8b50a65fc52a6d7cd5305021c56 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 4 Mar 2026 18:22:53 -0500 Subject: [PATCH 200/448] test change (#299310) --- src/vs/workbench/contrib/chat/browser/chatTipService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 40a007908dd..593df2b7657 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -41,9 +41,7 @@ type ChatTipClassification = { }; // Re-export tracking commands for backwards compatibility -export { - TipTrackingCommands, -}; +export { TipTrackingCommands }; /** @deprecated Use TipTrackingCommands.AttachFilesReferenceUsed */ export const ATTACH_FILES_REFERENCE_TRACKING_COMMAND = TipTrackingCommands.AttachFilesReferenceUsed; /** @deprecated Use TipTrackingCommands.CreateAgentInstructionsUsed */ From 01fea9a3690f163847dedcb5979babce66fb6e2f Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:33:42 -0800 Subject: [PATCH 201/448] Enhance slash command expansion to include prompt type in message (#299305) * Enhance slash command expansion to include prompt type in message * Update src/vs/sessions/contrib/chat/browser/slashCommands.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/chat/browser/slashCommands.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/slashCommands.ts b/src/vs/sessions/contrib/chat/browser/slashCommands.ts index bda010579a6..4cf481f915e 100644 --- a/src/vs/sessions/contrib/chat/browser/slashCommands.ts +++ b/src/vs/sessions/contrib/chat/browser/slashCommands.ts @@ -23,6 +23,7 @@ import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../.. import { AICustomizationManagementCommands, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; /** * Static command ID used by completion items to trigger immediate slash command execution, @@ -128,7 +129,8 @@ export class SlashCommandHandler extends Disposable { const args = match[2]?.trim() ?? ''; const uri = promptCommand.promptPath.uri; - const expanded = `Use the prompt file located at [${promptCommand.name}](${uri.toString()}).`; + const typeLabel = promptCommand.promptPath.type === PromptsType.skill ? 'skill' : 'prompt file'; + const expanded = `Use the ${typeLabel} located at [${promptCommand.name}](${uri.toString()}).`; return args ? `${expanded} ${args}` : expanded; } From 093376241c3bc126b3fdfdbb5f262ce549c1f9ff Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 4 Mar 2026 15:45:55 -0800 Subject: [PATCH 202/448] Sessions window: contributed pr actions --- extensions/github/package.json | 12 ------------ .../contrib/changesView/browser/changesView.ts | 13 ++++++------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index 815c6452706..78577f2192d 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -184,18 +184,6 @@ } ], "chat/input/editing/sessionApplyActions": [ - { - "command": "github.createPullRequest", - "group": "navigation", - "order": 1, - "when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && !github.hasOpenPullRequest" - }, - { - "command": "github.openPullRequest", - "group": "navigation", - "order": 1, - "when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && github.hasOpenPullRequest" - } ] }, "configuration": [ diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 1bed45ff7c6..3dcdffcec1d 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -54,7 +54,6 @@ import { createFileIconThemableTreeContainerScope } from '../../../../workbench/ import { IActivityService, NumberBadge } from '../../../../workbench/services/activity/common/activity.js'; import { IEditorService, MODAL_GROUP, SIDE_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; @@ -254,7 +253,6 @@ export class ChangesViewPane extends ViewPane { @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, @ILabelService private readonly labelService: ILabelService, @IStorageService private readonly storageService: IStorageService, - @ICommandService private readonly commandService: ICommandService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -561,12 +559,16 @@ export class ChangesViewPane extends ViewPane { return files > 0; })); - // Check if a PR exists when the active session changes + // Set context key for PR state from session metadata + const hasOpenPullRequestKey = scopedContextKeyService.createKey('github.copilot.chat.copilotCLI.hasOpenPullRequest', false); this.renderDisposables.add(autorun(reader => { const sessionResource = activeSessionResource.read(reader); + sessionsChangedSignal.read(reader); if (sessionResource) { const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; - this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ }); + hasOpenPullRequestKey.set(!!metadata?.pullRequestUrl); + } else { + hasOpenPullRequestKey.set(false); } })); @@ -592,9 +594,6 @@ export class ChangesViewPane extends ViewPane { ); return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } - if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') { - return { showIcon: true, showLabel: true, isSecondary: true }; - } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; } From 5df46cbf7e1a741a887a359728b18513f3dbd2de Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 4 Mar 2026 16:01:44 -0800 Subject: [PATCH 203/448] CSS change --- .../sessions/contrib/changesView/browser/media/changesView.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesView.css b/src/vs/sessions/contrib/changesView/browser/media/changesView.css index 4f6d74f525c..21cddd20fd8 100644 --- a/src/vs/sessions/contrib/changesView/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changesView/browser/media/changesView.css @@ -104,7 +104,6 @@ .changes-view-body .chat-editing-session-actions.outside-card .monaco-button { height: 26px; padding: 4px 14px; - border-radius: 4px; font-size: 12px; line-height: 18px; } From 5b2bd4495bc6618986cb894ab2f28b06395f4a63 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:16:59 -0800 Subject: [PATCH 204/448] Action widget: hover background for open pickers (#299301) * Action widget: full-width separators and hover background for open pickers - Remove horizontal padding from action widget, inset list rows instead so separators span edge-to-edge - Fire onDidChangeVisibility in ActionWidgetDropdown so aria-expanded is set correctly for all picker types - Apply toolbar hover background to picker buttons while their dropdown is open * Revert separator layout hack, keep hover background for open pickers --- .../platform/actionWidget/browser/actionWidgetDropdown.ts | 3 +++ .../contrib/chat/browser/widget/input/chatModelPicker.ts | 2 +- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index 2b2022fb435..82e6ff581ba 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -170,6 +170,7 @@ export class ActionWidgetDropdown extends BaseDropdown { action.run(); }, onHide: () => { + this.hide(); if (isHTMLElement(previouslyFocusedElement)) { previouslyFocusedElement.focus(); } @@ -221,6 +222,8 @@ export class ActionWidgetDropdown extends BaseDropdown { getWidgetRole: () => 'menu', }; + super.show(); + this.actionWidgetService.show( this._options.label ?? '', false, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 2011b0f95e9..ce390754ad2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -584,7 +584,7 @@ export class ModelPickerWidget extends Disposable { filterPlaceholder: localize('chat.modelPicker.search', "Search models"), focusFilterOnOpen: true, collapsedByDefault: new Set([ModelPickerSection.Other]), - minWidth: 300, + minWidth: 200, }; const previouslyFocusedElement = dom.getActiveElement(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index dd200620d82..e152c00e58a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1508,6 +1508,12 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-icon-foreground); } +/* Keep hover background while picker dropdown is open */ +.interactive-session .chat-input-toolbar .action-label[aria-expanded="true"], +.interactive-session .chat-secondary-toolbar .action-label[aria-expanded="true"] { + background-color: var(--vscode-toolbar-hoverBackground); +} + /* When chevrons are hidden and only showing an icon (no label), size to 22x22 with centered icon */ .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)), .interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:not(:has(.chat-input-picker-label)), From 0485c21d6bfd5cbb02bbafaf09da31eaf713ad01 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 16:17:38 -0800 Subject: [PATCH 205/448] plugins: refactor pluginSources with proper deletion (#299319) * plugins: refactor pluginSources with proper deletion - Consolidate source logic into IPluginSource implementations - Use that to implement more robust cleanup logic Closes https://github.com/microsoft/vscode/issues/297251 * pr comments --- .../browser/agentPluginRepositoryService.ts | 235 +++----- .../chat/browser/pluginInstallService.ts | 255 ++------- .../contrib/chat/browser/pluginSources.ts | 518 ++++++++++++++++++ .../plugins/agentPluginRepositoryService.ts | 20 +- .../common/plugins/agentPluginServiceImpl.ts | 14 +- .../chat/common/plugins/pluginSource.ts | 63 +++ .../agentPluginRepositoryService.test.ts | 189 +++++++ .../plugins/pluginInstallService.test.ts | 73 ++- 8 files changed, 988 insertions(+), 379 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/pluginSources.ts create mode 100644 src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts index 41abb16b8f8..49a0f694d65 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts @@ -6,12 +6,13 @@ import { Action } from '../../../../base/common/actions.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { revive } from '../../../../base/common/marshalling.js'; -import { dirname, isEqualOrParent, joinPath } from '../../../../base/common/resources.js'; +import { dirname, isEqual, isEqualOrParent, joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; @@ -19,6 +20,8 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import type { Dto } from '../../../services/extensions/common/proxyIdentifier.js'; import { IAgentPluginRepositoryService, IEnsureRepositoryOptions, IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js'; import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceReferenceKind, MarketplaceType, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IPluginSource } from '../common/plugins/pluginSource.js'; +import { GitHubPluginSource, GitUrlPluginSource, NpmPluginSource, PipPluginSource, RelativePathPluginSource } from './pluginSources.js'; const MARKETPLACE_INDEX_STORAGE_KEY = 'chat.plugins.marketplaces.index.v1'; @@ -34,17 +37,37 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi private readonly _cacheRoot: URI; private readonly _marketplaceIndex = new Lazy>(() => this._loadMarketplaceIndex()); + private readonly _pluginSources: ReadonlyMap; constructor( @ICommandService private readonly _commandService: ICommandService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService private readonly _fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @INotificationService private readonly _notificationService: INotificationService, @IProgressService private readonly _progressService: IProgressService, @IStorageService private readonly _storageService: IStorageService, ) { this._cacheRoot = joinPath(environmentService.cacheHome, 'agentPlugins'); + + // Build per-kind source repository map via instantiation service so + // each repository can inject its own dependencies. + this._pluginSources = new Map([ + [PluginSourceKind.RelativePath, new RelativePathPluginSource()], + [PluginSourceKind.GitHub, instantiationService.createInstance(GitHubPluginSource)], + [PluginSourceKind.GitUrl, instantiationService.createInstance(GitUrlPluginSource)], + [PluginSourceKind.Npm, instantiationService.createInstance(NpmPluginSource)], + [PluginSourceKind.Pip, instantiationService.createInstance(PipPluginSource)], + ]); + } + + getPluginSource(kind: PluginSourceKind): IPluginSource { + const repo = this._pluginSources.get(kind); + if (!repo) { + throw new Error(`No source repository registered for kind '${kind}'`); + } + return repo; } getRepositoryUri(marketplace: IMarketplaceReference, marketplaceType?: MarketplaceType): URI { @@ -214,176 +237,74 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi } getPluginSourceInstallUri(sourceDescriptor: IPluginSourceDescriptor): URI { - switch (sourceDescriptor.kind) { - case PluginSourceKind.RelativePath: - throw new Error('Use getPluginInstallUri() for relative-path sources'); - case PluginSourceKind.GitHub: { - const [owner, repo] = sourceDescriptor.repo.split('/'); - return joinPath(this._cacheRoot, 'github.com', owner, repo, ...this._getSourceRevisionCacheSuffix(sourceDescriptor)); - } - case PluginSourceKind.GitUrl: { - const segments = this._gitUrlCacheSegments(sourceDescriptor.url, sourceDescriptor.ref, sourceDescriptor.sha); - return joinPath(this._cacheRoot, ...segments); - } - case PluginSourceKind.Npm: - return joinPath(this._cacheRoot, 'npm', sanitizePackageName(sourceDescriptor.package), 'node_modules', sourceDescriptor.package); - case PluginSourceKind.Pip: - return joinPath(this._cacheRoot, 'pip', sanitizePackageName(sourceDescriptor.package)); - } + return this.getPluginSource(sourceDescriptor.kind).getInstallUri(this._cacheRoot, sourceDescriptor); } async ensurePluginSource(plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise { - const descriptor = plugin.sourceDescriptor; - switch (descriptor.kind) { - case PluginSourceKind.RelativePath: - return this.ensureRepository(plugin.marketplaceReference, options); - case PluginSourceKind.GitHub: { - const cloneUrl = `https://github.com/${descriptor.repo}.git`; - const repoDir = this.getPluginSourceInstallUri(descriptor); - const repoExists = await this._fileService.exists(repoDir); - if (repoExists) { - await this._checkoutPluginSourceRevision(repoDir, descriptor, options?.failureLabel ?? descriptor.repo); - return repoDir; - } - const progressTitle = options?.progressTitle ?? localize('cloningPluginSource', "Cloning plugin source '{0}'...", descriptor.repo); - const failureLabel = options?.failureLabel ?? descriptor.repo; - await this._cloneRepository(repoDir, cloneUrl, progressTitle, failureLabel, descriptor.ref); - await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); - return repoDir; - } - case PluginSourceKind.GitUrl: { - const repoDir = this.getPluginSourceInstallUri(descriptor); - const repoExists = await this._fileService.exists(repoDir); - if (repoExists) { - await this._checkoutPluginSourceRevision(repoDir, descriptor, options?.failureLabel ?? descriptor.url); - return repoDir; - } - const progressTitle = options?.progressTitle ?? localize('cloningPluginSourceUrl', "Cloning plugin source '{0}'...", descriptor.url); - const failureLabel = options?.failureLabel ?? descriptor.url; - await this._cloneRepository(repoDir, descriptor.url, progressTitle, failureLabel, descriptor.ref); - await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); - return repoDir; - } - case PluginSourceKind.Npm: { - // npm/pip install directories are managed by the install service. - // Return the expected install URI without performing installation. - return joinPath(this._cacheRoot, 'npm', sanitizePackageName(descriptor.package)); - } - case PluginSourceKind.Pip: { - return joinPath(this._cacheRoot, 'pip', sanitizePackageName(descriptor.package)); - } + const repo = this.getPluginSource(plugin.sourceDescriptor.kind); + if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { + return this.ensureRepository(plugin.marketplaceReference, options); } + return repo.ensure(this._cacheRoot, plugin, options); } async updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { - const descriptor = plugin.sourceDescriptor; - if (descriptor.kind !== PluginSourceKind.GitHub && descriptor.kind !== PluginSourceKind.GitUrl) { + const repo = this.getPluginSource(plugin.sourceDescriptor.kind); + if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { + return this.pullRepository(plugin.marketplaceReference, options); + } + return repo.update(this._cacheRoot, plugin, options); + } + + async cleanupPluginSource(plugin: IMarketplacePlugin): Promise { + const repo = this.getPluginSource(plugin.sourceDescriptor.kind); + const cleanupDir = repo.getCleanupTarget(this._cacheRoot, plugin.sourceDescriptor); + if (!cleanupDir) { return; } - const repoDir = this.getPluginSourceInstallUri(descriptor); - const repoExists = await this._fileService.exists(repoDir); - if (!repoExists) { - this._logService.warn(`[AgentPluginRepositoryService] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`); - return; - } - - const updateLabel = options?.pluginName ?? plugin.name; - const failureLabel = options?.failureLabel ?? updateLabel; - try { - await this._progressService.withProgress( - { - location: ProgressLocation.Notification, - title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel), - cancellable: false, - }, - async () => { - await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); - if (descriptor.sha) { - await this._commandService.executeCommand('git.fetch', repoDir.fsPath); - } else { - await this._commandService.executeCommand('_git.pull', repoDir.fsPath); - } - await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); + const exists = await this._fileService.exists(cleanupDir); + if (exists) { + await this._fileService.del(cleanupDir, { recursive: true }); + this._logService.info(`[${plugin.sourceDescriptor.kind}] Removed plugin cache: ${cleanupDir.toString()}`); + } + } catch (err) { + this._logService.warn(`[${plugin.sourceDescriptor.kind}] Failed to remove plugin cache '${cleanupDir.toString()}':`, err); + } + + try { + // Prune empty parent directories up to (but not including) the cache root + // so we don't leave dangling owner/authority folders behind. + await this._pruneEmptyParents(cleanupDir); + } catch (err) { + this._logService.warn(`[${plugin.sourceDescriptor.kind}] Failed to cleanup plugin source:`, err); + } + } + + /** + * Walk from {@link child}'s parent toward {@link _cacheRoot}, removing + * each directory that is empty. Stops as soon as a non-empty directory + * is found or the cache root is reached. Only operates on descendants + * of the cache root — returns immediately for paths outside it. + */ + private async _pruneEmptyParents(child: URI): Promise { + if (!isEqualOrParent(child, this._cacheRoot)) { + return; + } + let current = dirname(child); + while (isEqualOrParent(current, this._cacheRoot) && !isEqual(current, this._cacheRoot)) { + try { + const stat = await this._fileService.resolve(current); + if (stat.children && stat.children.length > 0) { + break; } - ); - } catch (err) { - this._logService.error(`[AgentPluginRepositoryService] Failed to update plugin source ${updateLabel}:`, err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), - actions: { - primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { - this._commandService.executeCommand('git.showOutput'); - })], - }, - }); - } - } - - private _gitUrlCacheSegments(url: string, ref?: string, sha?: string): string[] { - try { - const parsed = URI.parse(url); - const authority = (parsed.authority || 'unknown').replace(/[\\/:*?"<>|]/g, '_').toLowerCase(); - const pathPart = parsed.path.replace(/^\/+/, '').replace(/\.git$/i, '').replace(/\/+$/g, ''); - const segments = pathPart.split('/').map(s => s.replace(/[\\/:*?"<>|]/g, '_')); - return [authority, ...segments, ...this._getSourceRevisionCacheSuffix(ref, sha)]; - } catch { - return ['git', url.replace(/[\\/:*?"<>|]/g, '_'), ...this._getSourceRevisionCacheSuffix(ref, sha)]; - } - } - - private _getSourceRevisionCacheSuffix(descriptorOrRef: IPluginSourceDescriptor | string | undefined, sha?: string): string[] { - if (typeof descriptorOrRef === 'object' && descriptorOrRef) { - if (descriptorOrRef.kind === PluginSourceKind.GitHub || descriptorOrRef.kind === PluginSourceKind.GitUrl) { - return this._getSourceRevisionCacheSuffix(descriptorOrRef.ref, descriptorOrRef.sha); + await this._fileService.del(current); + } catch { + break; } - return []; - } - - const ref = descriptorOrRef; - if (sha) { - return [`sha_${sanitizePackageName(sha)}`]; - } - if (ref) { - return [`ref_${sanitizePackageName(ref)}`]; - } - return []; - } - - private async _checkoutPluginSourceRevision(repoDir: URI, descriptor: IPluginSourceDescriptor, failureLabel: string): Promise { - if (descriptor.kind !== PluginSourceKind.GitHub && descriptor.kind !== PluginSourceKind.GitUrl) { - return; - } - - if (!descriptor.sha && !descriptor.ref) { - return; - } - - try { - if (descriptor.sha) { - await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, descriptor.sha, true); - return; - } - - await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, descriptor.ref); - } catch (err) { - this._logService.error(`[AgentPluginRepositoryService] Failed to checkout plugin source revision for ${failureLabel}:`, err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('checkoutPluginSourceFailed', "Failed to checkout plugin '{0}' to requested revision: {1}", failureLabel, err?.message ?? String(err)), - actions: { - primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { - this._commandService.executeCommand('git.showOutput'); - })], - }, - }); - throw err; + current = dirname(current); } } -} -function sanitizePackageName(name: string): string { - return name.replace(/[\\/:*?"<>|]/g, '_'); } diff --git a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts index ae1bbf6cf4a..5beccafafbc 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts @@ -3,21 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancelablePromise, timeout } from '../../../../base/common/async.js'; -import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { isWindows } from '../../../../base/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; -import { TerminalCapability, type ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; -import { ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js'; import { IAgentPluginRepositoryService } from '../common/plugins/agentPluginRepositoryService.js'; import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; -import { getPluginSourceLabel, IMarketplacePlugin, INpmPluginSource, IPipPluginSource, IPluginMarketplaceService, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IMarketplacePlugin, IPluginMarketplaceService, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; export class PluginInstallService implements IPluginInstallService { declare readonly _serviceBrand: undefined; @@ -27,46 +20,38 @@ export class PluginInstallService implements IPluginInstallService { @IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService, @IFileService private readonly _fileService: IFileService, @INotificationService private readonly _notificationService: INotificationService, - @IDialogService private readonly _dialogService: IDialogService, - @ITerminalService private readonly _terminalService: ITerminalService, - @IProgressService private readonly _progressService: IProgressService, @ILogService private readonly _logService: ILogService, ) { } async installPlugin(plugin: IMarketplacePlugin): Promise { - switch (plugin.sourceDescriptor.kind) { - case PluginSourceKind.RelativePath: - return this._installRelativePathPlugin(plugin); - case PluginSourceKind.GitHub: - case PluginSourceKind.GitUrl: - return this._installGitPlugin(plugin); - case PluginSourceKind.Npm: - return this._installNpmPlugin(plugin, plugin.sourceDescriptor); - case PluginSourceKind.Pip: - return this._installPipPlugin(plugin, plugin.sourceDescriptor); + const kind = plugin.sourceDescriptor.kind; + + if (kind === PluginSourceKind.RelativePath) { + return this._installRelativePathPlugin(plugin); } + + if (kind === PluginSourceKind.Npm || kind === PluginSourceKind.Pip) { + return this._installPackagePlugin(plugin); + } + + // GitHub / GitUrl + return this._installGitPlugin(plugin); } async updatePlugin(plugin: IMarketplacePlugin): Promise { - switch (plugin.sourceDescriptor.kind) { - case PluginSourceKind.RelativePath: - return this._pluginRepositoryService.pullRepository(plugin.marketplaceReference, { - pluginName: plugin.name, - failureLabel: plugin.name, - marketplaceType: plugin.marketplaceType, - }); - case PluginSourceKind.GitHub: - case PluginSourceKind.GitUrl: - return this._pluginRepositoryService.updatePluginSource(plugin, { - pluginName: plugin.name, - failureLabel: plugin.name, - marketplaceType: plugin.marketplaceType, - }); - case PluginSourceKind.Npm: - return this._installNpmPlugin(plugin, plugin.sourceDescriptor); - case PluginSourceKind.Pip: - return this._installPipPlugin(plugin, plugin.sourceDescriptor); + const kind = plugin.sourceDescriptor.kind; + + if (kind === PluginSourceKind.Npm || kind === PluginSourceKind.Pip) { + // Package-manager "update" re-runs install via terminal + return this._installPackagePlugin(plugin); } + + // For relative-path and git sources, delegate to repository service + return this._pluginRepositoryService.updatePluginSource(plugin, { + pluginName: plugin.name, + failureLabel: plugin.name, + marketplaceType: plugin.marketplaceType, + }); } getPluginInstallUri(plugin: IMarketplacePlugin): URI { @@ -115,6 +100,7 @@ export class PluginInstallService implements IPluginInstallService { // --- GitHub / Git URL source (independent clone) -------------------------- private async _installGitPlugin(plugin: IMarketplacePlugin): Promise { + const repo = this._pluginRepositoryService.getPluginSource(plugin.sourceDescriptor.kind); let pluginDir: URI; try { pluginDir = await this._pluginRepositoryService.ensurePluginSource(plugin, { @@ -130,7 +116,7 @@ export class PluginInstallService implements IPluginInstallService { if (!pluginExists) { this._notificationService.notify({ severity: Severity.Error, - message: localize('pluginSourceNotFound', "Plugin source '{0}' not found after cloning.", getPluginSourceLabel(plugin.sourceDescriptor)), + message: localize('pluginSourceNotFound', "Plugin source '{0}' not found after cloning.", repo.getLabel(plugin.sourceDescriptor)), }); return; } @@ -138,186 +124,25 @@ export class PluginInstallService implements IPluginInstallService { this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); } - // --- npm source ----------------------------------------------------------- + // --- Package-manager sources (npm / pip) ---------------------------------- - private async _installNpmPlugin(plugin: IMarketplacePlugin, source: INpmPluginSource): Promise { - const packageSpec = source.version ? `${source.package}@${source.version}` : source.package; + private async _installPackagePlugin(plugin: IMarketplacePlugin): Promise { + const repo = this._pluginRepositoryService.getPluginSource(plugin.sourceDescriptor.kind); + if (!repo.runInstall) { + this._logService.error(`[PluginInstallService] Expected package repository for kind '${plugin.sourceDescriptor.kind}'`); + return; + } + + // Ensure the parent cache directory exists (returns npm/ or pip/) const installDir = await this._pluginRepositoryService.ensurePluginSource(plugin); - const args = ['npm', 'install', '--prefix', installDir.fsPath, packageSpec]; - if (source.registry) { - args.push('--registry', source.registry); - } - const command = this._formatShellCommand(args); + // The actual plugin content location (e.g. npm//node_modules/) + const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(plugin.sourceDescriptor); - const confirmed = await this._confirmTerminalCommand(plugin.name, command); - if (!confirmed) { + const result = await repo.runInstall(installDir, pluginDir, plugin); + if (!result) { return; } - const { success, terminal } = await this._runTerminalCommand( - command, - localize('installingNpmPlugin', "Installing npm plugin '{0}'...", plugin.name), - ); - if (!success) { - return; - } - - const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(source); - const pluginExists = await this._fileService.exists(pluginDir); - if (!pluginExists) { - this._notificationService.notify({ - severity: Severity.Error, - message: localize('npmPluginNotFound', "npm package '{0}' was not found after installation.", source.package), - }); - return; - } - - terminal?.dispose(); - this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); - } - - // --- pip source ----------------------------------------------------------- - - private async _installPipPlugin(plugin: IMarketplacePlugin, source: IPipPluginSource): Promise { - const packageSpec = source.version ? `${source.package}==${source.version}` : source.package; - const installDir = await this._pluginRepositoryService.ensurePluginSource(plugin); - const args = ['pip', 'install', '--target', installDir.fsPath, packageSpec]; - if (source.registry) { - args.push('--index-url', source.registry); - } - const command = this._formatShellCommand(args); - - const confirmed = await this._confirmTerminalCommand(plugin.name, command); - if (!confirmed) { - return; - } - - const { success, terminal } = await this._runTerminalCommand( - command, - localize('installingPipPlugin', "Installing pip plugin '{0}'...", plugin.name), - ); - if (!success) { - return; - } - - const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(source); - const pluginExists = await this._fileService.exists(pluginDir); - if (!pluginExists) { - this._notificationService.notify({ - severity: Severity.Error, - message: localize('pipPluginNotFound', "pip package '{0}' was not found after installation.", source.package), - }); - return; - } - - terminal?.dispose(); - this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); - } - - // --- Helpers -------------------------------------------------------------- - - private async _confirmTerminalCommand(pluginName: string, command: string): Promise { - const { confirmed } = await this._dialogService.confirm({ - type: 'question', - message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName), - detail: localize('confirmPluginInstallDetail', "This will run the following command in a terminal:\n\n{0}", command), - primaryButton: localize({ key: 'confirmInstall', comment: ['&& denotes a mnemonic'] }, "&&Install"), - }); - return confirmed; - } - - private async _runTerminalCommand(command: string, progressTitle: string) { - let terminal: ITerminalInstance | undefined; - try { - await this._progressService.withProgress( - { - location: ProgressLocation.Notification, - title: progressTitle, - cancellable: false, - }, - async () => { - terminal = await this._terminalService.createTerminal({ - config: { - name: localize('pluginInstallTerminal', "Plugin Install"), - forceShellIntegration: true, - isTransient: true, - isFeatureTerminal: true, - }, - }); - - await terminal.processReady; - this._terminalService.setActiveInstance(terminal); - - const commandResultPromise = this._waitForTerminalCommandCompletion(terminal); - await terminal.runCommand(command, true); - const exitCode = await commandResultPromise; - if (exitCode !== 0) { - throw new Error(localize('terminalCommandExitCode', "Command exited with code {0}", exitCode)); - } - } - ); - return { success: true, terminal }; - } catch (err) { - this._logService.error('[PluginInstallService] Terminal command failed:', err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('terminalCommandFailed', "Plugin installation command failed: {0}", err?.message ?? String(err)), - }); - return { success: false, terminal }; - } - } - - private _waitForTerminalCommandCompletion(terminal: ITerminalInstance): Promise { - return new Promise(resolve => { - const disposables = new DisposableStore(); - let isResolved = false; - - const resolveAndDispose = (exitCode: number | undefined): void => { - if (isResolved) { - return; - } - isResolved = true; - disposables.dispose(); - resolve(exitCode); - }; - - const attachCommandFinishedListener = (): void => { - const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection); - if (!commandDetection) { - return; - } - - disposables.add(commandDetection.onCommandFinished((command: ITerminalCommand) => { - resolveAndDispose(command.exitCode ?? 0); - })); - }; - - attachCommandFinishedListener(); - disposables.add(terminal.capabilities.onDidAddCommandDetectionCapability(() => attachCommandFinishedListener())); - - const timeoutHandle: CancelablePromise = timeout(120_000); - disposables.add(toDisposable(() => timeoutHandle.cancel())); - void timeoutHandle.then(() => { - if (isResolved) { - return; - } - this._logService.warn('[PluginInstallService] Terminal command completion timed out'); - resolveAndDispose(undefined); - }); - }); - } - - private _formatShellCommand(args: readonly string[]): string { - const [command, ...rest] = args; - return [command, ...rest.map(arg => this._shellEscapeArg(arg))].join(' '); - } - - private _shellEscapeArg(value: string): string { - if (isWindows) { - // PowerShell: use double quotes, escape backticks, dollar signs, and double quotes - return `"${value.replace(/[`$"]/g, '`$&')}"`; - } - // POSIX shells: use single quotes, escape by ending quote, adding escaped quote, reopening - return `'${value.replace(/'/g, `'\\''`)}'`; + this._pluginMarketplaceService.addInstalledPlugin(result.pluginDir, plugin); } } diff --git a/src/vs/workbench/contrib/chat/browser/pluginSources.ts b/src/vs/workbench/contrib/chat/browser/pluginSources.ts new file mode 100644 index 00000000000..fef8536d1d9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/pluginSources.ts @@ -0,0 +1,518 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from '../../../../base/common/actions.js'; +import { CancelablePromise, timeout } from '../../../../base/common/async.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { isWindows } from '../../../../base/common/platform.js'; +import { dirname, joinPath } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { TerminalCapability, type ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; +import { ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js'; +import { IEnsureRepositoryOptions, IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js'; +import { IGitHubPluginSource, IGitUrlPluginSource, IMarketplacePlugin, INpmPluginSource, IPipPluginSource, IPluginSourceDescriptor, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IPluginSource } from '../common/plugins/pluginSource.js'; + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +function sanitizeCacheSegment(name: string): string { + return name.replace(/[\\/:*?"<>|]/g, '_'); +} + +function gitRevisionCacheSuffix(ref?: string, sha?: string): string[] { + if (sha) { + return [`sha_${sanitizeCacheSegment(sha)}`]; + } + if (ref) { + return [`ref_${sanitizeCacheSegment(ref)}`]; + } + return []; +} + +function showGitOutputAction(commandService: ICommandService): Action { + return new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { + commandService.executeCommand('git.showOutput'); + }); +} + +function shellEscapeArg(value: string): string { + if (isWindows) { + return `"${value.replace(/[`$"]/g, '`$&')}"`; + } + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function formatShellCommand(args: readonly string[]): string { + const [command, ...rest] = args; + return [command, ...rest.map(arg => shellEscapeArg(arg))].join(' '); +} + +// --------------------------------------------------------------------------- +// Base for git-based sources (GitHub shorthand & arbitrary Git URL) +// --------------------------------------------------------------------------- + +abstract class AbstractGitPluginSource implements IPluginSource { + abstract readonly kind: PluginSourceKind; + constructor( + @ICommandService protected readonly _commandService: ICommandService, + @IFileService protected readonly _fileService: IFileService, + @ILogService protected readonly _logService: ILogService, + @INotificationService protected readonly _notificationService: INotificationService, + @IProgressService protected readonly _progressService: IProgressService, + ) { } + + abstract getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + abstract getLabel(descriptor: IPluginSourceDescriptor): string; + protected abstract _cloneUrl(descriptor: IPluginSourceDescriptor): string; + protected abstract _displayLabel(descriptor: IPluginSourceDescriptor): string; + + getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined { + return this.getInstallUri(cacheRoot, descriptor); + } + + async ensure(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise { + const descriptor = plugin.sourceDescriptor; + const repoDir = this.getInstallUri(cacheRoot, descriptor); + const repoExists = await this._fileService.exists(repoDir); + const label = this._displayLabel(descriptor); + + if (repoExists) { + await this._checkoutRevision(repoDir, descriptor, options?.failureLabel ?? label); + return repoDir; + } + + const progressTitle = options?.progressTitle ?? localize('cloningPluginSource', "Cloning plugin source '{0}'...", label); + const failureLabel = options?.failureLabel ?? label; + const ref = (descriptor as IGitHubPluginSource | IGitUrlPluginSource).ref; + + await this._cloneRepository(repoDir, this._cloneUrl(descriptor), progressTitle, failureLabel, ref); + await this._checkoutRevision(repoDir, descriptor, failureLabel); + return repoDir; + } + + async update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { + const descriptor = plugin.sourceDescriptor; + const repoDir = this.getInstallUri(cacheRoot, descriptor); + const repoExists = await this._fileService.exists(repoDir); + if (!repoExists) { + this._logService.warn(`[${this.kind}] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`); + return; + } + + const updateLabel = options?.pluginName ?? plugin.name; + const failureLabel = options?.failureLabel ?? updateLabel; + + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel), + cancellable: false, + }, + async () => { + await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); + const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource; + if (git.sha) { + await this._commandService.executeCommand('git.fetch', repoDir.fsPath); + } else { + await this._commandService.executeCommand('_git.pull', repoDir.fsPath); + } + await this._checkoutRevision(repoDir, descriptor, failureLabel); + } + ); + } catch (err) { + this._logService.error(`[${this.kind}] Failed to update plugin source '${updateLabel}':`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + } + } + + // -- internal helpers --- + + private async _cloneRepository(repoDir: URI, cloneUrl: string, progressTitle: string, failureLabel: string, ref?: string): Promise { + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: progressTitle, + cancellable: false, + }, + async () => { + await this._fileService.createFolder(dirname(repoDir)); + await this._commandService.executeCommand('_git.cloneRepository', cloneUrl, repoDir.fsPath, ref); + } + ); + } catch (err) { + this._logService.error(`[${this.kind}] Failed to clone ${cloneUrl}:`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('cloneFailed', "Failed to install plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + throw err; + } + } + + private async _checkoutRevision(repoDir: URI, descriptor: IPluginSourceDescriptor, failureLabel: string): Promise { + const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource; + if (!git.sha && !git.ref) { + return; + } + + try { + if (git.sha) { + await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, git.sha, true); + return; + } + await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, git.ref); + } catch (err) { + this._logService.error(`[${this.kind}] Failed to checkout revision for '${failureLabel}':`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('checkoutPluginSourceFailed', "Failed to checkout plugin '{0}' to requested revision: {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + throw err; + } + } +} + +// --------------------------------------------------------------------------- +// RelativePath — plugin lives inside a shared marketplace repository +// --------------------------------------------------------------------------- + +export class RelativePathPluginSource implements IPluginSource { + readonly kind = PluginSourceKind.RelativePath; + + getInstallUri(_cacheRoot: URI, _descriptor: IPluginSourceDescriptor): URI { + throw new Error('Use getPluginInstallUri() for relative-path sources'); + } + + async ensure(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IEnsureRepositoryOptions): Promise { + throw new Error('Use ensureRepository() for relative-path sources'); + } + + async update(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { + throw new Error('Use pullRepository() for relative-path sources'); + } + + getCleanupTarget(_cacheRoot: URI, _descriptor: IPluginSourceDescriptor): URI | undefined { + return undefined; + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as { path: string }).path || '.'; + } +} + +// --------------------------------------------------------------------------- +// GitHub — `{ source: "github", repo: "owner/repo" }` +// --------------------------------------------------------------------------- + +export class GitHubPluginSource extends AbstractGitPluginSource { + readonly kind = PluginSourceKind.GitHub; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const gh = descriptor as IGitHubPluginSource; + const [owner, repo] = gh.repo.split('/'); + return joinPath(cacheRoot, 'github.com', owner, repo, ...gitRevisionCacheSuffix(gh.ref, gh.sha)); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitHubPluginSource).repo; + } + + protected _cloneUrl(descriptor: IPluginSourceDescriptor): string { + return `https://github.com/${(descriptor as IGitHubPluginSource).repo}.git`; + } + + protected _displayLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitHubPluginSource).repo; + } +} + +// --------------------------------------------------------------------------- +// GitUrl — `{ source: "url", url: "https://…/repo.git" }` +// --------------------------------------------------------------------------- + +export class GitUrlPluginSource extends AbstractGitPluginSource { + readonly kind = PluginSourceKind.GitUrl; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const git = descriptor as IGitUrlPluginSource; + const segments = this._gitUrlCacheSegments(git.url, git.ref, git.sha); + return joinPath(cacheRoot, ...segments); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitUrlPluginSource).url; + } + + protected _cloneUrl(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitUrlPluginSource).url; + } + + protected _displayLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitUrlPluginSource).url; + } + + private _gitUrlCacheSegments(url: string, ref?: string, sha?: string): string[] { + try { + const parsed = URI.parse(url); + const authority = (parsed.authority || 'unknown').replace(/[\\/:*?"<>|]/g, '_').toLowerCase(); + const pathPart = parsed.path.replace(/^\/+/, '').replace(/\.git$/i, '').replace(/\/+$/g, ''); + const segments = pathPart.split('/').map(s => s.replace(/[\\/:*?"<>|]/g, '_')); + return [authority, ...segments, ...gitRevisionCacheSuffix(ref, sha)]; + } catch { + return ['git', url.replace(/[\\/:*?"<>|]/g, '_'), ...gitRevisionCacheSuffix(ref, sha)]; + } + } +} + +// --------------------------------------------------------------------------- +// Base for package-manager-based sources (npm, pip) +// --------------------------------------------------------------------------- + +export abstract class AbstractPackagePluginSource implements IPluginSource { + abstract readonly kind: PluginSourceKind; + constructor( + @IDialogService protected readonly _dialogService: IDialogService, + @IFileService protected readonly _fileService: IFileService, + @ILogService protected readonly _logService: ILogService, + @INotificationService protected readonly _notificationService: INotificationService, + @IProgressService protected readonly _progressService: IProgressService, + @ITerminalService protected readonly _terminalService: ITerminalService, + ) { } + + abstract getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + abstract getLabel(descriptor: IPluginSourceDescriptor): string; + + getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined { + return this._getCacheDir(cacheRoot, descriptor); + } + + /** + * Return the parent directory (prefix / target) where the package + * manager installs into. This is above the actual plugin content dir. + */ + protected abstract _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + + /** Build the terminal command args for install. */ + protected abstract _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[]; + + /** Human-readable package manager name for messages. */ + protected abstract get _managerName(): string; + + async ensure(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IEnsureRepositoryOptions): Promise { + const cacheDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor); + await this._fileService.createFolder(cacheDir); + return cacheDir; + } + + async update(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { + // For package-manager sources, "update" re-runs install. + const installDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor); + const pluginDir = this.getInstallUri(cacheRoot, plugin.sourceDescriptor); + await this.runInstall(installDir, pluginDir, plugin); + } + + async runInstall(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin): Promise<{ pluginDir: URI } | undefined> { + const args = this._buildInstallArgs(installDir, plugin); + const command = formatShellCommand(args); + const confirmed = await this._confirmTerminalCommand(plugin.name, command); + if (!confirmed) { + return undefined; + } + + const progressTitle = localize('installingPackagePlugin', "Installing {0} plugin '{1}'...", this._managerName, plugin.name); + const { success, terminal } = await this._runTerminalCommand(command, progressTitle); + if (!success) { + return undefined; + } + + const exists = await this._fileService.exists(pluginDir); + if (!exists) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('packagePluginNotFound', "{0} package '{1}' was not found after installation.", this._managerName, this.getLabel(plugin.sourceDescriptor)), + }); + return undefined; + } + + terminal?.dispose(); + return { pluginDir }; + } + + // -- terminal helpers (moved from PluginInstallService) --- + + private async _confirmTerminalCommand(pluginName: string, command: string): Promise { + const { confirmed } = await this._dialogService.confirm({ + type: 'question', + message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName), + detail: localize('confirmPluginInstallDetail', "This will run the following command in a terminal:\n\n{0}", command), + primaryButton: localize({ key: 'confirmInstall', comment: ['&& denotes a mnemonic'] }, "&&Install"), + }); + return confirmed; + } + + private async _runTerminalCommand(command: string, progressTitle: string) { + let terminal: ITerminalInstance | undefined; + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: progressTitle, + cancellable: false, + }, + async () => { + terminal = await this._terminalService.createTerminal({ + config: { + name: localize('pluginInstallTerminal', "Plugin Install"), + forceShellIntegration: true, + isTransient: true, + isFeatureTerminal: true, + }, + }); + await terminal.processReady; + this._terminalService.setActiveInstance(terminal); + + const commandResultPromise = this._waitForTerminalCommandCompletion(terminal); + await terminal.runCommand(command, true); + const exitCode = await commandResultPromise; + if (exitCode !== 0) { + throw new Error(localize('terminalCommandExitCode', "Command exited with code {0}", exitCode)); + } + } + ); + return { success: true, terminal }; + } catch (err) { + this._logService.error(`[${this.kind}] Terminal command failed:`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('terminalCommandFailed', "Plugin installation command failed: {0}", err?.message ?? String(err)), + }); + return { success: false, terminal }; + } + } + + private _waitForTerminalCommandCompletion(terminal: ITerminalInstance): Promise { + return new Promise(resolve => { + const disposables = new DisposableStore(); + let isResolved = false; + + const resolveAndDispose = (exitCode: number | undefined): void => { + if (isResolved) { + return; + } + isResolved = true; + disposables.dispose(); + resolve(exitCode); + }; + + const attachCommandFinishedListener = (): void => { + const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection); + if (!commandDetection) { + return; + } + disposables.add(commandDetection.onCommandFinished((command: ITerminalCommand) => { + resolveAndDispose(command.exitCode ?? 0); + })); + }; + + attachCommandFinishedListener(); + disposables.add(terminal.capabilities.onDidAddCommandDetectionCapability(() => attachCommandFinishedListener())); + + const timeoutHandle: CancelablePromise = timeout(120_000); + disposables.add(toDisposable(() => timeoutHandle.cancel())); + void timeoutHandle.then(() => { + if (isResolved) { + return; + } + this._logService.warn(`[${this.kind}] Terminal command completion timed out`); + resolveAndDispose(undefined); + }); + }); + } +} + +// --------------------------------------------------------------------------- +// npm — `{ source: "npm", package: "@org/plugin" }` +// --------------------------------------------------------------------------- + +export class NpmPluginSource extends AbstractPackagePluginSource { + readonly kind = PluginSourceKind.Npm; + protected readonly _managerName = 'npm'; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const npm = descriptor as INpmPluginSource; + return joinPath(cacheRoot, 'npm', sanitizeCacheSegment(npm.package), 'node_modules', npm.package); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + const npm = descriptor as INpmPluginSource; + return npm.version ? `${npm.package}@${npm.version}` : npm.package; + } + + protected _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const npm = descriptor as INpmPluginSource; + return joinPath(cacheRoot, 'npm', sanitizeCacheSegment(npm.package)); + } + + protected _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[] { + const npm = plugin.sourceDescriptor as INpmPluginSource; + const packageSpec = npm.version ? `${npm.package}@${npm.version}` : npm.package; + const args = ['npm', 'install', '--prefix', installDir.fsPath, packageSpec]; + if (npm.registry) { + args.push('--registry', npm.registry); + } + return args; + } +} + +// --------------------------------------------------------------------------- +// pip — `{ source: "pip", package: "my-plugin" }` +// --------------------------------------------------------------------------- + +export class PipPluginSource extends AbstractPackagePluginSource { + readonly kind = PluginSourceKind.Pip; + protected readonly _managerName = 'pip'; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const pip = descriptor as IPipPluginSource; + return joinPath(cacheRoot, 'pip', sanitizeCacheSegment(pip.package)); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + const pip = descriptor as IPipPluginSource; + return pip.version ? `${pip.package}==${pip.version}` : pip.package; + } + + protected _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const pip = descriptor as IPipPluginSource; + return joinPath(cacheRoot, 'pip', sanitizeCacheSegment(pip.package)); + } + + protected _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[] { + const pip = plugin.sourceDescriptor as IPipPluginSource; + const packageSpec = pip.version ? `${pip.package}==${pip.version}` : pip.package; + const args = ['pip', 'install', '--target', installDir.fsPath, packageSpec]; + if (pip.registry) { + args.push('--index-url', pip.registry); + } + return args; + } +} diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts index cfb76de1c1d..c0708bb503d 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts @@ -5,7 +5,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType } from './pluginMarketplaceService.js'; +import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType, PluginSourceKind } from './pluginMarketplaceService.js'; +import { IPluginSource } from './pluginSource.js'; export const IAgentPluginRepositoryService = createDecorator('agentPluginRepositoryService'); @@ -83,4 +84,21 @@ export interface IAgentPluginRepositoryService { * ref/sha checkout. For npm/pip sources this is a no-op. */ updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; + + /** + * Returns the {@link IPluginSource} strategy for the given + * source kind, allowing callers to invoke kind-specific operations + * (install, update, label, etc.) directly. + */ + getPluginSource(kind: PluginSourceKind): IPluginSource; + + /** + * Cleans up on-disk cache for a plugin source that owns its own install + * directory. For marketplace-relative sources this is a no-op (they share + * the marketplace repository cache). For direct sources (github, url, npm, + * pip) the cache directory is deleted. + * + * This is best-effort: failures are logged but do not throw. + */ + cleanupPluginSource(plugin: IMarketplacePlugin): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 722ff6da576..7377041a9e6 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -34,6 +34,7 @@ import { parseClaudeHooks } from '../promptSyntax/hookClaudeCompat.js'; import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js'; import { IHookCommand } from '../promptSyntax/hookSchema.js'; import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; +import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from './pluginMarketplaceService.js'; const COMMAND_FILE_SUFFIX = '.md'; @@ -865,6 +866,7 @@ export class MarketplaceAgentPluginDiscovery extends AbstractAgentPluginDiscover constructor( @IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService, + @IAgentPluginRepositoryService private readonly _pluginRepositoryService: IAgentPluginRepositoryService, @IFileService fileService: IFileService, @IPathService pathService: IPathService, @ILogService logService: ILogService, @@ -905,7 +907,17 @@ export class MarketplaceAgentPluginDiscovery extends AbstractAgentPluginDiscover enabled: entry.enabled, fromMarketplace: entry.plugin, setEnabled: (value: boolean) => this._pluginMarketplaceService.setInstalledPluginEnabled(entry.pluginUri, value), - remove: () => this._pluginMarketplaceService.removeInstalledPlugin(entry.pluginUri), + remove: () => { + // Always remove the metadata entry first so the plugin + // disappears from the UI immediately. + this._pluginMarketplaceService.removeInstalledPlugin(entry.pluginUri); + // For non-marketplace (direct-source) plugins, also clean up the + // on-disk cache. This is best-effort — failures are logged but + // do not block removal. + this._pluginRepositoryService.cleanupPluginSource(entry.plugin).catch(error => { + this._logService.error('[MarketplaceAgentPluginDiscovery] Failed to clean up plugin source', error); + }); + }, }); } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts new file mode 100644 index 00000000000..704d4334b0c --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../base/common/uri.js'; +import { IEnsureRepositoryOptions, IPullRepositoryOptions } from './agentPluginRepositoryService.js'; +import { IMarketplacePlugin, IPluginSourceDescriptor, PluginSourceKind } from './pluginMarketplaceService.js'; + +/** + * Per-kind strategy that centralizes install-path computation, source + * provisioning, update, label formatting, and uninstall cleanup for a + * single {@link PluginSourceKind}. + * + * Implementations are created via {@link IInstantiationService} so they + * can dependency-inject any services they need (git commands, file service, + * terminal service, etc.). + */ +export interface IPluginSource { + readonly kind: PluginSourceKind; + + /** + * Compute the local cache URI where this source's plugin files live. + * @param cacheRoot The root cache directory for all agent plugins. + */ + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + + /** + * Ensure the plugin source is available locally (clone, npm install, etc.). + * Returns the install directory URI. + */ + ensure(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise; + + /** + * Update an already-installed plugin source (git pull, npm update, etc.). + */ + update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; + + /** + * Returns the on-disk directory to delete when this plugin is + * uninstalled, or `undefined` if no cleanup is needed. + * + * Marketplace-relative sources return `undefined` because they share + * a marketplace repository cache. Direct sources (github, url, npm, + * pip) return the directory they own. + */ + getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined; + + /** + * Returns a human-readable label for a source descriptor of this kind, + * suitable for error messages and UI display. + */ + getLabel(descriptor: IPluginSourceDescriptor): string; + + /** + * For package-manager sources (npm, pip): run the terminal install + * command and return the resulting plugin directory, or `undefined` + * if the user cancelled or the command failed. + * + * Not implemented by non-package-manager sources. + */ + runInstall?(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin): Promise<{ pluginDir: URI } | undefined>; +} diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts index 0aad10fc122..f2f3b310a13 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts @@ -210,4 +210,193 @@ suite('AgentPluginRepositoryService', () => { assert.deepStrictEqual(commands, ['git.openRepository', 'git.fetch', '_git.checkout']); }); + + // ========================================================================= + // cleanupPluginSource — issue #297251 regression + // ========================================================================= + + suite('cleanupPluginSource', () => { + + function createServiceWithDel( + onDel: (resource: URI) => void, + options?: { resolve?: (resource: URI) => { children?: unknown[] } }, + ) { + const instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(ICommandService, { executeCommand: async () => undefined } as unknown as ICommandService); + instantiationService.stub(IEnvironmentService, { cacheHome: URI.file('/cache') } as unknown as IEnvironmentService); + instantiationService.stub(IFileService, { + exists: async () => true, + del: async (resource: URI) => { onDel(resource); }, + createFolder: async () => undefined, + resolve: async (resource: URI) => options?.resolve?.(resource) ?? { children: [] }, + } as unknown as IFileService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(INotificationService, { notify: () => undefined } as unknown as INotificationService); + instantiationService.stub(IProgressService, { withProgress: async (_o: unknown, cb: (...a: unknown[]) => Promise) => cb() } as unknown as IProgressService); + instantiationService.stub(IStorageService, store.add(new InMemoryStorageService())); + return instantiationService.createInstance(AgentPluginRepositoryService); + } + + test('does not delete files for relative-path (marketplace) plugin', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'marketplace-plugin', + description: '', + version: '', + source: 'plugins/foo', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/foo' }, + marketplace: 'microsoft/vscode', + marketplaceReference: parseMarketplaceReference('microsoft/vscode')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.strictEqual(deleted.length, 0); + }); + + test('deletes cache for github plugin source', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.ok(deleted.length >= 1); + assert.ok(deleted[0].includes('github.com/owner/repo')); + }); + + test('deletes parent cache dir for npm plugin source', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'npm-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.Npm, package: '@acme/plugin' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.ok(deleted.length >= 1); + // First delete should be the npm/ cache dir + assert.ok(deleted[0].includes('/npm/'), `Expected npm path, got: ${deleted[0]}`); + }); + + test('deletes cache for pip plugin source', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'pip-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pip-pkg' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.ok(deleted.length >= 1); + assert.ok(deleted[0].includes('pip/my-pip-pkg')); + }); + + test('does not throw when delete fails', async () => { + const instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(ICommandService, { executeCommand: async () => undefined } as unknown as ICommandService); + instantiationService.stub(IEnvironmentService, { cacheHome: URI.file('/cache') } as unknown as IEnvironmentService); + instantiationService.stub(IFileService, { + exists: async () => true, + del: async () => { throw new Error('permission denied'); }, + createFolder: async () => undefined, + resolve: async () => ({ children: [] }), + } as unknown as IFileService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(INotificationService, { notify: () => undefined } as unknown as INotificationService); + instantiationService.stub(IProgressService, { withProgress: async (_o: unknown, cb: (...a: unknown[]) => Promise) => cb() } as unknown as IProgressService); + instantiationService.stub(IStorageService, store.add(new InMemoryStorageService())); + const service = instantiationService.createInstance(AgentPluginRepositoryService); + + // Should not throw — cleanup is best-effort + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + }); + + test('prunes empty parent directories up to cache root', async () => { + // After deleting github.com/owner/repo, the "owner" dir is empty + // and should also be removed. + const deleted: string[] = []; + const service = createServiceWithDel( + r => deleted.push(r.path), + { resolve: () => ({ children: [] }) }, + ); + + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + // Should have deleted the repo dir + empty parents (owner, github.com) + assert.ok(deleted.length >= 2, `Expected at least 2 deletions (repo + parent), got ${deleted.length}: ${deleted.join(', ')}`); + assert.ok(deleted[0].includes('github.com/owner/repo'), 'First delete should be the repo dir'); + assert.ok(deleted.some(p => p.endsWith('/owner')), 'Should prune empty owner directory'); + }); + + test('stops pruning at non-empty parent', async () => { + const deleted: string[] = []; + const service = createServiceWithDel( + r => deleted.push(r.path), + { + resolve: (resource: URI) => { + // owner dir still has another repo + if (resource.path.endsWith('/owner')) { + return { children: [{ name: 'other-repo' }] }; + } + return { children: [] }; + }, + }, + ); + + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + // Should only delete the repo dir, stop at non-empty owner dir + assert.strictEqual(deleted.length, 1); + assert.ok(deleted[0].includes('github.com/owner/repo')); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts index 7a55baa369a..a0652f46641 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts @@ -16,6 +16,7 @@ import { ITerminalService } from '../../../../terminal/browser/terminal.js'; import { PluginInstallService } from '../../../browser/pluginInstallService.js'; import { IAgentPluginRepositoryService, IEnsureRepositoryOptions, IPullRepositoryOptions } from '../../../common/plugins/agentPluginRepositoryService.js'; import { IMarketplacePlugin, IMarketplaceReference, IPluginMarketplaceService, IPluginSourceDescriptor, MarketplaceType, parseMarketplaceReference, PluginSourceKind } from '../../../common/plugins/pluginMarketplaceService.js'; +import { IPluginSource } from '../../../common/plugins/pluginSource.js'; suite('PluginInstallService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -145,6 +146,69 @@ suite('PluginInstallService', () => { instantiationService.stub(ILogService, new NullLogService()); // IAgentPluginRepositoryService + // Build mock source repositories for npm/pip that simulate terminal-based install + const makeMockPackageRepo = (kind: PluginSourceKind): IPluginSource => ({ + kind, + getCleanupTarget: () => URI.file('/mock-cleanup'), + getInstallUri: () => URI.file('/mock'), + ensure: async () => state.ensurePluginSourceResult, + update: async () => { }, + getLabel: (d) => kind === PluginSourceKind.Npm ? (d as { package: string }).package : (d as { package: string }).package, + runInstall: async (_installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin) => { + // Simulate confirmation dialog + if (!state.dialogConfirmResult) { + return undefined; + } + + // Simulate building and running the command + const descriptor = plugin.sourceDescriptor; + let args: string[]; + if (kind === PluginSourceKind.Npm) { + const npm = descriptor as { package: string; version?: string; registry?: string }; + const packageSpec = npm.version ? `${npm.package}@${npm.version}` : npm.package; + args = ['npm', 'install', '--prefix', _installDir.fsPath, packageSpec]; + if (npm.registry) { + args.push('--registry', npm.registry); + } + } else { + const pip = descriptor as { package: string; version?: string; registry?: string }; + const packageSpec = pip.version ? `${pip.package}==${pip.version}` : pip.package; + args = ['pip', 'install', '--target', _installDir.fsPath, packageSpec]; + if (pip.registry) { + args.push('--index-url', pip.registry); + } + } + const command = args.join(' '); + state.terminalCommands.push(command); + + if (state.terminalExitCode !== 0) { + state.notifications.push({ severity: 3, message: `Plugin installation command failed: Command exited with code ${state.terminalExitCode}` }); + return undefined; + } + + // Check if plugin dir exists + const exists = typeof state.fileExistsResult === 'function' + ? await state.fileExistsResult(pluginDir) + : state.fileExistsResult; + if (!exists) { + const label = kind === PluginSourceKind.Npm ? 'npm' : 'pip'; + const pkg = (descriptor as { package: string }).package; + state.notifications.push({ severity: 3, message: `${label} package '${pkg}' was not found after installation.` }); + return undefined; + } + + return { pluginDir }; + }, + }); + + const mockSourceRepos = new Map([ + [PluginSourceKind.RelativePath, { kind: PluginSourceKind.RelativePath, getCleanupTarget: () => undefined, getInstallUri: () => { throw new Error(); }, ensure: async () => { throw new Error(); }, update: async () => { throw new Error(); }, getLabel: (d) => (d as { path: string }).path || '.' }], + [PluginSourceKind.GitHub, { kind: PluginSourceKind.GitHub, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => { }, getLabel: (d) => (d as { repo: string }).repo }], + [PluginSourceKind.GitUrl, { kind: PluginSourceKind.GitUrl, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => { }, getLabel: (d) => (d as { url: string }).url }], + [PluginSourceKind.Npm, makeMockPackageRepo(PluginSourceKind.Npm)], + [PluginSourceKind.Pip, makeMockPackageRepo(PluginSourceKind.Pip)], + ]); + instantiationService.stub(IAgentPluginRepositoryService, { getPluginInstallUri: (plugin: IMarketplacePlugin) => { return URI.joinPath(state.ensureRepositoryResult, plugin.source); @@ -164,6 +228,8 @@ suite('PluginInstallService', () => { updatePluginSource: async (plugin: IMarketplacePlugin, options?: IPullRepositoryOptions) => { state.updatePluginSourceCalls.push({ plugin, options }); }, + getPluginSource: (kind: PluginSourceKind) => mockSourceRepos.get(kind)!, + cleanupPluginSource: async () => { }, } as unknown as IAgentPluginRepositoryService); // IPluginMarketplaceService @@ -540,7 +606,7 @@ suite('PluginInstallService', () => { suite('updatePlugin', () => { - test('calls pullRepository for relative-path plugins', async () => { + test('calls updatePluginSource for relative-path plugins', async () => { const { service, state } = createService(); const plugin = createPlugin({ source: 'plugins/myPlugin', @@ -549,8 +615,7 @@ suite('PluginInstallService', () => { await service.updatePlugin(plugin); - assert.strictEqual(state.pullRepositoryCalls.length, 1); - assert.strictEqual(state.updatePluginSourceCalls.length, 0); + assert.strictEqual(state.updatePluginSourceCalls.length, 1); }); test('calls updatePluginSource for GitHub plugins', async () => { @@ -562,7 +627,6 @@ suite('PluginInstallService', () => { await service.updatePlugin(plugin); assert.strictEqual(state.updatePluginSourceCalls.length, 1); - assert.strictEqual(state.pullRepositoryCalls.length, 0); }); test('calls updatePluginSource for GitUrl plugins', async () => { @@ -574,7 +638,6 @@ suite('PluginInstallService', () => { await service.updatePlugin(plugin); assert.strictEqual(state.updatePluginSourceCalls.length, 1); - assert.strictEqual(state.pullRepositoryCalls.length, 0); }); test('re-installs for npm plugin updates', async () => { From 44e1948e8cc7dd1115bd3c9bbbd86d812b0e0950 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 4 Mar 2026 16:24:52 -0800 Subject: [PATCH 206/448] Refactor agent session provider handling and update background agent display name logic (#299328) --- .../chat/browser/agentSessions/agentSessions.ts | 10 ++-------- .../contrib/chat/browser/chat.contribution.ts | 11 +---------- .../chatSessions/chatSessions.contribution.ts | 3 +-- .../widget/input/sessionTargetPickerActionItem.ts | 14 +++----------- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index a8be72f98ff..f7662aa6914 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../../nls.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { observableValue } from '../../../../../base/common/observable.js'; + import { IChatSessionTiming } from '../../common/chatService/chatService.js'; import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; @@ -42,18 +42,12 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes } } -/** - * Observable holding the display name for the background agent session provider. - * Updated via experiment treatment to allow A/B testing of the display name. - */ -export const backgroundAgentDisplayName = observableValue('backgroundAgentDisplayName', localize('chat.session.providerLabel.background', "Background")); - export function getAgentSessionProviderName(provider: AgentSessionProviders): string { switch (provider) { case AgentSessionProviders.Local: return localize('chat.session.providerLabel.local', "Local"); case AgentSessionProviders.Background: - return backgroundAgentDisplayName.get(); + return localize('chat.session.providerLabel.background', "Copilot CLI"); case AgentSessionProviders.Cloud: return localize('chat.session.providerLabel.cloud', "Cloud"); case AgentSessionProviders.Claude: diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2c38f3c9874..7b9d6e57798 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -100,7 +100,7 @@ import { ChatDebugEditor } from './chatDebug/chatDebugEditor.js'; import { PromptsDebugContribution } from './promptsDebugContribution.js'; import { ChatDebugEditorInput, ChatDebugEditorInputSerializer } from './chatDebug/chatDebugEditorInput.js'; import './agentSessions/agentSessions.contribution.js'; -import { backgroundAgentDisplayName } from './agentSessions/agentSessions.js'; + import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { ChatViewId, IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService, isIChatResourceViewContext, isIChatViewViewContext } from './chat.js'; @@ -1433,7 +1433,6 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr super(); this.newChatButtonExperimentIcon = ChatContextKeys.newChatButtonExperimentIcon.bindTo(this.contextKeyService); this.registerMaxRequestsSetting(); - this.registerBackgroundAgentDisplayName(); this.registerNewChatButtonIcon(); } @@ -1465,14 +1464,6 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr this._register(Event.runAndSubscribe(Event.debounce(this.entitlementService.onDidChangeEntitlement, () => { }, 1000), () => registerMaxRequestsSetting())); } - private registerBackgroundAgentDisplayName(): void { - this.experimentService.getTreatment('backgroundAgentDisplayName').then((value) => { - if (value) { - backgroundAgentDisplayName.set(value, undefined); - } - }); - } - private registerNewChatButtonIcon(): void { this.experimentService.getTreatment('chatNewButtonIcon').then((value) => { const supportedValues = ['copilot', 'new-session', 'comment']; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 309e48a4472..34118f93f1f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -44,7 +44,7 @@ import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatViewId } from '../chat.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; -import { AgentSessionProviders, backgroundAgentDisplayName, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { BugIndicatingError, isCancellationError } from '../../../../../base/common/errors.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { LocalChatSessionUri } from '../../common/model/chatUri.js'; @@ -347,7 +347,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ ).recomputeInitiallyAndOnChange(this._store); this._register(autorun(reader => { - backgroundAgentDisplayName.read(reader); const activatedProviders = [...builtinSessionProviders, ...contributedSessionProviders.read(reader)]; for (const provider of Object.values(AgentSessionProviders)) { if (activatedProviders.includes(provider)) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 9ef9fb5eab6..d70bfd59695 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -18,11 +18,11 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; -import { AgentSessionProviders, backgroundAgentDisplayName, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { ISessionTypePickerDelegate } from '../../chat.js'; import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; -import { autorun } from '../../../../../../base/common/observable.js'; + export interface ISessionTypeItem { type: AgentSessionProviders; @@ -105,15 +105,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { this._updateAgentSessionItems(); })); - // Re-render when the background agent display name changes via experiment - // Note: autorun runs immediately, so this also handles initial population - this._register(autorun(reader => { - backgroundAgentDisplayName.read(reader); - this._updateAgentSessionItems(); - if (this.element) { - this.renderLabel(this.element); - } - })); + this._updateAgentSessionItems(); } protected _run(sessionTypeItem: ISessionTypeItem): void { From b543c356ed2c84728906c3ae2f55a78a6c96cffd Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 4 Mar 2026 16:27:43 -0800 Subject: [PATCH 207/448] Update --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 3dcdffcec1d..80a7be9fbd2 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -575,6 +575,7 @@ export class ChangesViewPane extends ViewPane { this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); + sessionsChangedSignal.read(reader); // Re-evaluate when session metadata changes (e.g. pullRequestUrl) const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; reader.store.add(scopedInstantiationService.createInstance( From 5222fc6f558431d8982f20a34e800082cd9cd28d Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 4 Mar 2026 16:51:02 -0800 Subject: [PATCH 208/448] Add feature flag for custom agent hooks (#299316) --- .../contrib/chat/browser/chat.contribution.ts | 9 ++++++++ .../chat/common/promptSyntax/config/config.ts | 5 ++++ .../promptHeaderAutocompletion.ts | 6 +++++ .../languageProviders/promptHovers.ts | 6 +++++ .../languageProviders/promptValidator.ts | 23 +++++++++++++++---- .../service/promptsServiceImpl.ts | 10 ++++++-- .../promptHeaderAutocompletion.test.ts | 1 + .../languageProviders/promptHovers.test.ts | 1 + .../languageProviders/promptValidator.test.ts | 1 + 9 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7b9d6e57798..d9bcf54ea39 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1070,6 +1070,15 @@ configurationRegistry.registerConfiguration({ disallowConfigurationDefault: true, tags: ['preview', 'prompts', 'hooks', 'agent'] }, + [PromptsConfig.USE_CUSTOM_AGENT_HOOKS]: { + type: 'boolean', + title: nls.localize('chat.useCustomAgentHooks.title', "Use Custom Agent Hooks",), + markdownDescription: nls.localize('chat.useCustomAgentHooks.description', "Controls whether hooks defined in custom agent frontmatter are parsed and executed. When disabled, hooks from agent files are ignored.",), + default: false, + restricted: true, + disallowConfigurationDefault: true, + tags: ['preview', 'prompts', 'hooks', 'agent'] + }, [PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: { type: 'object', scope: ConfigurationScope.RESOURCE, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts index 1f0b9da69ca..4c7497b9f7d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts @@ -115,6 +115,11 @@ export namespace PromptsConfig { */ export const USE_CLAUDE_HOOKS = 'chat.useClaudeHooks'; + /** + * Configuration key for enabling hooks defined in custom agent frontmatter. + */ + export const USE_CUSTOM_AGENT_HOOKS = 'chat.useCustomAgentHooks'; + /** * Configuration key for enabling stronger skill adherence prompt (experimental). */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 4884aed9fa8..3b4151e65eb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -21,6 +21,8 @@ import { localize } from '../../../../../../nls.js'; import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.js'; import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { PromptsConfig } from '../config/config.js'; export class PromptHeaderAutocompletion implements CompletionItemProvider { /** @@ -38,6 +40,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @IChatModeService private readonly chatModeService: IChatModeService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -138,6 +141,9 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { const target = getTarget(promptType, header); const attributesToPropose = new Set(getValidAttributeNames(promptType, false, target)); + if (!this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + attributesToPropose.delete(PromptHeaderAttributes.hooks); + } for (const attr of header.attributes) { attributesToPropose.delete(attr.key); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 00065d1b40c..e3d4b279cd9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -19,6 +19,8 @@ import { IHeaderAttribute, ISequenceValue, parseCommaSeparatedList, PromptBody, import { ClaudeHeaderAttributes, getAttributeDefinition, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptFileAttributes.js'; import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { PromptsConfig } from '../config/config.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -31,6 +33,7 @@ export class PromptHoverProvider implements HoverProvider { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IChatModeService private readonly chatModeService: IChatModeService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -89,6 +92,9 @@ export class PromptHoverProvider implements HoverProvider { case PromptHeaderAttributes.handOffs: return this.getHandsOffHover(attribute, position, target); case PromptHeaderAttributes.hooks: + if (!this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + return undefined; + } return this.getHooksHover(attribute, position, description, target); case PromptHeaderAttributes.infer: return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', 'Deprecated: Use `user-invocable` and `disable-model-invocation` instead.'), attribute.range); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index d6e71491d01..d814c0b866a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -21,6 +21,7 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../../../bas import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IPromptsService } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { AGENTS_SOURCE_FOLDER, CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; @@ -29,6 +30,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; import { HOOKS_BY_TARGET } from '../hookTypes.js'; +import { PromptsConfig } from '../config/config.js'; import { GithubPromptHeaderAttributes } from './promptFileAttributes.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -40,7 +42,8 @@ export class PromptValidator { @IChatModeService private readonly chatModeService: IChatModeService, @IFileService private readonly fileService: IFileService, @ILabelService private readonly labelService: ILabelService, - @IPromptsService private readonly promptsService: IPromptsService + @IPromptsService private readonly promptsService: IPromptsService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { } public async validate(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { @@ -192,7 +195,9 @@ export class PromptValidator { this.validateUserInvokable(attributes, report); this.validateDisableModelInvocation(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, target, report); - this.validateHooks(attributes, target, report); + if (this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + this.validateHooks(attributes, target, report); + } if (isVSCodeOrDefaultTarget(target)) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); @@ -215,11 +220,21 @@ export class PromptValidator { } private checkForInvalidArguments(attributes: IHeaderAttribute[], promptType: PromptsType, target: Target, report: (markers: IMarkerData) => void): void { - const validAttributeNames = getValidAttributeNames(promptType, true, target); + let validAttributeNames = getValidAttributeNames(promptType, true, target); + if (!this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + validAttributeNames = validAttributeNames.filter(name => name !== PromptHeaderAttributes.hooks); + } + const useCustomAgentHooks = this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS); const validGithubCopilotAttributeNames = new Lazy(() => new Set(getValidAttributeNames(promptType, false, Target.GitHubCopilot))); for (const attribute of attributes) { if (!validAttributeNames.includes(attribute.key)) { - const supportedNames = new Lazy(() => getValidAttributeNames(promptType, false, target).sort().join(', ')); + const supportedNames = new Lazy(() => { + let names = getValidAttributeNames(promptType, false, target); + if (!useCustomAgentHooks) { + names = names.filter(name => name !== PromptHeaderAttributes.hooks); + } + return names.sort().join(', '); + }); switch (promptType) { case PromptsType.prompt: report(toMarker(localize('promptValidator.unknownAttribute.prompt', "Attribute '{0}' is not supported in prompt files. Supported: {1}.", attribute.key, supportedNames.value), attribute.range, MarkerSeverity.Warning)); 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 688ad88d715..63f3e0b0e41 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -193,7 +193,12 @@ export class PromptsService extends Disposable implements IPromptsService { 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), this._onDidContributedWhenChange.event) + () => Event.any( + this.getFileLocatorEvent(PromptsType.agent), + Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent), + this._onDidContributedWhenChange.event, + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)), + ) )); this.cachedSlashCommands = this._register(new CachedPromise( @@ -742,8 +747,9 @@ export class PromptsService extends Disposable implements IPromptsService { // Parse hooks from the frontmatter if present let hooks: ChatRequestHooks | undefined; + const useCustomAgentHooks = this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS); const hooksRaw = ast.header.hooksRaw; - if (hooksRaw) { + if (useCustomAgentHooks && hooksRaw) { const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder; const workspaceRootUri = hookWorkspaceFolder?.uri; hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index 4ba131c8950..101e3235aea 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -37,6 +37,7 @@ suite('PromptHeaderAutocompletion', () => { setup(async () => { const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true); instaService = workbenchInstantiationService({ contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), configurationService: () => testConfigService diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts index f90f6d45707..ab2a3b4067c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -37,6 +37,7 @@ suite('PromptHoverProvider', () => { setup(async () => { const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true); instaService = workbenchInstantiationService({ contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), configurationService: () => testConfigService diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index e1ad97bb791..be2df9c0456 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -41,6 +41,7 @@ suite('PromptValidator', () => { const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true); instaService = workbenchInstantiationService({ contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), configurationService: () => testConfigService From 9a207cb6962daf8a7c2fb061e056c8072a78e573 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 16:51:57 -0800 Subject: [PATCH 209/448] chat: fix undo/redo skipping multiple no-edit requests (#299330) - Fixes _willRedoToEpoch to advance one request at a time when there are no edit operations ahead, instead of jumping past all remaining requests - When redoing with no operations in the queue, now finds the next request-start checkpoint boundary and advances there, following the same single-step pattern as undo - Adds test case verifying undo and redo step through consecutive no-edit requests one at a time Fixes #275234 (Commit message generated by Copilot) --- .../chatEditingCheckpointTimelineImpl.ts | 14 +++++- .../chatEditingCheckpointTimeline.test.ts | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts index 0877eac0fd6..500774bfeb8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts @@ -123,7 +123,19 @@ export class ChatEditingCheckpointTimelineImpl implements IChatEditingCheckpoint // Find the next edit operation that would be applied... const nextOperation = operations.find(op => op.epoch >= currentEpoch); - const nextCheckpoint = nextOperation && checkpoints.find(op => op.epoch > nextOperation.epoch); + + // When there are no more operations, advance one request at a time + // by finding the next request-start checkpoint boundary. + if (!nextOperation) { + const nextRequestStart = checkpoints.find(cp => cp.epoch >= currentEpoch && cp.undoStopId === undefined); + if (!nextRequestStart) { + return maxEncounteredEpoch + 1; + } + const requestAfter = checkpoints.find(cp => cp.epoch > nextRequestStart.epoch && cp.undoStopId === undefined); + return requestAfter ? requestAfter.epoch : (maxEncounteredEpoch + 1); + } + + const nextCheckpoint = checkpoints.find(op => op.epoch > nextOperation.epoch); // And figure out where we're going if we're navigating across request // 1. If there is no next request or if the next target checkpoint is in diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts index 57478466f4f..f6f3af2a699 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts @@ -1248,6 +1248,54 @@ suite('ChatEditingCheckpointTimeline', function () { await timeline.navigateToCheckpoint(stop2NewCheckpointId); assert.strictEqual(fileContents.get(uri), 'replacement edit', 'Content should still be correct after full timeline traversal'); }); + + test('undo/redo with multiple no-edit requests advances one request at a time', async function () { + // req1: no edits + timeline.createCheckpoint('req1', undefined, 'Start req1'); + + // req2: no edits + timeline.createCheckpoint('req2', undefined, 'Start req2'); + + // req3: no edits + timeline.createCheckpoint('req3', undefined, 'Start req3'); + + // req4: no edits + timeline.createCheckpoint('req4', undefined, 'Start req4'); + + // Undo should step one request at a time + assert.strictEqual(timeline.canUndo.get(), true); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4']); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3']); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3', 'req2']); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3', 'req2', 'req1']); + + assert.strictEqual(timeline.canUndo.get(), false); + + // Redo should also step one request at a time (not skip all at once) + assert.strictEqual(timeline.canRedo.get(), true); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3', 'req2']); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3']); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4']); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), []); + + assert.strictEqual(timeline.canRedo.get(), false); + }); }); // Mock notebook service for tests that don't need notebook functionality From f6fa90787236c36c2028ce9a62116568858e3c5a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Mar 2026 12:01:36 +1100 Subject: [PATCH 210/448] fix: Display chat session contributed models and ignore inline chat as they don't apply (#299335) --- .../chat/browser/widget/input/chatModelSelectionLogic.ts | 4 +--- .../test/browser/widget/input/chatModelSelectionLogic.test.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts index 62dbca81dc3..206a3d54765 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts @@ -31,9 +31,7 @@ export function filterModelsForSession( if (sessionType && sessionType !== 'local' && hasModelsTargetingSession(models, sessionType)) { return models.filter(entry => entry.metadata?.targetChatSessionType === sessionType && - entry.metadata?.isUserSelectable && - isModelSupportedForMode(entry, currentModeKind) && - isModelSupportedForInlineChat(entry, location, isInlineChatV2Enabled) + entry.metadata?.isUserSelectable ); } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts index d282483d7d7..581f0db3fb3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts @@ -239,7 +239,7 @@ suite('ChatModelSelectionLogic', () => { assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o']); }); - test('filters by mode for session-targeted models', () => { + test.skip('filters by mode for session-targeted models', () => { const cloudNoTools = createSessionModel('cloud-basic', 'Cloud Basic', 'cloud', { capabilities: { toolCalling: false, agentMode: false }, }); From 736ef2e05d6788bdd4a6ecfb981925e17e7cf3d6 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:06:06 -0800 Subject: [PATCH 211/448] Add 'launch' skill for VS Code UI automation via agent-browser (#299258) --- .agents/skills/launch/SKILL.md | 291 +++++ .claude/skills/launch | 1 + package-lock.json | 2225 +++++++++++++++++++++++++++++++- package.json | 1 + 4 files changed, 2453 insertions(+), 65 deletions(-) create mode 100644 .agents/skills/launch/SKILL.md create mode 120000 .claude/skills/launch diff --git a/.agents/skills/launch/SKILL.md b/.agents/skills/launch/SKILL.md new file mode 100644 index 00000000000..e1b425e497d --- /dev/null +++ b/.agents/skills/launch/SKILL.md @@ -0,0 +1,291 @@ +--- +name: launch +description: "Launch and automate VS Code (Code OSS) using agent-browser via Chrome DevTools Protocol. Use when you need to interact with the VS Code UI, automate the chat panel, test UI features, or take screenshots of VS Code. Triggers include 'automate VS Code', 'interact with chat', 'test the UI', 'take a screenshot', 'launch Code OSS with debugging'." +metadata: + allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*) +--- + +# VS Code Automation + +Automate VS Code (Code OSS) using agent-browser. VS Code is built on Electron/Chromium and exposes a Chrome DevTools Protocol (CDP) port that agent-browser can connect to, enabling the same snapshot-interact workflow used for web pages. + +## Prerequisites + +- **`agent-browser` must be installed.** It's listed in devDependencies — run `npm install` in the repo root. Use `npx agent-browser` if it's not on your PATH, or install globally with `npm install -g agent-browser`. +- **For Code OSS (VS Code dev build):** The repo must be built before launching. `./scripts/code.sh` runs the build automatically if needed, or set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built. +- **CSS selectors are internal implementation details.** Selectors like `.interactive-input-part`, `.interactive-input-editor`, and `.part.auxiliarybar` used in `eval` commands are VS Code internals that may change across versions. If they stop working, use `agent-browser snapshot -i` to re-discover the current DOM structure. + +## Core Workflow + +1. **Launch** Code OSS with remote debugging enabled +2. **Connect** agent-browser to the CDP port +3. **Snapshot** to discover interactive elements +4. **Interact** using element refs +5. **Re-snapshot** after navigation or state changes + +```bash +# Launch Code OSS with remote debugging +./scripts/code.sh --remote-debugging-port=9224 + +# Wait for Code OSS to start, retry until connected +for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done + +# Discover UI elements +agent-browser snapshot -i + +# Focus the chat input (macOS) +agent-browser press Control+Meta+i +``` + +## Connecting + +```bash +# Connect to a specific port +agent-browser connect 9222 + +# Or use --cdp on each command +agent-browser --cdp 9222 snapshot -i + +# Auto-discover a running Chromium-based app +agent-browser --auto-connect snapshot -i +``` + +After `connect`, all subsequent commands target the connected app without needing `--cdp`. + +## Tab Management + +Electron apps often have multiple windows or webviews. Use tab commands to list and switch between them: + +```bash +# List all available targets (windows, webviews, etc.) +agent-browser tab + +# Switch to a specific tab by index +agent-browser tab 2 + +# Switch by URL pattern +agent-browser tab --url "*settings*" +``` + +## Launching Code OSS (VS Code Dev Build) + +The VS Code repository includes `scripts/code.sh` which launches Code OSS from source. It passes all arguments through to the Electron binary, so `--remote-debugging-port` works directly: + +```bash +cd # the root of your VS Code checkout +./scripts/code.sh --remote-debugging-port=9224 +``` + +Wait for the window to fully initialize, then connect: + +```bash +# Wait for Code OSS to start, retry until connected +for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done +agent-browser snapshot -i +``` + +**Tips:** +- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built: `VSCODE_SKIP_PRELAUNCH=1 ./scripts/code.sh --remote-debugging-port=9224` (from the repo root) +- Code OSS uses the default user data directory. Unlike VS Code Insiders, you don't typically need `--user-data-dir` since there's usually only one Code OSS instance running. +- If you see "Sent env to running instance. Terminating..." it means Code OSS is already running and forwarded your args to the existing instance. Quit Code OSS and relaunch with the flag, or use `--user-data-dir=/tmp/code-oss-debug` to force a new instance. + +## Launching VS Code Extensions for Debugging + +To debug a VS Code extension via agent-browser, launch VS Code Insiders with `--extensionDevelopmentPath` and `--remote-debugging-port`. Use `--user-data-dir` to avoid conflicting with an already-running instance. + +```bash +# Build the extension first +cd # e.g., the root of your extension checkout +npm run compile + +# Launch VS Code Insiders with the extension and CDP +code-insiders \ + --extensionDevelopmentPath="" \ + --remote-debugging-port=9223 \ + --user-data-dir=/tmp/vscode-ext-debug + +# Wait for VS Code to start, retry until connected +for i in 1 2 3 4 5; do agent-browser connect 9223 2>/dev/null && break || sleep 3; done +agent-browser snapshot -i +``` + +**Key flags:** +- `--extensionDevelopmentPath=` — loads your extension from source (must be compiled first) +- `--remote-debugging-port=9223` — enables CDP (use 9223 to avoid conflicts with other apps on 9222) +- `--user-data-dir=` — uses a separate profile so it starts a new process instead of sending to an existing VS Code instance + +**Without `--user-data-dir`**, VS Code detects the running instance, forwards the args to it, and exits immediately — you'll see "Sent env to running instance. Terminating..." and CDP never starts. + +## Interacting with Monaco Editor (Chat Input, Code Editors) + +VS Code uses Monaco Editor for all text inputs including the Copilot Chat input. Monaco editors require specific agent-browser techniques — standard `click`, `fill`, and `keyboard type` commands may not work depending on the VS Code build. + +### The Universal Pattern: Focus via Keyboard Shortcut + `press` + +This works on **all** VS Code builds (Code OSS, Insiders, stable): + +```bash +# 1. Open and focus the chat input with the keyboard shortcut +# macOS: +agent-browser press Control+Meta+i +# Linux / Windows: +agent-browser press Control+Alt+i + +# 2. Type using individual press commands +agent-browser press H +agent-browser press e +agent-browser press l +agent-browser press l +agent-browser press o +agent-browser press Space # Use "Space" for spaces +agent-browser press w +agent-browser press o +agent-browser press r +agent-browser press l +agent-browser press d + +# Verify text appeared (optional) +agent-browser eval ' +(() => { + const sidebar = document.querySelector(".part.auxiliarybar"); + const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line"); + return Array.from(viewLines).map(vl => vl.textContent).join("|"); +})()' + +# 3. Send the message (same on all platforms) +agent-browser press Enter +``` + +**Chat focus shortcut by platform:** +- **macOS:** `Ctrl+Cmd+I` → `agent-browser press Control+Meta+i` +- **Linux:** `Ctrl+Alt+I` → `agent-browser press Control+Alt+i` +- **Windows:** `Ctrl+Alt+I` → `agent-browser press Control+Alt+i` + +This shortcut focuses the chat input and sets `document.activeElement` to a `DIV` with class `native-edit-context` — VS Code's native text editing surface that correctly processes key events from `agent-browser press`. + +### `type @ref` — Works on Some Builds + +On VS Code Insiders (extension debug mode), `type @ref` handles focus and input in one step: + +```bash +agent-browser snapshot -i +# Look for: textbox "The editor is not accessible..." [ref=e62] +agent-browser type @e62 "Hello from George!" +``` + +However, **`type @ref` silently fails on Code OSS** — the command completes without error but no text appears. This also applies to `keyboard type` and `keyboard inserttext`. Always verify text appeared after typing, and fall back to the keyboard shortcut + `press` pattern if it didn't. The `press`-per-key approach works universally across all builds. + +### Compatibility Matrix + +| Method | VS Code Insiders | Code OSS | +|--------|-----------------|----------| +| `press` per key (after focus shortcut) | ✅ Works | ✅ Works | +| `type @ref` | ✅ Works | ❌ Silent fail | +| `keyboard type` (after focus) | ✅ Works | ❌ Silent fail | +| `keyboard inserttext` (after focus) | ✅ Works | ❌ Silent fail | +| `click @ref` | ❌ Blocked by overlay | ❌ Blocked by overlay | +| `fill @ref` | ❌ Element not visible | ❌ Element not visible | + +### Fallback: Focus via JavaScript Mouse Events + +If the keyboard shortcut doesn't work (e.g., chat panel isn't configured), you can focus the editor via JavaScript: + +```bash +agent-browser eval ' +(() => { + const inputPart = document.querySelector(".interactive-input-part"); + const editor = inputPart.querySelector(".monaco-editor"); + const rect = editor.getBoundingClientRect(); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + editor.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: x, clientY: y })); + editor.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX: x, clientY: y })); + editor.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: x, clientY: y })); + return "activeElement: " + document.activeElement?.className; +})()' + +# Then use press for each character +agent-browser press H +agent-browser press e +# ... +``` + +### Verifying Text and Clearing + +```bash +# Verify text in the chat input +agent-browser eval ' +(() => { + const sidebar = document.querySelector(".part.auxiliarybar"); + const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line"); + return Array.from(viewLines).map(vl => vl.textContent).join("|"); +})()' + +# Clear the input (Select All + Backspace) +# macOS: +agent-browser press Meta+a +# Linux / Windows: +agent-browser press Control+a +# Then delete: +agent-browser press Backspace +``` + +### Screenshot Tips for VS Code + +On ultrawide monitors, the chat sidebar may be in the far-right corner of the CDP screenshot. Options: +- Use `agent-browser screenshot --full` to capture the entire window +- Use element screenshots: `agent-browser screenshot ".part.auxiliarybar" sidebar.png` +- Use `agent-browser screenshot --annotate` to see labeled element positions +- Maximize the sidebar first: click the "Maximize Secondary Side Bar" button + +> **macOS:** If `agent-browser screenshot` returns "Permission denied", your terminal needs Screen Recording permission. Grant it in **System Settings → Privacy & Security → Screen Recording**. As a fallback, use the `eval` verification snippet to confirm text was entered — this doesn't require screen permissions. + +## Troubleshooting + +### "Connection refused" or "Cannot connect" + +- Make sure Code OSS was launched with `--remote-debugging-port=NNNN` +- If Code OSS was already running, quit and relaunch with the flag +- Check that the port isn't in use by another process: + - macOS / Linux: `lsof -i :9224` + - Windows: `netstat -ano | findstr 9224` + +### Elements not appearing in snapshot + +- VS Code uses multiple webviews. Use `agent-browser tab` to list targets and switch to the right one +- Use `agent-browser snapshot -i -C` to include cursor-interactive elements (divs with onclick handlers) + +### Cannot type in Monaco Editor inputs + +- Use `agent-browser press` for individual keystrokes after focusing the input. Focus the chat input with the keyboard shortcut (macOS: `Ctrl+Cmd+I`, Linux/Windows: `Ctrl+Alt+I`). +- `type @ref`, `keyboard type`, and `keyboard inserttext` work on VS Code Insiders but **silently fail on Code OSS** — they complete without error but no text appears. The `press`-per-key approach works universally. +- See the "Interacting with Monaco Editor" section above for the full compatibility matrix. + +## Cleanup / Disconnect + +> **⚠️ IMPORTANT: Always quit Code OSS when you're done.** Code OSS is a full Electron app that consumes significant memory (often 1–4 GB+). Leaving it running in the background will slow your machine considerably. Don't just disconnect agent-browser — **kill the Code OSS process too.** + +```bash +# 1. Disconnect agent-browser +agent-browser close + +# 2. QUIT Code OSS — do not leave it running! +# macOS: Cmd+Q in the app window, or: +# Find the process +lsof -i :9224 | grep LISTEN +# Kill it (replace with the actual PID) +kill + +# Linux: +# kill $(lsof -t -i :9224) + +# Windows: +# taskkill /F /PID +# Or use Task Manager to end "Code - OSS" +``` + +If you launched with `./scripts/code.sh`, the process name is `Electron` or `Code - OSS`. Verify it's gone: +```bash +# Confirm no process is listening on the debug port +lsof -i :9224 # should return nothing +``` diff --git a/.claude/skills/launch b/.claude/skills/launch new file mode 120000 index 00000000000..b41e2b420ad --- /dev/null +++ b/.claude/skills/launch @@ -0,0 +1 @@ +../../.agents/skills/launch \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d031af36fdd..e8dfe47face 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.19", "@webgpu/types": "^0.1.66", + "agent-browser": "^0.16.3", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", @@ -202,6 +203,37 @@ "node": ">=18" } }, + "node_modules/@appium/logger": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-1.7.1.tgz", + "integrity": "sha512-9C2o9X/lBEDBUnKfAi3mRo9oG7Z03nmISLwsGkWxIWjMAvBdJD0RRSJMekWVKzfXN3byrI1WlCXTITzN4LAoLw==", + "dev": true, + "license": "ISC", + "dependencies": { + "console-control-strings": "1.1.0", + "lodash": "4.17.21", + "lru-cache": "10.4.3", + "set-blocking": "2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=8" + } + }, + "node_modules/@appium/logger/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@appium/logger/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@azure-rest/ai-translation-text": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.0-beta.1.tgz", @@ -1284,15 +1316,6 @@ "node": ">=18.0.0" } }, - "node_modules/@isaacs/fs-minipass/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2115,6 +2138,58 @@ "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", "license": "MIT" }, + "node_modules/@promptbook/utils": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", + "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "CC-BY-4.0", + "dependencies": { + "spacetrim": "0.11.59" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2218,6 +2293,13 @@ "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2496,6 +2578,13 @@ "@types/sinon": "*" } }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/svgo": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@types/svgo/-/svgo-1.3.6.tgz", @@ -2534,6 +2623,13 @@ "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", "dev": true }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.7", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", @@ -2553,6 +2649,16 @@ "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg= sha512-c4m/hnOI1j34i8hXlkZzelE6SXfOqaTWhBp0UgBuwmpiafh22OpsE261Rlg//agZtQHIY5cMgbkX8bnthUFrmA==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -3380,16 +3486,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/l10n-dev/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@vscode/native-watchdog": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@vscode/native-watchdog/-/native-watchdog-1.4.6.tgz", @@ -3582,16 +3678,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/test-cli/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@vscode/test-electron": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", @@ -3731,6 +3817,224 @@ "hasInstallScript": true, "license": "MIT" }, + "node_modules/@wdio/config": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.24.0.tgz", + "integrity": "sha512-rcHu0eG16rSEmHL0sEKDcr/vYFmGhQ5GOlmlx54r+1sgh6sf136q+kth4169s16XqviWGW3LjZbUfpTK29pGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.24.0", + "@wdio/utils": "9.24.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@wdio/config/node_modules/glob": { + "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": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.24.0.tgz", + "integrity": "sha512-ozQKYddBLT4TRvU9J+fGrhVUtx3iDAe+KNCJcTDMFMxNSdDMR2xFQdNp8HLHypspk58oXTYCvz6ZYjySthhqsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/types": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.24.0.tgz", + "integrity": "sha512-PYYunNl8Uq1r8YMJAK6ReRy/V/XIrCSyj5cpCtR5EqCL6heETOORFj7gt4uPnzidfgbtMBcCru0LgjjlMiH1UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/utils": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.24.0.tgz", + "integrity": "sha512-6WhtzC5SNCGRBTkaObX6A07Ofnnyyf+TQH/d/fuhZRqvBknrP4AMMZF+PFxGl1fwdySWdBn+gV2QLE+52Byowg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.24.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^6.1.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/utils/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", @@ -3866,12 +4170,37 @@ "addons/*" ] }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.22", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.22.tgz", + "integrity": "sha512-0KlzbVR6r8irIX2o3zvUlosBDef62VDl47oUfa1U/qgEs67h4/eGBrX/6HWa1RQbt+J6sAeVmtyFKbTHNdF8qQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3917,6 +4246,37 @@ "node": ">= 14" } }, + "node_modules/agent-browser": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.16.3.tgz", + "integrity": "sha512-dsg8PTJNBIQ7/LPp/La42KQwLTzsP8sudbCLpP1atsJXps4Fbuz1CeepUJAGrgxb8koc9y4yKobYVPAsds8hPQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-simctl": "^7.4.0", + "playwright-core": "^1.57.0", + "webdriverio": "^9.15.0", + "ws": "^8.19.0", + "zod": "^3.22.4" + }, + "bin": { + "agent-browser": "bin/agent-browser.js" + } + }, + "node_modules/agent-browser/node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -4053,6 +4413,261 @@ "node": ">=0.10.0" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "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": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -4074,6 +4689,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -4307,6 +4932,26 @@ "node": ">=0.10.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "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", @@ -4340,6 +4985,21 @@ "node": ">= 0.10" } }, + "node_modules/asyncbox": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-3.0.0.tgz", + "integrity": "sha512-X7U0nedUMKV3nn9c4R0Zgvdvv6cw97tbDlHSZicq1snGPi/oX9DgGmFSURWtxDdnBWd3V0YviKhqAYAVvoWQ/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bluebird": "^3.5.1", + "lodash": "^4.17.4", + "source-map-support": "^0.x" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4550,6 +5210,16 @@ "node": ">= 0.8" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -4590,6 +5260,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -4972,6 +5649,60 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5423,6 +6154,109 @@ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5469,6 +6303,13 @@ "proto-list": "~1.2.1" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -5578,6 +6419,106 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5682,6 +6623,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-shorthand-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "dev": true, + "license": "MIT" + }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -5695,6 +6643,12 @@ "node": ">=8.0.0" } }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -5729,6 +6683,16 @@ "type": "^1.0.1" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debounce": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", @@ -5874,6 +6838,16 @@ "node": ">=4.0.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -5986,6 +6960,21 @@ "node": ">=0.10.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.6.tgz", @@ -6117,10 +7106,11 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -6206,6 +7196,86 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edgedriver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.3.0.tgz", + "integrity": "sha512-ggEQL+oEyIcM4nP2QC3AtCQ04o4kDNefRM3hja0odvlPSnsaxiruMxEZ93v3gDCKWYW6BXUr51PPradb+3nffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^5.3.3", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "which": "^6.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/edgedriver/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/edgedriver/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/edgedriver/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/editorconfig": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz", @@ -6299,6 +7369,33 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6490,6 +7587,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.36.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", @@ -6710,6 +7829,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -6787,11 +7920,22 @@ "through": "~2.3.1" } }, - "node_modules/events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } @@ -7308,6 +8452,39 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz", + "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", @@ -7874,6 +9051,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geckodriver": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz", + "integrity": "sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "modern-tar": "^0.7.2" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/geckodriver/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7917,6 +9129,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -7965,6 +9190,21 @@ "once": "^1.3.1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -8524,6 +9764,13 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true, + "license": "MIT" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -10008,6 +11255,46 @@ "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", "dev": true }, + "node_modules/htmlfy": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -10278,6 +11565,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -10909,6 +12207,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -11491,6 +12799,41 @@ "uc.micro": "^2.0.0" } }, + "node_modules/locate-app": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", + "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@promptbook/utils": "0.69.5", + "type-fest": "4.26.0", + "userhome": "1.0.1" + } + }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11544,6 +12887,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11560,6 +12910,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12069,12 +13440,12 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { @@ -12089,14 +13460,12 @@ "node": ">= 18" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" }, "node_modules/mixin-deep": { "version": "1.3.2", @@ -12338,6 +13707,16 @@ "node": ">=10" } }, + "node_modules/modern-tar": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.5.tgz", + "integrity": "sha512-YTefgdpKKFgoTDbEUqXqgUJct2OG6/4hs4XWLsxcHkDLj/x/V8WmKIRppPnXP5feQ7d1vuYWSp3qKkxfwaFaxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -12497,6 +13876,16 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -12610,6 +13999,133 @@ "dev": true, "license": "MIT" }, + "node_modules/node-simctl": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-7.7.5.tgz", + "integrity": "sha512-lWflzDW9xLuOOvR6mTJ9efbDtO/iSCH6rEGjxFxTV0vGgz5XjoZlW2BkNCCZib0B6Y23tCOiYhYJaMQYB8FKIQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@appium/logger": "^1.3.0", + "asyncbox": "^3.0.0", + "bluebird": "^3.5.1", + "lodash": "^4.2.1", + "rimraf": "^5.0.0", + "semver": "^7.0.0", + "source-map-support": "^0.x", + "teen_process": "^2.2.0", + "uuid": "^11.0.1", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=8" + } + }, + "node_modules/node-simctl/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-simctl/node_modules/glob": { + "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": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-simctl/node_modules/isexe": { + "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": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-simctl/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-simctl/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-simctl/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/node-simctl/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", @@ -13324,6 +14840,40 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -13395,6 +14945,59 @@ "node": ">=0.10.0" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13864,6 +15467,36 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -13965,6 +15598,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true, + "license": "MIT" + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -14225,6 +15865,39 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -14561,6 +16234,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -14602,6 +16292,13 @@ "node": ">=0.10.0" } }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -14710,6 +16407,16 @@ } ] }, + "node_modules/safaridriver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.1.tgz", + "integrity": "sha512-jkg4434cYgtrIF2AeY/X0Wmd2W73cK5qIEFE3hDrrQenJH/2SDJIXGvPAigfvQTcE9+H31zkiNHbUqcihEiMRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -14724,6 +16431,36 @@ "ret": "~0.1.10" } }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-regex2/node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -14748,9 +16485,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -15436,11 +17173,12 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" }, @@ -15496,6 +17234,23 @@ "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, + "node_modules/spacetrim": { + "version": "0.11.59", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", + "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0" + }, "node_modules/sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", @@ -15561,6 +17316,16 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -15930,6 +17695,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -16168,15 +17946,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -16195,6 +17964,23 @@ "node": ">=22" } }, + "node_modules/teen_process": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-2.3.3.tgz", + "integrity": "sha512-NIdeetf/6gyEqLjnzvfgQe7PfipSceq2xDQM2Py2BkBnIIeWh3HRD3vNhulyO5WppfCv9z4mtsEHyq8kdiULTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bluebird": "^3.7.2", + "lodash": "^4.17.21", + "shell-quote": "^1.8.1", + "source-map-support": "^0.x" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0", + "npm": ">=8" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -17027,6 +18813,13 @@ "requires-port": "^1.0.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -17036,6 +18829,16 @@ "node": ">=0.10.0" } }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -17314,18 +19117,200 @@ "dev": true, "license": "MIT" }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", "integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==", "dev": true }, + "node_modules/webdriver": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.24.0.tgz", + "integrity": "sha512-2R31Ey83NzMsafkl4hdFq6GlIBvOODQMkueLjeRqYAITu3QCYiq9oqBdnWA6CdePuV4dbKlYsKRX0mwMiPclDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.24.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.24.0", + "@wdio/types": "9.24.0", + "@wdio/utils": "9.24.0", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriver/node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/webdriverio": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.24.0.tgz", + "integrity": "sha512-LTJt6Z/iDM0ne/4ytd3BykoPv9CuJ+CAILOzlwFeMGn4Mj02i4Bk2Rg9o/jeJ89f52hnv4OPmNjD0e8nzWAy5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.24.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.24.0", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.24.0", + "@wdio/utils": "9.24.0", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.24.0" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriverio/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriverio/node_modules/serialize-error": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.31.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -17483,6 +19468,28 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -17657,6 +19664,94 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 7567da0e40b..8291e70e334 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.19", "@webgpu/types": "^0.1.66", + "agent-browser": "^0.16.3", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", From 79af825700d599ce667996d752706bbe2a21c487 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:38:04 -0800 Subject: [PATCH 212/448] permissions picker warnings and improved hover (#299331) add warnings when switching, setting based, better picker --- .../actionWidget/browser/actionList.ts | 12 ++- .../actionWidget/browser/actionWidget.css | 25 +++++++ .../browser/widget/input/chatInputPart.ts | 4 +- .../input/permissionPickerActionItem.ts | 75 +++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index a871c7a2b8f..71f8ee665a2 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -369,6 +369,12 @@ export interface IActionListOptions { */ readonly minWidth?: number; + /** + * When true, descriptions are rendered as subtext below the title + * instead of inline to the right. + */ + readonly descriptionBelow?: boolean; + /** @@ -383,7 +389,7 @@ export class ActionList extends Disposable { private readonly _list: List>; - private readonly _actionLineHeight = 24; + private readonly _actionLineHeight: number; private readonly _headerLineHeight = 24; private readonly _separatorLineHeight = 8; @@ -431,6 +437,10 @@ export class ActionList extends Disposable { super(); this.domNode = document.createElement('div'); this.domNode.classList.add('actionList'); + if (this._options?.descriptionBelow) { + this.domNode.classList.add('description-below'); + } + this._actionLineHeight = this._options?.descriptionBelow ? 48 : 24; // Initialize collapsed sections if (this._options?.collapsedByDefault) { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 74c89c2c486..3c2022bd29d 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -217,6 +217,31 @@ font-size: 12px; } +/* Description below mode — shows descriptions as subtext under the title */ +.action-widget .description-below .monaco-list .monaco-list-row.action { + flex-wrap: wrap; + align-content: center; + padding-top: 6px; + padding-right: 2px; + + .title { + line-height: 14px; + } + + .description { + display: block !important; + width: 100%; + margin-left: 0; + padding-left: 18px; + font-size: 11px; + line-height: 14px; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + /* Item toolbar - shows on hover/focus */ .action-widget .monaco-list-row.action .action-list-item-toolbar { display: none; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index db90c77372d..45f4a15d648 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -904,8 +904,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.selectedToolsModel.resetSessionEnablementState(); this._chatSessionIsEmpty = chatSessionIsEmpty; - // Reset permission level to default on new sessions - if (chatSessionIsEmpty) { + // Reset permission level on new sessions, unless global auto-approve is on + if (chatSessionIsEmpty && !this.configurationService.getValue(ChatConfiguration.GlobalAutoApprove)) { this._currentPermissionLevel.set(ChatPermissionLevel.Default, undefined); this.permissionLevelKey.set(ChatPermissionLevel.Default); this.permissionWidget?.refresh(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 5f0cc0988d2..26f6954832c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -18,8 +18,26 @@ import { ITelemetryService } from '../../../../../../platform/telemetry/common/t import { ChatConfiguration, ChatPermissionLevel } from '../../../common/constants.js'; import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; +import Severity from '../../../../../../base/common/severity.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +// Track whether warnings have been shown this VS Code session +const shownWarnings = new Set(); + +function hasShownElevatedWarning(level: ChatPermissionLevel): boolean { + if (shownWarnings.has(level)) { + return true; + } + // Autopilot is stricter than AutoApprove, so confirming Autopilot + // implies the user already accepted the AutoApprove risks. + if (level === ChatPermissionLevel.AutoApprove && shownWarnings.has(ChatPermissionLevel.Autopilot)) { + return true; + } + return false; +} + export interface IPermissionPickerDelegate { readonly currentPermissionLevel: IObservable; readonly setPermissionLevel: (level: ChatPermissionLevel) => void; @@ -35,6 +53,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService, ) { const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; const isAutopilotEnabled = () => configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; @@ -47,6 +66,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.default', label: localize('permissions.default', "Default Approvals"), + description: localize('permissions.default.subtext', "Copilot uses your configured settings"), icon: ThemeIcon.fromId(Codicon.shield.id), checked: currentLevel === ChatPermissionLevel.Default, tooltip: '', @@ -65,6 +85,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.autoApprove', label: localize('permissions.autoApprove', "Bypass Approvals"), + description: localize('permissions.autoApprove.subtext', "All tool calls are auto-approved"), icon: ThemeIcon.fromId(Codicon.warning.id), checked: currentLevel === ChatPermissionLevel.AutoApprove, enabled: !policyRestricted, @@ -76,6 +97,32 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { position: pickerOptions.hoverPosition }, run: async () => { + if (!hasShownElevatedWarning(ChatPermissionLevel.AutoApprove)) { + const result = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('permissions.autoApprove.warning.title', "Enable Bypass Approvals?"), + buttons: [ + { + label: localize('permissions.autoApprove.warning.confirm', "Enable"), + run: () => true + }, + { + label: localize('permissions.autoApprove.warning.cancel', "Cancel"), + run: () => false + }, + ], + custom: { + icon: Codicon.warning, + markdownDetails: [{ + markdown: new MarkdownString(localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.")), + }], + }, + }); + if (result.result !== true) { + return; + } + shownWarnings.add(ChatPermissionLevel.AutoApprove); + } delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove); if (this.element) { this.renderLabel(this.element); @@ -88,6 +135,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.autopilot', label: localize('permissions.autopilot', "Autopilot (Preview)"), + description: localize('permissions.autopilot.subtext', "Copilot handles it from start to finish"), icon: ThemeIcon.fromId(Codicon.rocket.id), checked: currentLevel === ChatPermissionLevel.Autopilot, enabled: !policyRestricted, @@ -99,6 +147,32 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { position: pickerOptions.hoverPosition }, run: async () => { + if (!hasShownElevatedWarning(ChatPermissionLevel.Autopilot)) { + const result = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('permissions.autopilot.warning.title', "Enable Autopilot?"), + buttons: [ + { + label: localize('permissions.autopilot.warning.confirm', "Enable"), + run: () => true + }, + { + label: localize('permissions.autopilot.warning.cancel', "Cancel"), + run: () => false + }, + ], + custom: { + icon: Codicon.rocket, + markdownDetails: [{ + markdown: new MarkdownString(localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.")), + }], + }, + }); + if (result.result !== true) { + return; + } + shownWarnings.add(ChatPermissionLevel.Autopilot); + } delegate.setPermissionLevel(ChatPermissionLevel.Autopilot); if (this.element) { this.renderLabel(this.element); @@ -113,6 +187,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { super(action, { actionProvider, reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, + listOptions: { descriptionBelow: true, minWidth: 232 }, }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); } From a3c86528c3b11f7eb62b2b9b4549dddb50deca6c Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:00:40 -0800 Subject: [PATCH 213/448] add telemetry for chat permissions on request (#299349) add telemetry --- .../contrib/chat/common/chatService/chatServiceTelemetry.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts index 2a0f2cf7cc8..8dfb8f5333d 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts @@ -11,7 +11,7 @@ import { ChatRequestModel, IChatRequestVariableData } from '../model/chatModel.j import { ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from '../requestParser/chatParserTypes.js'; import { ChatAgentVoteDirection, ChatCopyKind, IChatSendRequestOptions, IChatUserActionEvent } from './chatService.js'; import { isImageVariableEntry } from '../attachments/chatVariableEntries.js'; -import { ChatAgentLocation } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ILanguageModelsService } from '../languageModels.js'; import { chatSessionResourceToId } from '../model/chatUri.js'; @@ -149,6 +149,7 @@ export type ChatProviderInvokedEvent = { enableCommandDetection: boolean; attachmentKinds: string[]; model: string | undefined; + permissionLevel: ChatPermissionLevel | undefined; }; export type ChatProviderInvokedClassification = { @@ -167,6 +168,7 @@ export type ChatProviderInvokedClassification = { enableCommandDetection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether participation detection was disabled for this invocation.' }; attachmentKinds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The types of variables/attachments that the user included with their query.' }; model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model used to generate the response.' }; + permissionLevel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The tool auto-approval permission level selected in the permission picker (default, autoApprove, or autopilot). Undefined when the picker is not applicable (e.g. ask mode or API-driven requests).' }; owner: 'roblourens'; comment: 'Provides insight into the performance of Chat agents.'; }; @@ -303,6 +305,7 @@ export class ChatRequestTelemetry { numCodeBlocks: getCodeBlocks(request.response?.response.toString() ?? '').length, attachmentKinds: this.attachmentKindsForTelemetry(request.variableData), model: this.resolveModelId(this.opts.options?.userSelectedModelId), + permissionLevel: this.opts.options?.modeInfo?.kind === ChatModeKind.Ask ? undefined : this.opts.options?.modeInfo?.permissionLevel, }); } From efd866344598bf2bfc7eb686bbc33a5e798b3311 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:10 -0800 Subject: [PATCH 214/448] Customizations: Adjust search bar height in layout methods for consistency across widgets (#299333) Adjust search bar height in layout methods for consistency across widgets --- .../chat/browser/aiCustomization/aiCustomizationListWidget.ts | 2 +- .../contrib/chat/browser/aiCustomization/mcpListWidget.ts | 2 +- .../browser/aiCustomization/media/aiCustomizationManagement.css | 2 +- .../contrib/chat/browser/aiCustomization/pluginListWidget.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index e890fb82a04..8a77a8e5223 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -1155,7 +1155,7 @@ export class AICustomizationListWidget extends Disposable { */ layout(height: number, width: number): void { const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; - const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 52; const listHeight = height - sectionFooterHeight - searchBarHeight; this.searchInput.layout(); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index e92639f9b95..e16ecec8d57 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -777,7 +777,7 @@ export class McpListWidget extends Disposable { */ layout(height: number, width: number): void { const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; - const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 52; const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index d9781fd7664..81651ef26dd 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -166,7 +166,7 @@ align-items: center; gap: 8px; flex-shrink: 0; - margin: 6px 0px; + padding: 6px 0; } .ai-customization-list-widget .list-search-container, diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts index 52e409f290b..ff1135b9a58 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -795,7 +795,7 @@ export class PluginListWidget extends Disposable { layout(height: number, width: number): void { const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; - const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 52; const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight; From 07565be34aa660675ae5bd4e2cd7de701b97fc9c Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 4 Mar 2026 18:35:24 -0800 Subject: [PATCH 215/448] Disable while running --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 80a7be9fbd2..13f21962ff8 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -584,6 +584,7 @@ export class ChangesViewPane extends ViewPane { menuId, { telemetrySource: 'changesView', + disableWhileRunning: isSessionMenu, menuOptions: isSessionMenu && sessionResource ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, From b9c51b0c593cf0ce680fae690960f57127e64df9 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 4 Mar 2026 18:55:11 -0800 Subject: [PATCH 216/448] context key rename --- src/vs/sessions/contrib/changesView/browser/changesView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 13f21962ff8..1ef5586817f 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -560,7 +560,7 @@ export class ChangesViewPane extends ViewPane { })); // Set context key for PR state from session metadata - const hasOpenPullRequestKey = scopedContextKeyService.createKey('github.copilot.chat.copilotCLI.hasOpenPullRequest', false); + const hasOpenPullRequestKey = scopedContextKeyService.createKey('sessions.hasOpenPullRequest', false); this.renderDisposables.add(autorun(reader => { const sessionResource = activeSessionResource.read(reader); sessionsChangedSignal.read(reader); From b929e4a80a4efef27d78998dad24e015ed7313cd Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 20:49:49 -0800 Subject: [PATCH 217/448] chat: add marketplace trust prompt for agent plugin installation (#299354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds trust confirmation dialog requiring users to explicitly trust a marketplace before installing plugins from it. Protects against accidental plugin installation from untrusted sources. - Introduces observableMemento for storing trusted marketplace canonical IDs in persistent storage (StorageScope.APPLICATION), tied to user profiles, never expiring. - Trust is scoped per-marketplace (by canonicalId), so trusting one marketplace trusts all plugins sourced from it. Reduces friction for plugins from the same trusted source. - Trust gate applies to all plugin source kinds (RelativePath, GitHub, GitUrl, npm, pip) — for npm/pip it's additive to the existing terminal command confirmation. - Expands IPluginMarketplaceService with isMarketplaceTrusted() and trustMarketplace() methods, and injects IDialogService into PluginInstallService. (Commit message generated by Copilot) --- .../chat/browser/pluginInstallService.ts | 32 ++++++++ .../plugins/pluginMarketplaceService.ts | 30 ++++++++ .../plugins/pluginInstallService.test.ts | 76 +++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts index 5beccafafbc..ebe741adaad 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from '../../../../base/common/codicons.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; @@ -20,10 +22,15 @@ export class PluginInstallService implements IPluginInstallService { @IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService, @IFileService private readonly _fileService: IFileService, @INotificationService private readonly _notificationService: INotificationService, + @IDialogService private readonly _dialogService: IDialogService, @ILogService private readonly _logService: ILogService, ) { } async installPlugin(plugin: IMarketplacePlugin): Promise { + if (!await this._ensureMarketplaceTrusted(plugin)) { + return; + } + const kind = plugin.sourceDescriptor.kind; if (kind === PluginSourceKind.RelativePath) { @@ -61,6 +68,31 @@ export class PluginInstallService implements IPluginInstallService { return this._pluginRepositoryService.getPluginSourceInstallUri(plugin.sourceDescriptor); } + // --- Trust gate ------------------------------------------------------------- + + private async _ensureMarketplaceTrusted(plugin: IMarketplacePlugin): Promise { + if (this._pluginMarketplaceService.isMarketplaceTrusted(plugin.marketplaceReference)) { + return true; + } + + const { confirmed } = await this._dialogService.confirm({ + type: 'question', + message: localize('trustMarketplace', "Trust Plugins from '{0}'?", plugin.marketplaceReference.displayLabel), + detail: localize('trustMarketplaceDetail', "Plugins can run code on your machine. Only install plugins from sources you trust.\n\nSource: {0}", plugin.marketplaceReference.rawValue), + primaryButton: localize({ key: 'trustAndInstall', comment: ['&& denotes a mnemonic'] }, "&&Trust"), + custom: { + icon: Codicon.shield, + }, + }); + + if (!confirmed) { + return false; + } + + this._pluginMarketplaceService.trustMarketplace(plugin.marketplaceReference); + return true; + } + // --- Relative-path source (existing git-based flow) ----------------------- private async _installRelativePathPlugin(plugin: IMarketplacePlugin): Promise { diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index dbd6e9b7b17..86054f48450 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -154,6 +154,10 @@ export interface IPluginMarketplaceService { addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void; removeInstalledPlugin(pluginUri: URI): void; setInstalledPluginEnabled(pluginUri: URI, enabled: boolean): void; + /** Returns whether the given marketplace has been explicitly trusted by the user. */ + isMarketplaceTrusted(ref: IMarketplaceReference): boolean; + /** Records that the user trusts the given marketplace, persisted permanently. */ + trustMarketplace(ref: IMarketplaceReference): void; } /** @@ -209,10 +213,21 @@ const installedPluginsMemento = observableMemento({ + defaultValue: [], + key: 'chat.plugins.trustedMarketplaces.v1', + toStorage: value => JSON.stringify(value), + fromStorage: value => { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + }, +}); + export class PluginMarketplaceService extends Disposable implements IPluginMarketplaceService { declare readonly _serviceBrand: undefined; private readonly _gitHubMarketplaceCache = new Lazy>(() => this._loadPersistedGitHubMarketplaceCache()); private readonly _installedPluginsStore: ObservableMemento; + private readonly _trustedMarketplacesStore: ObservableMemento; readonly onDidChangeMarketplaces: Event; @@ -232,6 +247,10 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke installedPluginsMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, _storageService) ); + this._trustedMarketplacesStore = this._register( + trustedMarketplacesMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, _storageService) + ); + this.installedPlugins = this._installedPluginsStore.map(s => (revive(s) as readonly IMarketplaceInstalledPlugin[]).map(e => ({ ...e, @@ -456,6 +475,17 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke ); } + isMarketplaceTrusted(ref: IMarketplaceReference): boolean { + return this._trustedMarketplacesStore.get().includes(ref.canonicalId); + } + + trustMarketplace(ref: IMarketplaceReference): void { + const current = this._trustedMarketplacesStore.get(); + if (!current.includes(ref.canonicalId)) { + this._trustedMarketplacesStore.set([...current, ref.canonicalId], undefined); + } + } + private async _fetchFromClonedRepo(reference: IMarketplaceReference, token: CancellationToken): Promise { let repoDir: URI; try { diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts index a0652f46641..1d8b6862fa3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts @@ -62,6 +62,10 @@ suite('PluginInstallService', () => { terminalCompletes: boolean; pullRepositoryCalls: { marketplace: IMarketplaceReference; options?: IPullRepositoryOptions }[]; updatePluginSourceCalls: { plugin: IMarketplacePlugin; options?: IPullRepositoryOptions }[]; + /** Whether the marketplace is already trusted */ + marketplaceTrusted: boolean; + /** Canonical IDs that were trusted via trustMarketplace() */ + trustedMarketplaces: string[]; } function createDefaults(): MockState { @@ -78,6 +82,8 @@ suite('PluginInstallService', () => { terminalCompletes: true, pullRepositoryCalls: [], updatePluginSourceCalls: [], + marketplaceTrusted: true, + trustedMarketplaces: [], }; } @@ -237,6 +243,10 @@ suite('PluginInstallService', () => { addInstalledPlugin: (uri: URI, plugin: IMarketplacePlugin) => { state.addedPlugins.push({ uri: uri.toString(), plugin }); }, + isMarketplaceTrusted: () => state.marketplaceTrusted, + trustMarketplace: (ref: IMarketplaceReference) => { + state.trustedMarketplaces.push(ref.canonicalId); + }, } as unknown as IPluginMarketplaceService); const service = instantiationService.createInstance(PluginInstallService); @@ -347,6 +357,8 @@ suite('PluginInstallService', () => { instantiationService.stub(IProgressService, { withProgress: async (_o: unknown, cb: () => Promise) => cb() } as unknown as IProgressService); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IPluginMarketplaceService, { addInstalledPlugin: () => { } } as unknown as IPluginMarketplaceService); + instantiationService.stub(IPluginMarketplaceService, 'isMarketplaceTrusted', () => true); + instantiationService.stub(IPluginMarketplaceService, 'trustMarketplace', () => { }); const svc = instantiationService.createInstance(PluginInstallService); const plugin = createPlugin({ @@ -671,4 +683,68 @@ suite('PluginInstallService', () => { assert.ok(state.terminalCommands[0].includes('pip')); }); }); + + // ========================================================================= + // installPlugin — marketplace trust + // ========================================================================= + + suite('installPlugin — marketplace trust', () => { + + test('skips trust prompt when marketplace is already trusted', async () => { + const { service, state } = createService({ marketplaceTrusted: true }); + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.addedPlugins.length, 1); + assert.strictEqual(state.trustedMarketplaces.length, 0, 'should not re-trust'); + }); + + test('shows trust prompt and installs when user confirms', async () => { + const { service, state } = createService({ marketplaceTrusted: false, dialogConfirmResult: true }); + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.trustedMarketplaces.length, 1); + assert.strictEqual(state.addedPlugins.length, 1); + }); + + test('does not install when user declines trust', async () => { + const { service, state } = createService({ marketplaceTrusted: false, dialogConfirmResult: false }); + const plugin = createPlugin({ + source: 'plugins/myPlugin', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/myPlugin' }, + }); + + await service.installPlugin(plugin); + + assert.strictEqual(state.trustedMarketplaces.length, 0); + assert.strictEqual(state.addedPlugins.length, 0); + }); + + test('trust prompt applies to all source kinds', async () => { + const { service, state } = createService({ marketplaceTrusted: false, dialogConfirmResult: false }); + + const kinds: IPluginSourceDescriptor[] = [ + { kind: PluginSourceKind.RelativePath, path: 'p' }, + { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + { kind: PluginSourceKind.GitUrl, url: 'https://example.com/repo.git' }, + { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + ]; + + for (const sourceDescriptor of kinds) { + await service.installPlugin(createPlugin({ sourceDescriptor })); + } + + assert.strictEqual(state.addedPlugins.length, 0, 'no plugins should be installed when trust is declined'); + }); + }); }); From 7344939be3791776534e4176dc57e10e9da137bc Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:13:46 -0800 Subject: [PATCH 218/448] [Terminal_Sandboxing]Adding default allowWrite folders. (#299367) * code changes * updating tmp folder based on OS --- .../chatAgentTools/common/terminalSandboxService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts index b3ce3a196ce..3ff42709b51 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts @@ -47,6 +47,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb private _remoteEnvDetails: IRemoteAgentEnvironment | null = null; private _appRoot: string; private _os: OperatingSystem = OS; + private _defaultWritePaths: string[] = ['~/.npm']; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -163,6 +164,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb ? this._configurationService.getValue<{ denyRead?: string[]; allowWrite?: string[]; denyWrite?: string[] }>(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ?? {} : {}; const configFileUri = URI.joinPath(this._tempDir, `vscode-sandbox-settings-${this._sandboxSettingsId}.json`); + const defaultAllowWrite = [...this._defaultWritePaths]; + const linuxAllowWrite = [...new Set([...defaultAllowWrite, ...(linuxFileSystemSetting.allowWrite ?? [])])]; + const macAllowWrite = [...new Set([...defaultAllowWrite, ...(macFileSystemSetting.allowWrite ?? [])])]; let allowedDomains = networkSetting.allowedDomains ?? []; if (networkSetting.allowTrustedDomains) { @@ -176,7 +180,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb }, filesystem: { denyRead: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyRead : linuxFileSystemSetting.denyRead, - allowWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.allowWrite : linuxFileSystemSetting.allowWrite, + allowWrite: this._os === OperatingSystem.Macintosh ? macAllowWrite : linuxAllowWrite, denyWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyWrite : linuxFileSystemSetting.denyWrite, } }; @@ -203,6 +207,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI }; this._tempDir = environmentService.tmpDir; } + if (this._tempDir) { + this._defaultWritePaths.push(this._tempDir.path); + } if (!this._tempDir) { this._logService.warn('TerminalSandboxService: Cannot create sandbox settings file because no tmpDir is available in this environment'); } From bcd6b6b1df939a21c9d9f513008c2562576a465d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Mar 2026 17:43:23 +1100 Subject: [PATCH 219/448] Display github copilot tools in Sessions Window (#299314) * Display github copilot tools in Sessions Window * Update src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../languageProviders/promptHeaderAutocompletion.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 3b4151e65eb..de33bb91fd0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -21,6 +21,7 @@ import { localize } from '../../../../../../nls.js'; import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.js'; import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; +import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { PromptsConfig } from '../config/config.js'; @@ -40,6 +41,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @IChatModeService private readonly chatModeService: IChatModeService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -221,8 +223,8 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { if (value.type === 'sequence') { // if the position is inside the tools metadata, we provide tool name completions const getValues = async () => { - if (target === Target.GitHubCopilot) { - // for GitHub Copilot agent files, we only suggest the known set of tools that are supported by GitHub Copilot, instead of all tools that the user has defined, because many tools won't work with GitHub Copilot and it would be frustrating for users to select a tool that doesn't work + if (target === Target.GitHubCopilot || this.environmentService.isSessionsWindow) { + // for GitHub Copilot targets and the Sessions Window, we only suggest the known set of tools that are supported by GitHub Copilot, instead of all tools that the user has defined, because many tools won't work in these contexts and it would be frustrating for users to select a tool that doesn't work return knownGithubCopilotTools; } else if (target === Target.Claude) { return knownClaudeTools; From 5ee6f4f53267a1cfb34264a480b39c1af3ab182c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 5 Mar 2026 07:59:20 +0100 Subject: [PATCH 220/448] fix reading configuration from workspace folders (#299302) * fix reading configuration from workspace folders * feedback --- eslint.config.js | 1 + .../electron-browser/sessions.main.ts | 5 +- .../browser/configurationService.ts | 366 +++++++++++++++++- .../test/browser/configurationService.test.ts | 339 ++++++++++++++++ .../browser/workspaceContextService.ts | 11 +- .../preferences/browser/preferencesWidgets.ts | 2 +- 6 files changed, 710 insertions(+), 14 deletions(-) create mode 100644 src/vs/sessions/services/configuration/test/browser/configurationService.test.ts diff --git a/eslint.config.js b/eslint.config.js index ec5efb7c5fc..9714d00e8bc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2015,6 +2015,7 @@ export default tseslint.config( 'vs/editor/contrib/*/~', 'vs/workbench/~', 'vs/workbench/services/*/~', + 'vs/sessions/services/*/~', { 'when': 'test', 'pattern': 'vs/workbench/contrib/*/~' diff --git a/src/vs/sessions/electron-browser/sessions.main.ts b/src/vs/sessions/electron-browser/sessions.main.ts index 3a5ed7dff30..2dc3a764678 100644 --- a/src/vs/sessions/electron-browser/sessions.main.ts +++ b/src/vs/sessions/electron-browser/sessions.main.ts @@ -352,14 +352,15 @@ export class SessionsMain extends Disposable { logService: ILogService, policyService: IPolicyService ): Promise<{ configurationService: ConfigurationService; workspaceContextService: SessionsWorkspaceContextService }> { - const configurationService = new ConfigurationService(userDataProfileService.currentProfile.settingsResource, fileService, policyService, logService); + const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService); + const configurationService = new ConfigurationService(userDataProfileService, workspaceContextService, uriIdentityService, fileService, policyService, logService); try { await configurationService.initialize(); } catch (error) { onUnexpectedError(error); } - const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService, configurationService); + workspaceContextService.setConfigurationService(configurationService); return { configurationService, workspaceContextService }; } diff --git a/src/vs/sessions/services/configuration/browser/configurationService.ts b/src/vs/sessions/services/configuration/browser/configurationService.ts index 3c145277fa4..01fb00152a6 100644 --- a/src/vs/sessions/services/configuration/browser/configurationService.ts +++ b/src/vs/sessions/services/configuration/browser/configurationService.ts @@ -3,25 +3,375 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from '../../../../base/common/event.js'; -import { Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { ConfigurationService as BaseConfigurationService } from '../../../../platform/configuration/common/configurationService.js'; +import { onUnexpectedError } from '../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Queue } from '../../../../base/common/async.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { JSONPath, ParseError, parse } from '../../../../base/common/json.js'; +import { applyEdits, setProperty } from '../../../../base/common/jsonEdit.js'; +import { Edit, FormattingOptions } from '../../../../base/common/jsonFormatter.js'; +import { equals } from '../../../../base/common/objects.js'; +import { distinct, equals as arrayEquals } from '../../../../base/common/arrays.js'; +import { OS, OperatingSystem } from '../../../../base/common/platform.js'; +import { IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationUpdateOptions, IConfigurationUpdateOverrides, IConfigurationValue, ConfigurationTarget, isConfigurationOverrides, isConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js'; +import { ConfigurationChangeEvent, ConfigurationModel } from '../../../../platform/configuration/common/configurationModels.js'; +import { DefaultConfiguration, IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from '../../../../platform/configuration/common/configurations.js'; +import { Extensions, IConfigurationRegistry, keyFromOverrideIdentifiers } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IFileService, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IPolicyService, NullPolicyService } from '../../../../platform/policy/common/policy.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService, RestrictedSettings } from '../../../../workbench/services/configuration/common/configuration.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceFolder, WorkbenchState, Workspace } from '../../../../platform/workspace/common/workspace.js'; +import { FolderConfiguration, UserConfiguration } from '../../../../workbench/services/configuration/browser/configuration.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, FOLDER_CONFIG_FOLDER_NAME, FOLDER_SETTINGS_PATH, IWorkbenchConfigurationService, RestrictedSettings } from '../../../../workbench/services/configuration/common/configuration.js'; +import { Configuration } from '../../../../workbench/services/configuration/common/configurationModels.js'; +import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; -// Import to register contributions +// Import to register configuration contributions import '../../../../workbench/services/configuration/browser/configurationService.js'; -export class ConfigurationService extends BaseConfigurationService implements IWorkbenchConfigurationService { - readonly restrictedSettings: RestrictedSettings = { default: [] }; +export class ConfigurationService extends Disposable implements IWorkbenchConfigurationService { + + declare readonly _serviceBrand: undefined; + + private _configuration: Configuration; + private readonly defaultConfiguration: DefaultConfiguration; + private readonly policyConfiguration: IPolicyConfiguration; + private readonly userConfiguration: UserConfiguration; + private readonly cachedFolderConfigs = this._register(new DisposableMap(new ResourceMap())); + + private readonly _onDidChangeConfiguration = this._register(new Emitter()); + readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; + readonly onDidChangeRestrictedSettings = Event.None; + readonly restrictedSettings: RestrictedSettings = { default: [] }; + + private readonly configurationRegistry = Registry.as(Extensions.Configuration); + + private readonly settingsResource: URI; + private readonly configurationEditing: ConfigurationEditing; + + constructor( + userDataProfileService: IUserDataProfileService, + private readonly workspaceService: IWorkspaceContextService, + private readonly uriIdentityService: IUriIdentityService, + private readonly fileService: IFileService, + policyService: IPolicyService, + private readonly logService: ILogService, + ) { + super(); + + this.settingsResource = userDataProfileService.currentProfile.settingsResource; + this.defaultConfiguration = this._register(new DefaultConfiguration(logService)); + this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); + this.userConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, userDataProfileService.currentProfile.mcpResource, {}, fileService, uriIdentityService, logService)); + this.configurationEditing = new ConfigurationEditing(fileService, this); + + this._configuration = new Configuration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + this.workspaceService.getWorkspace() as Workspace, + this.logService + ); + + this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDefaultConfigurationChanged(defaults, properties))); + this._register(this.policyConfiguration.onDidChangeConfiguration(configurationModel => this.onPolicyConfigurationChanged(configurationModel))); + this._register(this.userConfiguration.onDidChangeConfiguration(userConfiguration => this.onUserConfigurationChanged(userConfiguration))); + this._register(this.workspaceService.onWillChangeWorkspaceFolders(e => e.join(this.loadFolderConfigurations(e.changes.added)))); + this._register(this.workspaceService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); + } + + async initialize(): Promise { + const [defaultModel, policyModel, userModel] = await Promise.all([ + this.defaultConfiguration.initialize(), + this.policyConfiguration.initialize(), + this.userConfiguration.initialize() + ]); + const workspace = this.workspaceService.getWorkspace() as Workspace; + this._configuration = new Configuration( + defaultModel, + policyModel, + ConfigurationModel.createEmptyModel(this.logService), + userModel, + ConfigurationModel.createEmptyModel(this.logService), + ConfigurationModel.createEmptyModel(this.logService), + new ResourceMap(), + ConfigurationModel.createEmptyModel(this.logService), + new ResourceMap(), + workspace, + this.logService + ); + await this.loadFolderConfigurations(workspace.folders); + } + + // #region IWorkbenchConfigurationService + + getConfigurationData(): IConfigurationData { + return this._configuration.toData(); + } + + getValue(): T; + getValue(section: string): T; + getValue(overrides: IConfigurationOverrides): T; + getValue(section: string, overrides: IConfigurationOverrides): T; + getValue(arg1?: unknown, arg2?: unknown): unknown { + const section = typeof arg1 === 'string' ? arg1 : undefined; + const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : undefined; + return this._configuration.getValue(section, overrides); + } + + updateValue(key: string, value: unknown): Promise; + updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise; + updateValue(key: string, value: unknown, target: ConfigurationTarget): Promise; + updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, options?: IConfigurationUpdateOptions): Promise; + async updateValue(key: string, value: unknown, arg3?: unknown, arg4?: unknown, _options?: IConfigurationUpdateOptions): Promise { + const overrides: IConfigurationUpdateOverrides | undefined = isConfigurationUpdateOverrides(arg3) ? arg3 + : isConfigurationOverrides(arg3) ? { resource: arg3.resource, overrideIdentifiers: arg3.overrideIdentifier ? [arg3.overrideIdentifier] : undefined } : undefined; + const target: ConfigurationTarget | undefined = (overrides ? arg4 : arg3) as ConfigurationTarget | undefined; + + if (overrides?.overrideIdentifiers) { + overrides.overrideIdentifiers = distinct(overrides.overrideIdentifiers); + overrides.overrideIdentifiers = overrides.overrideIdentifiers.length ? overrides.overrideIdentifiers : undefined; + } + + const inspect = this.inspect(key, { resource: overrides?.resource, overrideIdentifier: overrides?.overrideIdentifiers ? overrides.overrideIdentifiers[0] : undefined }); + if (inspect.policyValue !== undefined) { + throw new Error(`Unable to write ${key} because it is configured in system policy.`); + } + + // Remove the setting, if the value is same as default value + if (equals(value, inspect.defaultValue)) { + value = undefined; + } + + if (overrides?.overrideIdentifiers?.length && overrides.overrideIdentifiers.length > 1) { + const overrideIdentifiers = overrides.overrideIdentifiers.sort(); + const existingOverrides = this._configuration.localUserConfiguration.overrides.find(override => arrayEquals([...override.identifiers].sort(), overrideIdentifiers)); + if (existingOverrides) { + overrides.overrideIdentifiers = existingOverrides.identifiers; + } + } + + const path = overrides?.overrideIdentifiers?.length ? [keyFromOverrideIdentifiers(overrides.overrideIdentifiers), key] : [key]; + + const settingsResource = this.getSettingsResource(target, overrides?.resource ?? undefined); + await this.configurationEditing.write(settingsResource, path, value); + await this.reloadConfiguration(); + } + + private getSettingsResource(target: ConfigurationTarget | undefined, resource: URI | undefined): URI { + if (target === ConfigurationTarget.WORKSPACE_FOLDER || target === ConfigurationTarget.WORKSPACE) { + if (resource) { + const folder = this.workspaceService.getWorkspaceFolder(resource); + if (folder) { + return this.uriIdentityService.extUri.joinPath(folder.uri, FOLDER_SETTINGS_PATH); + } + } + } + return this.settingsResource; + } + + inspect(key: string, overrides?: IConfigurationOverrides): IConfigurationValue { + return this._configuration.inspect(key, overrides); + } + + keys(): { default: string[]; policy: string[]; user: string[]; workspace: string[]; workspaceFolder: string[] } { + return this._configuration.keys(); + } + + async reloadConfiguration(_target?: ConfigurationTarget | IWorkspaceFolder): Promise { + const userModel = await this.userConfiguration.initialize(); + const previousData = this._configuration.toData(); + const change = this._configuration.compareAndUpdateLocalUserConfiguration(userModel); + + // Reload folder configurations + for (const folder of this.workspaceService.getWorkspace().folders) { + const folderConfiguration = this.cachedFolderConfigs.get(folder.uri); + if (folderConfiguration) { + const folderModel = await folderConfiguration.loadConfiguration(); + const folderChange = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderModel); + change.keys.push(...folderChange.keys); + change.overrides.push(...folderChange.overrides); + } + } + + this.triggerConfigurationChange(change, previousData, ConfigurationTarget.USER); + } + + hasCachedConfigurationDefaultsOverrides(): boolean { + return false; + } + async whenRemoteConfigurationLoaded(): Promise { } + isSettingAppliedForAllProfiles(key: string): boolean { - const scope = Registry.as(Extensions.Configuration).getConfigurationProperties()[key]?.scope; + const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope; if (scope && APPLICATION_SCOPES.includes(scope)) { return true; } const allProfilesSettings = this.getValue(APPLY_ALL_PROFILES_SETTING) ?? []; return Array.isArray(allProfilesSettings) && allProfilesSettings.includes(key); } + + // #endregion + + // #region Configuration change handlers + + private onDefaultConfigurationChanged(defaults: ConfigurationModel, properties?: string[]): void { + const previousData = this._configuration.toData(); + const change = this._configuration.compareAndUpdateDefaultConfiguration(defaults, properties); + this._configuration.updateLocalUserConfiguration(this.userConfiguration.reparse()); + for (const folder of this.workspaceService.getWorkspace().folders) { + const folderConfiguration = this.cachedFolderConfigs.get(folder.uri); + if (folderConfiguration) { + this._configuration.updateFolderConfiguration(folder.uri, folderConfiguration.reparse()); + } + } + this.triggerConfigurationChange(change, previousData, ConfigurationTarget.DEFAULT); + } + + private onPolicyConfigurationChanged(policyConfiguration: ConfigurationModel): void { + const previousData = this._configuration.toData(); + const change = this._configuration.compareAndUpdatePolicyConfiguration(policyConfiguration); + this.triggerConfigurationChange(change, previousData, ConfigurationTarget.DEFAULT); + } + + private onUserConfigurationChanged(userConfiguration: ConfigurationModel): void { + const previousData = this._configuration.toData(); + const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); + this.triggerConfigurationChange(change, previousData, ConfigurationTarget.USER); + } + + private onWorkspaceFoldersChanged(e: IWorkspaceFoldersChangeEvent): void { + // Remove configurations for removed folders + const previousData = this._configuration.toData(); + const keys: string[] = []; + const overrides: [string, string[]][] = []; + for (const folder of e.removed) { + const change = this._configuration.compareAndDeleteFolderConfiguration(folder.uri); + keys.push(...change.keys); + overrides.push(...change.overrides); + this.cachedFolderConfigs.deleteAndDispose(folder.uri); + } + if (keys.length || overrides.length) { + this.triggerConfigurationChange({ keys, overrides }, previousData, ConfigurationTarget.WORKSPACE_FOLDER); + } + } + + private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder): void { + const folderConfiguration = this.cachedFolderConfigs.get(folder.uri); + if (folderConfiguration) { + folderConfiguration.loadConfiguration().then(configurationModel => { + const previousData = this._configuration.toData(); + const change = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, configurationModel); + this.triggerConfigurationChange(change, previousData, ConfigurationTarget.WORKSPACE_FOLDER); + }, onUnexpectedError); + } + } + + private async loadFolderConfigurations(folders: readonly IWorkspaceFolder[]): Promise { + for (const folder of folders) { + let folderConfiguration = this.cachedFolderConfigs.get(folder.uri); + if (!folderConfiguration) { + folderConfiguration = new FolderConfiguration(false, folder, FOLDER_CONFIG_FOLDER_NAME, WorkbenchState.WORKSPACE, true, this.fileService, this.uriIdentityService, this.logService, { needsCaching: () => false, read: async () => '', write: async () => { }, remove: async () => { } }); + folderConfiguration.addRelated(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); + this.cachedFolderConfigs.set(folder.uri, folderConfiguration); + } + const configurationModel = await folderConfiguration.loadConfiguration(); + this._configuration.updateFolderConfiguration(folder.uri, configurationModel); + } + } + + private triggerConfigurationChange(change: IConfigurationChange, previousData: IConfigurationData, target: ConfigurationTarget): void { + if (change.keys.length) { + const workspace = this.workspaceService.getWorkspace() as Workspace; + const event = new ConfigurationChangeEvent(change, { data: previousData, workspace }, this._configuration, workspace, this.logService); + event.source = target; + this._onDidChangeConfiguration.fire(event); + } + } + + // #endregion +} + +class ConfigurationEditing { + + private readonly queue = new Queue(); + + constructor( + private readonly fileService: IFileService, + private readonly configurationService: ConfigurationService, + ) { } + + write(settingsResource: URI, path: JSONPath, value: unknown): Promise { + return this.queue.queue(() => this.doWriteConfiguration(settingsResource, path, value)); + } + + private async doWriteConfiguration(settingsResource: URI, path: JSONPath, value: unknown): Promise { + let content: string; + try { + const fileContent = await this.fileService.readFile(settingsResource); + content = fileContent.value.toString(); + } catch (error) { + if ((error as FileOperationError).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + content = '{}'; + } else { + throw error; + } + } + + const parseErrors: ParseError[] = []; + parse(content, parseErrors, { allowTrailingComma: true, allowEmptyContent: true }); + if (parseErrors.length > 0) { + throw new Error('Unable to write into the settings file. Please open the file to correct errors/warnings in the file and try again.'); + } + + const edits = this.getEdits(content, path, value); + content = applyEdits(content, edits); + + await this.fileService.writeFile(settingsResource, VSBuffer.fromString(content)); + } + + private getEdits(content: string, path: JSONPath, value: unknown): Edit[] { + const { tabSize, insertSpaces, eol } = this.formattingOptions; + + if (!path.length) { + const newContent = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t'); + return [{ + content: newContent, + length: content.length, + offset: 0 + }]; + } + + return setProperty(content, path, value, { tabSize, insertSpaces, eol }); + } + + private _formattingOptions: Required | undefined; + private get formattingOptions(): Required { + if (!this._formattingOptions) { + let eol = OS === OperatingSystem.Linux || OS === OperatingSystem.Macintosh ? '\n' : '\r\n'; + const configuredEol = this.configurationService.getValue('files.eol', { overrideIdentifier: 'jsonc' }); + if (configuredEol && typeof configuredEol === 'string' && configuredEol !== 'auto') { + eol = configuredEol; + } + this._formattingOptions = { + eol, + insertSpaces: !!this.configurationService.getValue('editor.insertSpaces', { overrideIdentifier: 'jsonc' }), + tabSize: this.configurationService.getValue('editor.tabSize', { overrideIdentifier: 'jsonc' }) + }; + } + return this._formattingOptions; + } } diff --git a/src/vs/sessions/services/configuration/test/browser/configurationService.test.ts b/src/vs/sessions/services/configuration/test/browser/configurationService.test.ts new file mode 100644 index 00000000000..fdfd8a4f1e9 --- /dev/null +++ b/src/vs/sessions/services/configuration/test/browser/configurationService.test.ts @@ -0,0 +1,339 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../../platform/configuration/common/configurationRegistry.js'; +import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { FileService } from '../../../../../platform/files/common/fileService.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { NullPolicyService } from '../../../../../platform/policy/common/policy.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { UriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentityService.js'; +import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { joinPath } from '../../../../../base/common/resources.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { UserDataProfilesService } from '../../../../../platform/userDataProfile/common/userDataProfile.js'; +import { UserDataProfileService } from '../../../../../workbench/services/userDataProfile/common/userDataProfileService.js'; +import { FileUserDataProvider } from '../../../../../platform/userData/common/fileUserDataProvider.js'; +import { TestEnvironmentService } from '../../../../../workbench/test/browser/workbenchTestServices.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; +import { ConfigurationService } from '../../browser/configurationService.js'; +import { SessionsWorkspaceContextService } from '../../../workspace/browser/workspaceContextService.js'; +import { getWorkspaceIdentifier } from '../../../../../workbench/services/workspaces/browser/workspaces.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IUserDataProfileService } from '../../../../../workbench/services/userDataProfile/common/userDataProfile.js'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + +suite('Sessions ConfigurationService', () => { + + let testObject: ConfigurationService; + let workspaceService: SessionsWorkspaceContextService; + let fileService: FileService; + let userDataProfileService: IUserDataProfileService; + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suiteSetup(() => { + configurationRegistry.registerConfiguration({ + 'id': '_test_sessions', + 'type': 'object', + 'properties': { + 'sessionsConfigurationService.testSetting': { + 'type': 'string', + 'default': 'defaultValue', + scope: ConfigurationScope.RESOURCE + }, + 'sessionsConfigurationService.machineSetting': { + 'type': 'string', + 'default': 'defaultValue', + scope: ConfigurationScope.MACHINE + }, + 'sessionsConfigurationService.applicationSetting': { + 'type': 'string', + 'default': 'defaultValue', + scope: ConfigurationScope.APPLICATION + }, + } + }); + }); + + setup(async () => { + const logService = new NullLogService(); + fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + + const environmentService = TestEnvironmentService; + const uriIdentityService = disposables.add(new UriIdentityService(fileService)); + const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, userDataProfilesService, uriIdentityService, logService)))); + userDataProfileService = disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile)); + + const configResource = joinPath(ROOT, 'agent-sessions.code-workspace'); + await fileService.writeFile(configResource, VSBuffer.fromString(JSON.stringify({ folders: [] }))); + + workspaceService = disposables.add(new SessionsWorkspaceContextService(getWorkspaceIdentifier(configResource), uriIdentityService)); + testObject = disposables.add(new ConfigurationService(userDataProfileService, workspaceService, uriIdentityService, fileService, new NullPolicyService(), logService)); + await testObject.initialize(); + }); + + // #region Reading + + test('defaults', () => { + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.machineSetting'), 'defaultValue'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.applicationSetting'), 'defaultValue'); + }); + + test('user settings override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }')); + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'userValue'); + })); + + test('workspace folder settings override user settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'myFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }')); + await testObject.reloadConfiguration(); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + })); + + test('folder settings are read when folders are added', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'addedFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + })); + + test('folder settings are removed when folders are removed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'removedFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + await workspaceService.removeFolders([folder]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'defaultValue'); + })); + + test('configuration change event is fired when folders with settings are removed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'removedFolder2'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await workspaceService.removeFolders([folder]); + const event = await promise; + assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting')); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'defaultValue'); + })); + + test('configuration change event is fired on user settings change', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }')); + await testObject.reloadConfiguration(); + const event = await promise; + assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting')); + })); + + test('inspect returns correct values per layer', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'inspectFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }')); + await testObject.reloadConfiguration(); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + + const inspection = testObject.inspect('sessionsConfigurationService.testSetting', { resource: folder }); + assert.strictEqual(inspection.defaultValue, 'defaultValue'); + assert.strictEqual(inspection.userValue, 'userValue'); + assert.strictEqual(inspection.workspaceFolderValue, 'folderValue'); + })); + + test('application settings are not read from workspace folder', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'appFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.applicationSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.applicationSetting', { resource: folder }), 'defaultValue'); + })); + + test('machine settings are not read from workspace folder', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'machineFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.machineSetting": "folderValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.machineSetting', { resource: folder }), 'defaultValue'); + })); + + test('folder settings change fires configuration change event', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'changeFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "initialValue" }')); + await workspaceService.addFolders([{ uri: folder }]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'initialValue'); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "updatedValue" }')); + const event = await promise; + assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting')); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'updatedValue'); + })); + + // #endregion + + // #region Writing + + test('updateValue writes to user settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('sessionsConfigurationService.testSetting', 'writtenValue'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'writtenValue'); + })); + + test('updateValue persists to settings file', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('sessionsConfigurationService.testSetting', 'persistedValue'); + + const content = (await fileService.readFile(userDataProfileService.currentProfile.settingsResource)).value.toString(); + assert.ok(content.includes('"sessionsConfigurationService.testSetting"')); + assert.ok(content.includes('persistedValue')); + })); + + test('updateValue fires change event', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await testObject.updateValue('sessionsConfigurationService.testSetting', 'eventValue'); + const event = await promise; + assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting')); + })); + + test('updateValue removes setting when value equals default', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('sessionsConfigurationService.testSetting', 'nonDefault'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'nonDefault'); + + await testObject.updateValue('sessionsConfigurationService.testSetting', 'defaultValue'); + const content = (await fileService.readFile(userDataProfileService.currentProfile.settingsResource)).value.toString(); + assert.ok(!content.includes('sessionsConfigurationService.testSetting')); + })); + + test('updateValue can update multiple settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('sessionsConfigurationService.testSetting', 'value1'); + await testObject.updateValue('sessionsConfigurationService.machineSetting', 'value2'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'value1'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.machineSetting'), 'value2'); + })); + + test('updateValue with language override', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('sessionsConfigurationService.testSetting', 'langValue', { overrideIdentifier: 'jsonc' }); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { overrideIdentifier: 'jsonc' }), 'langValue'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue'); + })); + + test('updateValue is reflected in inspect', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('sessionsConfigurationService.testSetting', 'inspectedValue'); + const inspection = testObject.inspect('sessionsConfigurationService.testSetting'); + assert.strictEqual(inspection.defaultValue, 'defaultValue'); + assert.strictEqual(inspection.userValue, 'inspectedValue'); + })); + + // #endregion + + // #region Workspace Folder - Read and Write + + test('read setting from workspace folder', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'readFolder'); + await fileService.createFolder(folder); + await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }')); + + await workspaceService.addFolders([{ uri: folder }]); + + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue'); + })); + + test('write setting to workspace folder', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'writeFolder'); + await fileService.createFolder(folder); + + await workspaceService.addFolders([{ uri: folder }]); + + await testObject.updateValue('sessionsConfigurationService.testSetting', 'writtenFolderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER); + + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'writtenFolderValue'); + })); + + test('write setting to workspace folder persists to folder settings file', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'persistFolder'); + await fileService.createFolder(folder); + + await workspaceService.addFolders([{ uri: folder }]); + + await testObject.updateValue('sessionsConfigurationService.testSetting', 'persistedFolderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER); + + const content = (await fileService.readFile(joinPath(folder, '.vscode', 'settings.json'))).value.toString(); + assert.ok(content.includes('"sessionsConfigurationService.testSetting"')); + assert.ok(content.includes('persistedFolderValue')); + })); + + test('write setting to workspace folder does not affect user settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'isolateFolder'); + await fileService.createFolder(folder); + + await workspaceService.addFolders([{ uri: folder }]); + + await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderOnly', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER); + + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderOnly'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue'); + })); + + test('workspace folder setting overrides user setting for resource', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'overrideFolder'); + await fileService.createFolder(folder); + + await workspaceService.addFolders([{ uri: folder }]); + + await testObject.updateValue('sessionsConfigurationService.testSetting', 'userValue'); + await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER); + + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'userValue'); + })); + + test('inspect shows workspace folder value after write', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'inspectWriteFolder'); + await fileService.createFolder(folder); + + await workspaceService.addFolders([{ uri: folder }]); + + await testObject.updateValue('sessionsConfigurationService.testSetting', 'userVal'); + await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderVal', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER); + + const inspection = testObject.inspect('sessionsConfigurationService.testSetting', { resource: folder }); + assert.strictEqual(inspection.defaultValue, 'defaultValue'); + assert.strictEqual(inspection.userValue, 'userVal'); + assert.strictEqual(inspection.workspaceFolderValue, 'folderVal'); + })); + + test('removing folder clears its written settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const folder = joinPath(ROOT, 'clearFolder'); + await fileService.createFolder(folder); + + await workspaceService.addFolders([{ uri: folder }]); + await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue'); + + await workspaceService.removeFolders([folder]); + assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'defaultValue'); + })); + + // #endregion +}); diff --git a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts index 9d8b6763dab..dce935e3f24 100644 --- a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts +++ b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts @@ -35,7 +35,6 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork constructor( workspaceIdentifier: IWorkspaceIdentifier, private readonly uriIdentityService: IUriIdentityService, - private readonly configurationService: IConfigurationService, ) { super(); this.workspace = new Workspace(workspaceIdentifier.id, [], false, workspaceIdentifier.configPath, uri => uriIdentityService.extUri.ignorePathCasing(uri)); @@ -53,8 +52,13 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork return WorkbenchState.WORKSPACE; } + private _configurationService: IConfigurationService | undefined; + setConfigurationService(configurationService: IConfigurationService) { + this._configurationService = configurationService; + } + hasWorkspaceData(): boolean { - return this.configurationService.getValue('sessions.workspace.sendWorkspaceDataToExtHost') === true; + return this._configurationService?.getValue('sessions.workspace.sendWorkspaceDataToExtHost') === true; } getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { @@ -159,7 +163,8 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork // Update workspace const workspaceIdentifier = getWorkspaceIdentifier(this.workspace.configuration!); - this.workspace = new Workspace(workspaceIdentifier.id, newFolders, false, workspaceIdentifier.configPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri)); + const workspace = new Workspace(workspaceIdentifier.id, newFolders, false, workspaceIdentifier.configPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri)); + this.workspace.update(workspace); // Fire did change event this._onDidChangeWorkspaceFolders.fire(changes); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 8c40f80dbe2..e33e0d1413d 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -363,7 +363,7 @@ export class SettingsTargetsWidget extends Widget { private async update(): Promise { this.settingsSwitcherBar.domNode.classList.toggle('empty-workbench', this.contextService.getWorkbenchState() === WorkbenchState.EMPTY); this.userRemoteSettings.enabled = !!(this.options.enableRemoteSettings && this.environmentService.remoteAuthority); - this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; + this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && !this.environmentService.isSessionsWindow; this.folderSettings.action.enabled = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.contextService.getWorkspace().folders.length > 0; this.workspaceSettings.tooltip = localize('workspaceSettings', "Workspace"); From 59cb786bc1da620df8b896ab3726acc6dae003b6 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:07:52 -0800 Subject: [PATCH 221/448] Copy over .sh scripts in git extension too Fixes #299332 Restoring previous webpack behavior. In the future let's consider just moving these to the `git/scripts` folder so we don't have to copy them around --- extensions/git/esbuild.mts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/extensions/git/esbuild.mts b/extensions/git/esbuild.mts index 35c8f6c63f0..1b397880bc6 100644 --- a/extensions/git/esbuild.mts +++ b/extensions/git/esbuild.mts @@ -2,12 +2,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); +async function copyNonTsFiles(outDir: string): Promise { + const entries = await fs.readdir(srcDir, { withFileTypes: true, recursive: true }); + for (const entry of entries) { + if (!entry.isFile() || entry.name.endsWith('.ts')) { + continue; + } + const srcPath = path.join(entry.parentPath, entry.name); + const relativePath = path.relative(srcDir, srcPath); + const destPath = path.join(outDir, relativePath); + await fs.mkdir(path.dirname(destPath), { recursive: true }); + await fs.copyFile(srcPath, destPath); + } +} + run({ platform: 'node', entryPoints: { @@ -17,4 +32,4 @@ run({ }, srcDir, outdir: outDir, -}, process.argv); +}, process.argv, copyNonTsFiles); From d4e6af83906d9c4acef3faaf860bf7b05d4969b2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 07:09:39 +0000 Subject: [PATCH 222/448] Default to recursively searching for *.instructions.md in .github/instructions to match CLI & Web experiences (#298973) * Initial plan * feat: search recursively in .github/instructions for *.instructions.md files Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * refactor: limit recursive instructions traversal to default source folders only Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * refactor: limit recursive instructions traversal to non-root, wildcard-free folders with max depth 5 Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> * fix: use isEqual from resources.ts for URI comparison in resolveFilesAtLocation Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> Co-authored-by: Martin Aeschlimann --- .../promptSyntax/utils/promptFilesLocator.ts | 30 ++++++--- .../utils/promptFilesLocator.test.ts | 63 +++++++++++++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 5ee6297bb31..c0675b37659 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -9,10 +9,10 @@ import { ResourceSet } from '../../../../../../base/common/map.js'; import * as nls from '../../../../../../nls.js'; import { FileOperationError, FileOperationResult, IFileService } from '../../../../../../platform/files/common/files.js'; import { getPromptFileLocationsConfigKey, isTildePath, PromptsConfig } from '../config/config.js'; -import { basename, dirname, isEqualOrParent, joinPath } from '../../../../../../base/common/resources.js'; +import { basename, dirname, isEqual, isEqualOrParent, joinPath } from '../../../../../../base/common/resources.js'; import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource, isInClaudeRulesFolder } from '../config/promptFileLocations.js'; +import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; import { PromptsType } from '../promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -26,6 +26,11 @@ import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; +/** + * Maximum recursion depth when traversing subdirectories for instruction files. + */ +const MAX_INSTRUCTIONS_RECURSION_DEPTH = 5; + /** * Utility class to locate prompt files. */ @@ -492,14 +497,23 @@ export class PromptFilesLocator { /** * Uses the file service to resolve the provided location and return either the file at the location of files in the directory. - * For claude rules folders (.claude/rules), this searches recursively to support subdirectories. + * For instruction folders, this searches recursively (up to {@link MAX_INSTRUCTIONS_RECURSION_DEPTH} levels deep) provided + * the location is not a workspace folder root and does not contain wildcards, to support subdirectories while avoiding + * accidentally broad traversal. */ - private async resolveFilesAtLocation(location: URI, type: PromptsType, token: CancellationToken): Promise { + private async resolveFilesAtLocation(location: URI, type: PromptsType, token: CancellationToken, depth: number = 0): Promise { if (type === PromptsType.skill) { return this.findAgentSkillsInFolder(location, token); } - // Claude rules folders support subdirectories, so search recursively - const recursive = type === PromptsType.instructions && isInClaudeRulesFolder(joinPath(location, 'dummy.md')); + // Recurse into subdirectories for instruction folders, but only if: + // - the location is not a workspace folder root (to avoid full workspace traversal) + // - the path does not contain wildcards (already filtered upstream, but guard here too) + // - the recursion depth hasn't exceeded the limit + const isWorkspaceRoot = depth === 0 && this.getWorkspaceFolders().some(f => isEqual(f.uri, location)); + const recursive = type === PromptsType.instructions + && !isWorkspaceRoot + && !hasGlobPattern(location.path) + && depth < MAX_INSTRUCTIONS_RECURSION_DEPTH; try { const info = await this.fileService.resolve(location); if (token.isCancellationRequested) { @@ -513,8 +527,8 @@ export class PromptFilesLocator { if (child.isFile) { result.push(child.resource); } else if (recursive && child.isDirectory) { - // Recursively search subdirectories for claude rules - const subFiles = await this.resolveFilesAtLocation(child.resource, type, token); + // Recursively search subdirectories for instructions + const subFiles = await this.resolveFilesAtLocation(child.resource, type, token, depth + 1); result.push(...subFiles); } } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 5d9664f7907..2dde1dfb0a2 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -2366,6 +2366,69 @@ suite('PromptFilesLocator', () => { }); }); + suite('instructions', () => { + testT('finds instructions files in subdirectories of .github/instructions', async () => { + const locator = await createPromptsLocator( + { + '.github/instructions': true, + '.claude/rules': false, + '~/.copilot/instructions': false, + }, + ['/Users/legomushroom/repos/vscode'], + [ + { + name: '/Users/legomushroom/repos/vscode', + children: [ + { + name: '.github/instructions', + children: [ + { + name: 'root.instructions.md', + contents: 'root instructions', + }, + { + name: 'frontend', + children: [ + { + name: 'react.instructions.md', + contents: 'react instructions', + }, + { + name: 'css.instructions.md', + contents: 'css instructions', + }, + ], + }, + { + name: 'backend', + children: [ + { + name: 'api.instructions.md', + contents: 'api instructions', + }, + ], + }, + ], + }, + ], + }, + ], + ); + + assertOutcome( + await locator.listFiles(PromptsType.instructions, PromptsStorage.local, CancellationToken.None), + [ + '/Users/legomushroom/repos/vscode/.github/instructions/root.instructions.md', + '/Users/legomushroom/repos/vscode/.github/instructions/frontend/react.instructions.md', + '/Users/legomushroom/repos/vscode/.github/instructions/frontend/css.instructions.md', + '/Users/legomushroom/repos/vscode/.github/instructions/backend/api.instructions.md', + ], + 'Must find instructions files recursively in subdirectories of .github/instructions.', + ); + await locator.disposeAsync(); + }); + }); + suite('skills', () => { suite('findAgentSkills', () => { testT('finds skill files in configured locations', async () => { From a482aa047da3e0a85e2d9a350da0eb76dc689d54 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 5 Mar 2026 08:13:06 +0100 Subject: [PATCH 223/448] Revert "sessions - allow to open preview from markdown files" (#299392) Revert "sessions - allow to open preview from markdown files (#299047)" This reverts commit a7f87d92f9ee0ecd38ee866bf0e884ddbf8cb2d6. --- .../browser/markdownPreview.contribution.ts | 24 ------------------- src/vs/sessions/sessions.desktop.main.ts | 1 - 2 files changed, 25 deletions(-) delete mode 100644 src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts diff --git a/src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts b/src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts deleted file mode 100644 index f186d71637d..00000000000 --- a/src/vs/sessions/contrib/markdownPreview/browser/markdownPreview.contribution.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../nls.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; - -// Show a floating "Open Preview" button in the editor content -// area when editing markdown or related prompt/instructions/chatagent/skill -// language content in the sessions window. -MenuRegistry.appendMenuItem(MenuId.EditorContent, { - command: { - id: 'markdown.showPreviewToSide', - title: localize('openPreview', "Open Preview"), - }, - when: ContextKeyExpr.and( - IsSessionsWindowContext, - ContextKeyExpr.regex(EditorContextKeys.languageId.key, /^(markdown|prompt|instructions|chatagent|skill)$/), - ), -}); diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 9ede9e80baa..efe6d190c0d 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -211,7 +211,6 @@ import './contrib/gitSync/browser/gitSync.contribution.js'; import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; -import './contrib/markdownPreview/browser/markdownPreview.contribution.js'; import './contrib/terminal/browser/sessionsTerminalContribution.js'; import './contrib/logs/browser/logs.contribution.js'; From 08535d9c5ef4a8b9f89a49e8ac53d98000cda9df Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:13:55 -0800 Subject: [PATCH 224/448] Fix terminal-suggest extension icon Same root cause as #299396 but only caused the icon to be missing so not critical --- .../terminal-suggest/{src => }/media/icon.png | Bin extensions/terminal-suggest/package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename extensions/terminal-suggest/{src => }/media/icon.png (100%) diff --git a/extensions/terminal-suggest/src/media/icon.png b/extensions/terminal-suggest/media/icon.png similarity index 100% rename from extensions/terminal-suggest/src/media/icon.png rename to extensions/terminal-suggest/media/icon.png diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 734e3e91c82..5eea60ebf7b 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -6,7 +6,7 @@ "version": "1.0.1", "private": true, "license": "MIT", - "icon": "./src/media/icon.png", + "icon": "./media/icon.png", "engines": { "vscode": "^1.95.0" }, From 6ae33b0d2896857795f6c418d3e720d4c5b5fbb9 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 5 Mar 2026 11:10:37 +0000 Subject: [PATCH 225/448] update selection background colors in 2026 dark and light themes for better visibility --- extensions/theme-2026/themes/2026-dark.json | 2 +- extensions/theme-2026/themes/2026-light.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index a346ebba78f..46367385850 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -110,7 +110,7 @@ "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#BBBEBF", "editorCursor.foreground": "#BBBEBF", - "editor.selectionBackground": "#27678280", + "editor.selectionBackground": "#276782dd", "editor.inactiveSelectionBackground": "#27678260", "editor.selectionHighlightBackground": "#27678260", "editor.wordHighlightBackground": "#27678250", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index e9db9b37656..a03d296a0c6 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -116,7 +116,7 @@ "editorLineNumber.foreground": "#606060", "editorLineNumber.activeForeground": "#202020", "editorCursor.foreground": "#202020", - "editor.selectionBackground": "#0069CC1A", + "editor.selectionBackground": "#0069CC40", "editor.inactiveSelectionBackground": "#0069CC1A", "editor.selectionHighlightBackground": "#0069CC15", "editor.wordHighlightBackground": "#0069CC26", From 79fbc6b37856c68726f88c5580fd1a37c551b70c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 5 Mar 2026 12:18:49 +0100 Subject: [PATCH 226/448] Sessions: Show repo name with worktree basename in workspace folder label (#299448) Show repo name with worktree basename in workspace folder label Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/workspace/browser/workspaceFolderManagement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts index 3bdba7d96a6..1bd80b586c7 100644 --- a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts +++ b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts @@ -68,7 +68,7 @@ export class WorkspaceFolderManagementContribution extends Disposable implements if (session.worktree) { return { uri: session.worktree, - name: session.repository ? `${this.uriIdentityService.extUri.basename(session.repository)} (worktree)` : undefined + name: session.repository ? `${this.uriIdentityService.extUri.basename(session.repository)} (${this.uriIdentityService.extUri.basename(session.worktree)})` : this.uriIdentityService.extUri.basename(session.worktree) }; } From 91c1a5a8bfea17b043898a03f626f1b8d74b7686 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 5 Mar 2026 12:32:04 +0100 Subject: [PATCH 227/448] sessions - hide "Open as Editor" and "Open to the Side" in agent sessions window (#299428) * chore - clean up `mcp.json` and update imports in `agentSessionsActions.ts` * chore - update `mcp.json` with server configurations * Disable "Open as Editor" / "Open to the Side" keybindings in Sessions Window (#299430) * Initial plan * fix: disable keybindings for Open as Editor/Open to the Side in Sessions Window Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --- .../chat/browser/agentSessions/agentSessionsActions.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 3150acc6c23..950fea0381e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -28,7 +28,7 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { AgentSessionsPicker } from './agentSessionsPicker.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; @@ -708,10 +708,11 @@ export class OpenAgentSessionInEditorGroupAction extends BaseOpenAgentSessionAct primary: KeyMod.WinCtrl | KeyCode.Enter }, weight: KeybindingWeight.WorkbenchContrib + 1, - when: ChatContextKeys.agentSessionsViewerFocused, + when: ContextKeyExpr.and(ChatContextKeys.agentSessionsViewerFocused, IsSessionsWindowContext.negate()), }, menu: { id: MenuId.AgentSessionsContext, + when: IsSessionsWindowContext.negate(), order: 1, group: 'navigation' } @@ -741,10 +742,11 @@ export class OpenAgentSessionInNewEditorGroupAction extends BaseOpenAgentSession primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter }, weight: KeybindingWeight.WorkbenchContrib + 1, - when: ChatContextKeys.agentSessionsViewerFocused, + when: ContextKeyExpr.and(ChatContextKeys.agentSessionsViewerFocused, IsSessionsWindowContext.negate()), }, menu: { id: MenuId.AgentSessionsContext, + when: IsSessionsWindowContext.negate(), order: 2, group: 'navigation' } From 3488ba472aa2d02800fabec1a5ed521ba15d214b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 5 Mar 2026 12:32:25 +0100 Subject: [PATCH 228/448] sessions - allow all workbench commands when `useModal` is `all` (#299431) * refactor - remove unused `IWorkbenchEnvironmentService` * [WIP] Address feedback on modal editor workbench commands (#299450) * Initial plan * Cache useModal config value to avoid repeated lookups on keydown Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --- .../browser/parts/editor/modalEditorPart.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 13c86decaf3..1f472ed3dd8 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -26,7 +26,6 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { EditorPartModalContext, EditorPartModalMaximizedContext, EditorPartModalNavigationContext } from '../../../common/contextkeys.js'; import { EditorResourceAccessor, SideBySideEditor, Verbosity } from '../../../common/editor.js'; import { ResourceLabel } from '../../labels.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { mainWindow } from '../../../../base/browser/window.js'; @@ -65,7 +64,7 @@ export class ModalEditorPart { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IHostService private readonly hostService: IHostService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -86,11 +85,18 @@ export class ModalEditorPart { } })); + let useModalMode = this.configurationService.getValue('workbench.editor.useModal'); + disposables.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('workbench.editor.useModal')) { + useModalMode = this.configurationService.getValue('workbench.editor.useModal'); + } + })); + disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); - // Prevent unsupported commands (not in sessions windows) - if (!this.environmentService.isSessionsWindow) { + // Prevent unsupported commands unless all editors open in modal + if (useModalMode !== 'all') { const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer); if (resolved.kind === ResultKind.KbFound && resolved.commandId) { if ( From a29d3c0f6fdc4f4069ea29f6e7753e2bd10dedc2 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 5 Mar 2026 11:37:45 +0000 Subject: [PATCH 229/448] Fix disabledForeground color in 2026 Dark theme --- extensions/theme-2026/themes/2026-dark.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index a346ebba78f..c162565f810 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -5,7 +5,7 @@ "type": "dark", "colors": { "foreground": "#bfbfbf", - "disabledForeground": "#666666", + "disabledForeground": "#555555", "errorForeground": "#f48771", "descriptionForeground": "#8C8C8C", "icon.foreground": "#8C8C8C", From 4e443a1c312dce1338a5548c91e4f03c6cc0a9dd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 5 Mar 2026 12:43:31 +0100 Subject: [PATCH 230/448] sessions - show folder icon instead of worktree when session has no worktree (#299456) feat - distinguish worktree vs folder in session icon --- .../contrib/sessions/browser/sessionsTitleBarWidget.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index f15bed0dc94..08ae2d0a1e0 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -32,7 +32,8 @@ import { AgentSessionsPicker } from '../../../../workbench/contrib/chat/browser/ import { autorun } from '../../../../base/common/observable.js'; import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { getAgentSessionProvider, getAgentSessionProviderIcon } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { basename } from '../../../../base/common/resources.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; @@ -290,6 +291,12 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { // Try to get icon from the agent session model (has provider-resolved icon) const agentSession = this.agentSessionsService.getSession(activeSession.resource); if (agentSession) { + // For background sessions, distinguish worktree vs folder based on metadata + if (agentSession.providerType === AgentSessionProviders.Background) { + const hasWorktree = typeof agentSession.metadata?.worktreePath === 'string'; + return hasWorktree ? Codicon.worktree : Codicon.folder; + } + return agentSession.icon; } From c35a1a5fb62a9f2398c5cb575d86895318ba4172 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:16:40 +0000 Subject: [PATCH 231/448] Fix SCM input widget ignoring `editor.roundedSelection` setting (#299398) * Initial plan * fix: SCM input widget respects editor.roundedSelection setting Co-authored-by: lszomoru <3372902+lszomoru@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lszomoru <3372902+lszomoru@users.noreply.github.com> --- src/vs/workbench/contrib/scm/browser/scmInput.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmInput.ts b/src/vs/workbench/contrib/scm/browser/scmInput.ts index bcc312c6ba2..a35d479e1a8 100644 --- a/src/vs/workbench/contrib/scm/browser/scmInput.ts +++ b/src/vs/workbench/contrib/scm/browser/scmInput.ts @@ -261,6 +261,7 @@ class SCMInputWidgetEditorOptions { e.affectsConfiguration('editor.cursorWidth') || e.affectsConfiguration('editor.emptySelectionClipboard') || e.affectsConfiguration('editor.fontFamily') || + e.affectsConfiguration('editor.roundedSelection') || e.affectsConfiguration('editor.rulers') || e.affectsConfiguration('editor.wordWrap') || e.affectsConfiguration('editor.wordSegmenterLocales') || @@ -304,8 +305,9 @@ class SCMInputWidgetEditorOptions { const cursorStyle = this.configurationService.getValue('editor.cursorStyle'); const cursorWidth = this.configurationService.getValue('editor.cursorWidth') ?? 1; const emptySelectionClipboard = this.configurationService.getValue('editor.emptySelectionClipboard') === true; + const roundedSelection = this.configurationService.getValue('editor.roundedSelection') === true; - return { ...this._getEditorLanguageConfiguration(), accessibilitySupport, cursorBlinking, cursorStyle, cursorWidth, fontFamily, fontSize, lineHeight, emptySelectionClipboard, wordSegmenterLocales }; + return { ...this._getEditorLanguageConfiguration(), accessibilitySupport, cursorBlinking, cursorStyle, cursorWidth, fontFamily, fontSize, lineHeight, emptySelectionClipboard, roundedSelection, wordSegmenterLocales }; } private _getEditorFontFamily(): string { From c08bdfcb1317ce0123588140a683f46b6c828322 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Mar 2026 23:32:59 +1100 Subject: [PATCH 232/448] feat: add optional id field to ChatRequestModeInstructions interface (#299128) * feat: add optional id field to ChatRequestModeInstructions interface * feat: surface uri instead of id in ChatRequestModeInstructions (#299166) * Initial plan * feat: surface uri instead of id in ChatRequestModeInstructions Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: aeschli <6461412+aeschli@users.noreply.github.com> --- src/vs/workbench/api/common/extHostTypeConverters.ts | 1 + .../contrib/chat/browser/widget/input/chatInputPart.ts | 1 + src/vs/workbench/contrib/chat/common/model/chatModel.ts | 1 + src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts | 2 ++ 4 files changed, 5 insertions(+) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9c5adf7bc0d..78b0c904947 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3607,6 +3607,7 @@ export namespace ChatRequestModeInstructions { export function to(mode: IChatRequestModeInstructions | undefined): vscode.ChatRequestModeInstructions | undefined { if (mode) { return { + uri: URI.revive(mode.uri), name: mode.name, content: mode.content, toolReferences: ChatLanguageModelToolReferences.to(mode.toolReferences), diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 45f4a15d648..985ff7fd77a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -436,6 +436,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge kind: this.currentModeKind, isBuiltin: mode.isBuiltin, modeInstructions: modeInstructions ? { + uri: mode.uri?.get(), name: mode.name.get(), content: modeInstructions.content, toolReferences: this.toolService.toToolReferences(modeInstructions.toolReferences), diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 98098bb9376..a08574a75c8 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -318,6 +318,7 @@ export interface IChatRequestModeInfo { } export interface IChatRequestModeInstructions { + readonly uri?: URI; readonly name: string; readonly content: string; readonly toolReferences: readonly ChatRequestToolReferenceEntry[]; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f78e56bed5a..9143c72c08d 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -1059,6 +1059,8 @@ declare module 'vscode' { } export interface ChatRequestModeInstructions { + /** set when the mode a custom agent (not built-in), to be used as identifier */ + readonly uri?: Uri; readonly name: string; readonly content: string; readonly toolReferences?: readonly ChatLanguageModelToolReference[]; From 5b18dda8fe3696077918944a6c8e1e9dfeedae73 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 5 Mar 2026 12:42:20 +0000 Subject: [PATCH 233/448] refactor(chat): update structure and styling for context usage widget Co-authored-by: Copilot --- .../viewPane/chatContextUsageDetails.ts | 35 ++++--- .../media/chatContextUsageDetails.css | 93 +++++++++---------- 2 files changed, 60 insertions(+), 68 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 43fc982f081..1988ece0aa7 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -52,20 +52,19 @@ export class ChatContextUsageDetails extends Disposable { this.domNode = $('.chat-context-usage-details'); - // Using same structure as ChatUsageWidget quota items - this.quotaItem = this.domNode.appendChild($('.quota-item')); + // Quota indicator — using same structure as ChatStatusDashboard + this.quotaItem = this.domNode.appendChild($('.quota-indicator')); - // Header row with label - const quotaItemHeader = this.quotaItem.appendChild($('.quota-item-header')); - const quotaItemLabel = quotaItemHeader.appendChild($('.quota-item-label')); - quotaItemLabel.textContent = localize('contextWindow', "Context Window"); + // Header row + const header = this.domNode.insertBefore($('div.header'), this.quotaItem); + header.textContent = localize('contextWindow', "Context Window"); - // Token count and percentage row (on same line) - const tokenRow = this.quotaItem.appendChild($('.token-row')); - this.tokenCountLabel = tokenRow.appendChild($('.token-count-label')); - this.percentageLabel = tokenRow.appendChild($('.quota-item-value')); + // Quota label row with token count + percentage + const quotaLabel = this.quotaItem.appendChild($('.quota-label')); + this.tokenCountLabel = quotaLabel.appendChild($('span')); + this.percentageLabel = quotaLabel.appendChild($('span.quota-value')); - // Progress bar - using same structure as chat usage widget + // Progress bar const progressBar = this.quotaItem.appendChild($('.quota-bar')); this.progressFill = progressBar.appendChild($('.quota-bit')); @@ -73,15 +72,15 @@ export class ChatContextUsageDetails extends Disposable { this.tokenDetailsContainer = this.domNode.appendChild($('.token-details-container')); // Warning message (shown when usage is high) - this.warningMessage = this.domNode.appendChild($('.warning-message')); + this.warningMessage = this.domNode.appendChild($('div.description')); this.warningMessage.textContent = localize('qualityWarning', "Quality may decline as limit nears."); this.warningMessage.style.display = 'none'; - // Actions section with header, separator, and button bar + // Actions section with hr, header, and button bar this.actionsSection = this.domNode.appendChild($('.actions-section')); - this.actionsSection.appendChild($('.separator')); - const actionsHeader = this.actionsSection.appendChild($('.actions-header')); - actionsHeader.textContent = localize('actions', "Actions"); + // this.actionsSection.appendChild($('hr')); + // const actionsHeader = this.actionsSection.appendChild($('div.header')); + // actionsHeader.textContent = localize('actions', "Actions"); const buttonBarContainer = this.actionsSection.appendChild($('.button-bar-container')); this._register(this.instantiationService.createInstance(MenuWorkbenchButtonBar, buttonBarContainer, MenuId.ChatContextUsageActions, { toolbarOptions: { @@ -104,14 +103,14 @@ export class ChatContextUsageDetails extends Disposable { update(data: IChatContextUsageData): void { const { percentage, usedTokens, totalContextWindow, promptTokenDetails } = data; - // Update token count and percentage on same line + // Update token count and percentage this.tokenCountLabel.textContent = localize( 'tokenCount', "{0} / {1} tokens", this.formatTokenCount(usedTokens, 1), this.formatTokenCount(totalContextWindow, 0) ); - this.percentageLabel.textContent = `• ${percentage.toFixed(0)}%`; + this.percentageLabel.textContent = `${percentage.toFixed(0)}%`; // Update progress bar this.progressFill.style.width = `${Math.min(100, percentage)}%`; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css index 41b7a7ad9d1..fa9e68ee669 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css @@ -15,7 +15,8 @@ .chat-context-usage-details { display: flex; flex-direction: column; - padding: 4px 0; + margin-top: 4px; + margin-bottom: 4px; min-width: 200px; } @@ -23,97 +24,95 @@ outline: none; } -/* Using same structure as ChatUsageWidget quota items */ -.chat-context-usage-details .quota-item { +/* Section headers — matching ChatStatusDashboard */ +.chat-context-usage-details div.header { + display: flex; + align-items: center; + color: var(--vscode-descriptionForeground); margin-bottom: 4px; + font-weight: 600; } -.chat-context-usage-details .quota-item-header { +/* Separator */ +.chat-context-usage-details hr { + margin-top: 8px; + margin-bottom: 8px; +} + +/* Quota indicator — matching ChatStatusDashboard */ +.chat-context-usage-details .quota-indicator .quota-label { display: flex; - align-items: center; justify-content: space-between; - margin-bottom: 2px; + gap: 20px; + margin-bottom: 3px; } -.chat-context-usage-details .quota-item-label { - color: var(--vscode-foreground); -} - -.chat-context-usage-details .quota-item-value { +.chat-context-usage-details .quota-indicator .quota-label .quota-value { color: var(--vscode-descriptionForeground); } -.chat-context-usage-details .token-row { - display: flex; - align-items: center; - gap: 4px; - margin-bottom: 2px; -} - -.chat-context-usage-details .token-count-label { - font-size: 12px; - color: var(--vscode-descriptionForeground); -} - -/* Progress bar - matching chat usage implementation */ -.chat-context-usage-details .quota-item .quota-bar { +.chat-context-usage-details .quota-indicator .quota-bar { width: 100%; height: 4px; background-color: var(--vscode-gauge-background); border-radius: 4px; border: 1px solid var(--vscode-gauge-border); - margin: 2px 0; + margin: 4px 0; } -.chat-context-usage-details .quota-item .quota-bar .quota-bit { +.chat-context-usage-details .quota-indicator .quota-bar .quota-bit { height: 100%; background-color: var(--vscode-gauge-foreground); border-radius: 4px; transition: width 0.3s ease; } -.chat-context-usage-details .quota-item.warning .quota-bar { +.chat-context-usage-details .quota-indicator.warning .quota-bar { background-color: var(--vscode-gauge-warningBackground); } -.chat-context-usage-details .quota-item.warning .quota-bar .quota-bit { +.chat-context-usage-details .quota-indicator.warning .quota-bar .quota-bit { background-color: var(--vscode-gauge-warningForeground); } -.chat-context-usage-details .quota-item.error .quota-bar { +.chat-context-usage-details .quota-indicator.error .quota-bar { background-color: var(--vscode-gauge-errorBackground); } -.chat-context-usage-details .quota-item.error .quota-bar .quota-bit { +.chat-context-usage-details .quota-indicator.error .quota-bar .quota-bit { background-color: var(--vscode-gauge-errorForeground); } -.chat-context-usage-details .warning-message { - font-size: 12px; +/* Description / warning text — matching ChatStatusDashboard */ +.chat-context-usage-details div.description { + font-size: 11px; color: var(--vscode-descriptionForeground); - margin-bottom: 4px; + display: flex; + align-items: center; + gap: 3px; } /* Token details breakdown */ -.chat-context-usage-details .token-details-container { - margin-top: 4px; -} .chat-context-usage-details .token-category { - margin-bottom: 4px; + margin-bottom: 6px; } .chat-context-usage-details .token-category-header { + display: flex; + align-items: center; + color: var(--vscode-descriptionForeground); + margin-top: 16px; + margin-bottom: 4px; font-weight: 600; - color: var(--vscode-foreground); - margin-bottom: 2px; } .chat-context-usage-details .token-detail-item { display: flex; justify-content: space-between; align-items: center; - padding-left: 8px; + gap: 20px; + margin-bottom: 2px; } .chat-context-usage-details .token-detail-label { @@ -124,15 +123,9 @@ color: var(--vscode-descriptionForeground); } -.chat-context-usage-details .actions-section .separator { - border-top: 1px solid var(--vscode-editorHoverWidget-border); - margin: 4px 0; -} - -.chat-context-usage-details .actions-section .actions-header { - font-weight: 600; - color: var(--vscode-foreground); - margin-bottom: 4px; +/* Actions section */ +.chat-context-usage-details .actions-section { + margin-top: 8px; } .chat-context-usage-details .actions-section .button-bar-container { From 5ffa330178056a763500d670e9149825639ddb1f Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 5 Mar 2026 12:45:29 +0000 Subject: [PATCH 234/448] refactor(chat): remove unused actions section and clean up CSS --- .../browser/widgetHosts/viewPane/chatContextUsageDetails.ts | 3 --- .../widgetHosts/viewPane/media/chatContextUsageDetails.css | 1 - 2 files changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 1988ece0aa7..30f8019d863 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -78,9 +78,6 @@ export class ChatContextUsageDetails extends Disposable { // Actions section with hr, header, and button bar this.actionsSection = this.domNode.appendChild($('.actions-section')); - // this.actionsSection.appendChild($('hr')); - // const actionsHeader = this.actionsSection.appendChild($('div.header')); - // actionsHeader.textContent = localize('actions', "Actions"); const buttonBarContainer = this.actionsSection.appendChild($('.button-bar-container')); this._register(this.instantiationService.createInstance(MenuWorkbenchButtonBar, buttonBarContainer, MenuId.ChatContextUsageActions, { toolbarOptions: { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css index fa9e68ee669..672e2e8d23f 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css @@ -93,7 +93,6 @@ } /* Token details breakdown */ - .chat-context-usage-details .token-category { margin-bottom: 6px; } From 5a8fb3c212bf07c898c00d8fa6330c0ee2201c3a Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 5 Mar 2026 12:51:04 +0000 Subject: [PATCH 235/448] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../widgetHosts/viewPane/media/chatContextUsageDetails.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css index 672e2e8d23f..21dd9bcb7d4 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css @@ -33,12 +33,6 @@ font-weight: 600; } -/* Separator */ -.chat-context-usage-details hr { - margin-top: 8px; - margin-bottom: 8px; -} - /* Quota indicator — matching ChatStatusDashboard */ .chat-context-usage-details .quota-indicator .quota-label { display: flex; From f957df8e89ac13e9868d0ab0aebd111406423222 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 5 Mar 2026 12:51:58 +0000 Subject: [PATCH 236/448] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/widgetHosts/viewPane/chatContextUsageDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 30f8019d863..6df94655eb9 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -107,7 +107,7 @@ export class ChatContextUsageDetails extends Disposable { this.formatTokenCount(usedTokens, 1), this.formatTokenCount(totalContextWindow, 0) ); - this.percentageLabel.textContent = `${percentage.toFixed(0)}%`; + this.percentageLabel.textContent = localize('quotaDisplay', "{0}%", percentage.toFixed(0)); // Update progress bar this.progressFill.style.width = `${Math.min(100, percentage)}%`; From 32acff1aa2400a8cb1d6d3a72dae421671fd89fe Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 5 Mar 2026 12:52:10 +0000 Subject: [PATCH 237/448] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/widgetHosts/viewPane/chatContextUsageDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 6df94655eb9..9c63561bb60 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -76,7 +76,7 @@ export class ChatContextUsageDetails extends Disposable { this.warningMessage.textContent = localize('qualityWarning', "Quality may decline as limit nears."); this.warningMessage.style.display = 'none'; - // Actions section with hr, header, and button bar + // Actions section with button bar this.actionsSection = this.domNode.appendChild($('.actions-section')); const buttonBarContainer = this.actionsSection.appendChild($('.button-bar-container')); this._register(this.instantiationService.createInstance(MenuWorkbenchButtonBar, buttonBarContainer, MenuId.ChatContextUsageActions, { From 2df781bda93780a021e55c1bf782cb8c864c5028 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 5 Mar 2026 13:56:52 +0100 Subject: [PATCH 238/448] sessions - allowlist more commands to run in modal editors (#299464) --- .../browser/parts/editor/modalEditorPart.ts | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 1f472ed3dd8..2e8ed96d010 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -35,12 +35,55 @@ import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, MO import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; const defaultModalEditorAllowableCommands = new Set([ + + // Application 'workbench.action.quit', 'workbench.action.reloadWindow', - 'workbench.action.closeActiveEditor', - 'workbench.action.closeAllEditors', + 'workbench.action.toggleFullScreen', + + // Quick access + 'workbench.action.gotoSymbol', + 'workbench.action.gotoLine', + + // Zoom + 'workbench.action.zoomIn', + 'workbench.action.zoomOut', + 'workbench.action.zoomReset', + + // File operations 'workbench.action.files.save', 'workbench.action.files.saveAll', + 'workbench.action.files.revert', + + // Close editors + 'workbench.action.closeActiveEditor', + 'workbench.action.closeAllEditors', + 'workbench.action.closeEditorsInGroup', + 'workbench.action.closeUnmodifiedEditors', + + // Settings + 'workbench.action.openSettings', + 'workbench.action.openSettings2', + 'workbench.action.openSettingsJson', + 'workbench.action.openGlobalSettings', + 'workbench.action.openApplicationSettingsJson', + 'workbench.action.openRawDefaultSettings', + 'workbench.action.openWorkspaceSettings', + 'workbench.action.openWorkspaceSettingsFile', + 'workbench.action.openFolderSettings', + 'workbench.action.openFolderSettingsFile', + 'workbench.action.openRemoteSettings', + 'workbench.action.openRemoteSettingsFile', + 'workbench.action.openAccessibilitySettings', + 'workbench.action.configureLanguageBasedSettings', + + // Keybindings + 'workbench.action.openGlobalKeybindings', + 'workbench.action.openDefaultKeybindingsFile', + 'workbench.action.openGlobalKeybindingsFile', + 'workbench.action.openKeyboardLayoutPicker', + + // Modal editor CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, MOVE_MODAL_EDITOR_TO_WINDOW_COMMAND_ID, From 0c87a51e309a99cb0c3721dfbfb959935ea0142e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 5 Mar 2026 14:41:03 +0100 Subject: [PATCH 239/448] sessions - do not steal focus when auto-opening changes view (#299487) fix - update `syncAuxiliaryBarVisibility` to open view --- .../sessions/contrib/changesView/browser/toggleChangesView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts b/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts index e7371a4f73d..abc40780aa6 100644 --- a/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts @@ -110,7 +110,7 @@ export class ToggleChangesViewContribution extends Disposable { private syncAuxiliaryBarVisibility(hasChanges: boolean): void { if (hasChanges) { - this.viewsService.openView(CHANGES_VIEW_ID, true); + this.viewsService.openView(CHANGES_VIEW_ID, false); } else { this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); } From 1a608475b81198a75d6533b4f166dfb19bffa215 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 5 Mar 2026 15:53:33 +0100 Subject: [PATCH 240/448] fix: update precondition for FixDiagnosticsAction and hide input widget on command execution (#299499) fixes https://github.com/microsoft/vscode/issues/299251 --- .../workbench/contrib/inlineChat/browser/inlineChatActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 8829e4bb912..aef9aeefc52 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -239,7 +239,7 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction { id: 'inlineChat.fixDiagnostics', title: localize2('fix', 'Fix'), icon: Codicon.editSparkle, - precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_FIX_DIAGNOSTICS_ENABLED, EditorContextKeys.selectionHasDiagnostics, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + precondition: ContextKeyExpr.and(CTX_FIX_DIAGNOSTICS_ENABLED, EditorContextKeys.selectionHasDiagnostics, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), menu: [{ id: MenuId.InlineChatEditorAffordance, group: '1_quickfix', @@ -255,6 +255,7 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction { } override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: unknown[]): void { + ctrl.inputWidget.hide(); ctrl.run({ autoSend: true, attachDiagnostics: true }); } } From 61dc401a3fe773601c2baa1815d2588e2ce70d21 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 5 Mar 2026 16:13:27 +0100 Subject: [PATCH 241/448] workaround for https://github.com/microsoft/vscode/issues/299484 (#299503) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 985ff7fd77a..e91a0f81269 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -3146,7 +3146,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth(); const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * toolbarItemGap; const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * toolbarItemGap : 0; - const contextUsageWidth = dom.getTotalWidth(this.contextUsageWidgetContainer); + const contextUsageWidth = 0;// dom.getTotalWidth(this.contextUsageWidgetContainer); const inputToolbarsPadding = 12; // pdading between input toolbar/execute toolbar/contextUsage. return executeToolbarWidth + executeToolbarPadding + contextUsageWidth + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding + inputToolbarsPadding); }; From 9a4492fde6057a380f491fec8d5fa7c922e9d7d7 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 5 Mar 2026 10:59:46 -0500 Subject: [PATCH 242/448] Cleanup some team members from end game notebook (#299507) --- .vscode/notebooks/my-endgame.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 50ebbfdb750..f44d9c4a45b 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintangent -author:jukasper -author:zhichli" + "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintangent -author:jukasper -author:zhichli" }, { "kind": 1, From 9086b47862f240a5a593759a92580f457fcf353b Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 5 Mar 2026 08:45:54 -0800 Subject: [PATCH 243/448] Temporarily restore webpack ext builds Needed since https://github.com/microsoft/vscode/pull/298920 snuck in which brought back webpack for the github extension --- build/gulpfile.extensions.ts | 12 + build/lib/extensions.ts | 208 ++- extensions/mangle-loader.js | 66 + extensions/shared.webpack.config.mjs | 209 +++ package-lock.json | 1529 ++++++++++++++++++++- package.json | 9 + test/monaco/package-lock.json | 1852 +------------------------- test/monaco/package.json | 12 +- 8 files changed, 2030 insertions(+), 1867 deletions(-) create mode 100644 extensions/mangle-loader.js create mode 100644 extensions/shared.webpack.config.mjs diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index e0137816c8c..8f9ac9b2b21 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -309,6 +309,13 @@ async function buildWebExtensions(isWatch: boolean): Promise { { ignore: ['**/node_modules'] } ); + // Find all webpack configs, excluding those that will be esbuilt + const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p))); + const webpackConfigLocations = (await nodeUtil.promisify(glob)( + path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), + { ignore: ['**/node_modules'] } + )).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath))); + const promises: Promise[] = []; // Esbuild for extensions @@ -323,5 +330,10 @@ async function buildWebExtensions(isWatch: boolean): Promise { ); } + // Run webpack for remaining extensions + if (webpackConfigLocations.length > 0) { + promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })))); + } + await Promise.all(promises); } diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index aacf25cbbc1..5710f4d6919 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -20,8 +20,10 @@ import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; +import webpack from 'webpack'; import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; +import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; @@ -30,8 +32,8 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const root = path.dirname(path.dirname(import.meta.dirname)); -// const commit = getVersion(root); -// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +const commit = getVersion(root); +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -63,24 +65,32 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream { +function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb ? 'esbuild.browser.mts' : 'esbuild.mts'; + const webpackConfigFileName = forWeb + ? `extension-browser.webpack.config.js` + : `extension.webpack.config.js`; + const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); + const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - // Esbuild only does bundling so we still want to run a separate type check step + // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), ...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)), ); isBundled = true; + } else if (hasWebpack) { + input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); + isBundled = true; } else { input = fromLocalNormal(extensionPath); } @@ -112,6 +122,132 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean) return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } +function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { + const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); + const webpack = require('webpack'); + const webpackGulp = require('webpack-stream'); + const result = es.through(); + + const packagedDependencies: string[] = []; + const stripOutSourceMaps: string[] = []; + const packageJsonConfig = require(path.join(extensionPath, 'package.json')); + if (packageJsonConfig.dependencies) { + const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = webpackConfig.default; + for (const key in webpackRootConfig.externals) { + if (key in packageJsonConfig.dependencies) { + packagedDependencies.push(key); + } + } + + if (webpackConfig.StripOutSourceMaps) { + for (const filePath of webpackConfig.StripOutSourceMaps) { + stripOutSourceMaps.push(filePath); + } + } + } + + // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar + // to vsce.PackageManager.Yarn. + // A static analysis showed there are no webpack externals that are dependencies of the current + // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list + // as a temporary workaround. + vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { + const files = fileNames + .map(fileName => path.join(extensionPath, fileName)) + .map(filePath => new File({ + path: filePath, + stat: fs.statSync(filePath), + base: extensionPath, + contents: fs.createReadStream(filePath) + })); + + // check for a webpack configuration files, then invoke webpack + // and merge its output with the files stream. + const webpackConfigLocations = (glob.sync( + path.join(extensionPath, '**', webpackConfigFileName), + { ignore: ['**/node_modules'] } + ) as string[]); + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { + + const webpackDone = (err: Error | undefined, stats: any) => { + fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); + if (err) { + result.emit('error', err); + } + const { compilation } = stats; + if (compilation.errors.length > 0) { + result.emit('error', compilation.errors.join('\n')); + } + if (compilation.warnings.length > 0) { + result.emit('error', compilation.warnings.join('\n')); + } + }; + + const exportedConfig = require(webpackConfigPath).default; + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + if (disableMangle) { + if (Array.isArray(config.module.rules)) { + for (const rule of config.module.rules) { + if (Array.isArray(rule.use)) { + for (const use of rule.use) { + if (String(use.loader).endsWith('mangle-loader.js')) { + use.options.disabled = true; + } + } + } + } + } + } + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data: File) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + if (path.extname(data.basename) === '.js') { + if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); + } else { + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + } + } + + this.emit('data', data); + })); + }); + }); + + es.merge(...webpackStreams, es.readArray(files)) + // .pipe(es.through(function (data) { + // // debug + // console.log('out', data.path, data.contents.length); + // this.emit('data', data); + // })) + .pipe(result); + + }).catch(err => { + console.error(extensionPath); + console.error(packagedDependencies); + result.emit('error', err); + }); + + return result.pipe(createStatsStream(path.basename(extensionPath))); +} function fromLocalNormal(extensionPath: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -513,6 +649,70 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); +export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { + const webpack = require('webpack') as typeof import('webpack'); + + const webpackConfigs: webpack.Configuration[] = []; + + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath).default; + function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { + for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { + const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; + if (outputRoot) { + config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!)); + } + webpackConfigs.push(config); + } + } + addConfig(configOrFnOrArray); + } + + function reporter(fullStats: any) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error: any) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning: any) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } else { + reporter(stats?.toJson()); + } + }); + } else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } else { + reporter(stats?.toJson()); + resolve(); + } + }); + } + }); +} + export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise { function reporter(stdError: string, script: string) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); diff --git a/extensions/mangle-loader.js b/extensions/mangle-loader.js new file mode 100644 index 00000000000..ed32a85e633 --- /dev/null +++ b/extensions/mangle-loader.js @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check + +const fs = require('fs'); +const webpack = require('webpack'); +const fancyLog = require('fancy-log'); +const ansiColors = require('ansi-colors'); +const { Mangler } = require('../build/lib/mangle/index.js'); + +/** + * Map of project paths to mangled file contents + * + * @type {Map>>} + */ +const mangleMap = new Map(); + +/** + * @param {string} projectPath + */ +function getMangledFileContents(projectPath) { + let entry = mangleMap.get(projectPath); + if (!entry) { + const log = (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data); + log(`Mangling ${projectPath}`); + const ts2tsMangler = new Mangler(projectPath, log, { mangleExports: true, manglePrivateFields: true }); + entry = ts2tsMangler.computeNewFileContents(); + mangleMap.set(projectPath, entry); + } + + return entry; +} + +/** + * @type {webpack.LoaderDefinitionFunction} + */ +module.exports = async function (source, sourceMap, meta) { + if (this.mode !== 'production') { + // Only enable mangling in production builds + return source; + } + if (true) { + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + return source; + } + const options = this.getOptions(); + if (options.disabled) { + // Dynamically disabled + return source; + } + + if (source !== fs.readFileSync(this.resourcePath).toString()) { + // File content has changed by previous webpack steps. + // Skip mangling. + return source; + } + + const callback = this.async(); + + const fileContentsMap = await getMangledFileContents(options.configFile); + + const newContents = fileContentsMap.get(this.resourcePath); + callback(null, newContents?.out ?? source, sourceMap, meta); +}; diff --git a/extensions/shared.webpack.config.mjs b/extensions/shared.webpack.config.mjs new file mode 100644 index 00000000000..12b1ea522a4 --- /dev/null +++ b/extensions/shared.webpack.config.mjs @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +import path from 'node:path'; +import fs from 'node:fs'; +import merge from 'merge-options'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import webpack from 'webpack'; +import { createRequire } from 'node:module'; + +/** @typedef {import('webpack').Configuration} WebpackConfig **/ + +const require = createRequire(import.meta.url); + +const tsLoaderOptions = { + compilerOptions: { + 'sourceMap': true, + }, + onlyCompileBundledFiles: true, +}; + +function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfig) { + const defaultConfig = { + mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + target: 'node', // extensions run in a node context + node: { + __dirname: false // leave the __dirname-behaviour intact + }, + + resolve: { + conditionNames: ['import', 'require', 'node-addons', 'node'], + mainFields: ['module', 'main'], + extensions: ['.ts', '.js'], // support ts-files and js-files + extensionAlias: { + // this is needed to resolve dynamic imports that now require the .js extension + '.js': ['.js', '.ts'], + } + }, + module: { + rules: [{ + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + // configure TypeScript loader: + // * enable sources maps for end-to-end source maps + loader: 'ts-loader', + options: tsLoaderOptions + }, + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + // { + // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), + // options: { + // configFile: path.join(extConfig.context, 'tsconfig.json') + // }, + // }, + ] + }] + }, + externals: { + 'electron': 'commonjs electron', // ignored to avoid bundling from node_modules + 'vscode': 'commonjs vscode', // ignored because it doesn't exist, + 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module + '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use + '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module + '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module + '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module + }, + output: { + // all output goes into `dist`. + // packaging depends on that and this must always be like it + filename: '[name].js', + path: path.join(extConfig.context, 'dist'), + libraryTarget: 'commonjs', + }, + // yes, really source maps + devtool: 'source-map', + plugins: nodePlugins(extConfig.context), + }; + + return merge(defaultConfig, extConfig); +} + +/** + * + * @param {string} context + */ +function nodePlugins(context) { + // Need to find the top-most `package.json` file + const folderName = path.relative(import.meta.dirname, context).split(/[\\\/]/)[0]; + const pkgPath = path.join(import.meta.dirname, folderName, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const id = `${pkg.publisher}.${pkg.name}`; + return [ + new CopyWebpackPlugin({ + patterns: [ + { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } + ] + }) + ]; +} +/** + * @typedef {{ + * configFile?: string + * }} AdditionalBrowserConfig + */ + +function withBrowserDefaults(/**@type WebpackConfig & { context: string }*/extConfig, /** @type AdditionalBrowserConfig */ additionalOptions = {}) { + /** @type WebpackConfig */ + const defaultConfig = { + mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + target: 'webworker', // extensions run in a webworker context + resolve: { + mainFields: ['browser', 'module', 'main'], + extensions: ['.ts', '.js'], // support ts-files and js-files + fallback: { + 'path': require.resolve('path-browserify'), + 'os': require.resolve('os-browserify'), + 'util': require.resolve('util') + }, + extensionAlias: { + // this is needed to resolve dynamic imports that now require the .js extension + '.js': ['.js', '.ts'], + }, + }, + module: { + rules: [{ + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + // configure TypeScript loader: + // * enable sources maps for end-to-end source maps + loader: 'ts-loader', + options: { + ...tsLoaderOptions, + // ...(additionalOptions ? {} : { configFile: additionalOptions.configFile }), + } + }, + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + // { + // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), + // options: { + // configFile: path.join(extConfig.context, additionalOptions?.configFile ?? 'tsconfig.json') + // }, + // }, + ] + }, { + test: /\.wasm$/, + type: 'asset/inline' + }] + }, + externals: { + 'vscode': 'commonjs vscode', // ignored because it doesn't exist, + 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module + '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use + '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module + '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module + '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module + }, + performance: { + hints: false + }, + output: { + // all output goes into `dist`. + // packaging depends on that and this must always be like it + filename: '[name].js', + path: path.join(extConfig.context, 'dist', 'browser'), + libraryTarget: 'commonjs', + }, + // yes, really source maps + devtool: 'source-map', + plugins: browserPlugins(extConfig.context) + }; + + return merge(defaultConfig, extConfig); +} + +/** + * + * @param {string} context + */ +function browserPlugins(context) { + // Need to find the top-most `package.json` file + // const folderName = path.relative(__dirname, context).split(/[\\\/]/)[0]; + // const pkgPath = path.join(__dirname, folderName, 'package.json'); + // const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + // const id = `${pkg.publisher}.${pkg.name}`; + return [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + }), + new CopyWebpackPlugin({ + patterns: [ + { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } + ] + }), + new webpack.DefinePlugin({ + 'process.platform': JSON.stringify('web'), + 'process.env': JSON.stringify({}), + 'process.env.BROWSER_ENV': JSON.stringify('true') + }) + ]; +} + +export default withNodeDefaults; +export { withNodeDefaults as node, withBrowserDefaults as browser, nodePlugins, browserPlugins }; diff --git a/package-lock.json b/package-lock.json index e8dfe47face..479fab9ed18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", + "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -99,6 +100,8 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.6.0", @@ -108,6 +111,7 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", + "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -148,12 +152,17 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", + "style-loader": "^3.3.2", "tar": "^7.5.9", + "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4", + "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, @@ -846,6 +855,16 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@electron/get": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", @@ -1356,6 +1375,28 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", @@ -2390,6 +2431,17 @@ "@types/json-schema": "*" } }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2623,6 +2675,18 @@ "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", "dev": true }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, "node_modules/@types/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", @@ -4035,6 +4099,167 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", @@ -4042,6 +4267,53 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -4170,6 +4442,20 @@ "addons/*" ] }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@zip.js/zip.js": { "version": "2.8.22", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.22.tgz", @@ -4228,6 +4514,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4294,6 +4593,61 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -5227,6 +5581,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5776,6 +6140,16 @@ } } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -5974,6 +6348,44 @@ "node": ">= 0.10" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -6085,6 +6497,13 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6395,6 +6814,44 @@ "is-plain-object": "^5.0.0" } }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -6607,6 +7064,71 @@ "source-map-resolve": "^0.6.0" } }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -6661,6 +7183,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -7064,6 +7599,19 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -7359,6 +7907,16 @@ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -7405,9 +7963,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7439,6 +7997,32 @@ "node": ">=6" } }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -8485,6 +9069,16 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", @@ -8532,6 +9126,56 @@ "node": ">=16.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -9327,6 +9971,13 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/glob-watcher": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", @@ -9709,6 +10360,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glogg": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", @@ -11514,6 +12185,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -11565,6 +12249,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -12207,6 +12911,37 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -12344,6 +13079,13 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -12799,6 +13541,35 @@ "uc.micro": "^2.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-app": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", @@ -12869,6 +13640,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "deprecated": "This package is deprecated. Use structuredClone instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -12887,6 +13666,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -13308,6 +14094,36 @@ "timers-ext": "^0.1.7" } }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/memory-fs/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -13342,6 +14158,13 @@ "node": ">=4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -13810,6 +14633,25 @@ "dev": true, "optional": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -13876,6 +14718,13 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -15119,6 +15968,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -15219,6 +16078,75 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/playwright": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", @@ -15332,6 +16260,90 @@ "url": "https://opencollective.com/postcss/" } }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/postcss/node_modules/picocolors": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", @@ -15502,6 +16514,13 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT" + }, "node_modules/pseudo-localization": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/pseudo-localization/-/pseudo-localization-2.4.0.tgz", @@ -16122,6 +17141,29 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", @@ -16484,6 +17526,50 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -16721,6 +17807,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16940,6 +18049,19 @@ "node": ">=8" } }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/slashes": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", @@ -17195,6 +18317,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-resolve": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", @@ -17708,6 +18840,23 @@ ], "license": "MIT" }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -18002,6 +19151,77 @@ "node": ">=6.0.0" } }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -18329,6 +19549,37 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/ts-morph": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz", @@ -19135,6 +20386,20 @@ "node": ">=10" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -19274,6 +20539,257 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", + "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fancy-log": "^1.3.3", + "lodash.clone": "^4.3.2", + "lodash.some": "^4.2.2", + "memory-fs": "^0.5.0", + "plugin-error": "^1.0.1", + "supports-color": "^8.1.1", + "through": "^2.3.8", + "vinyl": "^2.2.1" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "webpack": "^5.21.2" + } + }, + "node_modules/webpack-stream/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-stream/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/webpack-stream/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -19361,6 +20877,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/windows-foreground-love": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/windows-foreground-love/-/windows-foreground-love-0.6.1.tgz", diff --git a/package.json b/package.json index 8291e70e334..680a4bcd8cf 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", + "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -169,6 +170,8 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.6.0", @@ -178,6 +181,7 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", + "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -218,12 +222,17 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", + "style-loader": "^3.3.2", "tar": "^7.5.9", + "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4", + "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 45660fad3e8..513d33eeb34 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -8,79 +8,11 @@ "name": "test-monaco", "version": "1.0.0", "license": "MIT", - "dependencies": { - "postcss": "^8.5.6" - }, "devDependencies": { "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "css-loader": "^6.9.1", - "file-loader": "^6.2.0", - "style-loader": "^3.3.2", - "warnings-to-errors-webpack-plugin": "^2.3.0", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "warnings-to-errors-webpack-plugin": "^2.3.0" } }, "node_modules/@types/chai": { @@ -89,42 +21,6 @@ "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", "dev": true }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/junit-report-builder": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", @@ -132,312 +28,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", - "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -490,91 +80,6 @@ "playwright": ">1.0.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001775", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", - "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -601,122 +106,6 @@ "node": "*" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -729,273 +118,6 @@ "node": ">=0.12" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -1006,174 +128,6 @@ "node": "*" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "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/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/junit-report-builder": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz", @@ -1189,58 +143,6 @@ "node": ">=16" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -1264,36 +166,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -1304,104 +176,6 @@ "mustache": "bin/mustache" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -1415,230 +189,9 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1649,185 +202,6 @@ "semver": "bin/semver.js" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", - "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -1837,61 +211,6 @@ "node": ">=4" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/warnings-to-errors-webpack-plugin": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/warnings-to-errors-webpack-plugin/-/warnings-to-errors-webpack-plugin-2.3.0.tgz", @@ -1901,173 +220,6 @@ "webpack": "^2.2.0-rc || ^3 || ^4 || ^5" } }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.105.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", - "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/test/monaco/package.json b/test/monaco/package.json index 89902f2304f..c7373919431 100644 --- a/test/monaco/package.json +++ b/test/monaco/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "compile": "node ../../node_modules/typescript/bin/tsc", - "bundle-webpack": "webpack --config ./webpack.config.js --bail", + "bundle-webpack": "node ../../node_modules/webpack/bin/webpack --config ./webpack.config.js --bail", "esm-check": "node esm-check/esm-check.js", "test": "node runner.js" }, @@ -14,14 +14,6 @@ "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "css-loader": "^6.9.1", - "file-loader": "^6.2.0", - "style-loader": "^3.3.2", - "warnings-to-errors-webpack-plugin": "^2.3.0", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4" - }, - "dependencies": { - "postcss": "^8.5.6" + "warnings-to-errors-webpack-plugin": "^2.3.0" } } From 2ba48d2b522c6e53bf09f38f6dde21ce70617d0c Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 6 Mar 2026 02:15:36 +0900 Subject: [PATCH 244/448] chore: update dependency info for inno-updater bump (#299394) --- build/win32/Cargo.lock | 837 +++++++++++++++++++++++++++++------------ build/win32/Cargo.toml | 4 +- 2 files changed, 591 insertions(+), 250 deletions(-) diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index d35c41e4098..bc102802568 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -3,93 +3,141 @@ version = 4 [[package]] -name = "bitflags" -version = "1.3.2" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc" -version = "3.0.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ - "cfg-if", - "once_cell", + "powerfmt", ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "erased-serde" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ - "libc", - "redox_users", - "winapi", + "serde", ] [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -99,37 +147,102 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "getrandom" -version = "0.2.7" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", + "wasip3", ] [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] [[package]] name = "inno_updater" -version = "0.18.2" +version = "0.19.0" dependencies = [ "byteorder", "crc", @@ -142,40 +255,74 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] -name = "num_threads" -version = "0.1.6" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "libc", + "autocfg", ] [[package]] @@ -185,19 +332,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "proc-macro2" -version = "1.0.40" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -208,56 +371,96 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "redox_syscall" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom 0.2.7", - "redox_syscall", - "thiserror", -] - [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.7" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slog" -version = "2.7.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" +checksum = "9b3b8565691b22d2bdfc066426ed48f837fc0c5f2c8cad8d9718f7f99d6995c1" +dependencies = [ + "anyhow", + "erased-serde", + "rustversion", + "serde_core", +] [[package]] name = "slog-async" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" dependencies = [ "crossbeam-channel", "slog", @@ -267,10 +470,11 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" +checksum = "5cb1fc680b38eed6fad4c02b3871c09d2c81db8c96aa4e9c0a34904c830f09b5" dependencies = [ + "chrono", "is-terminal", "slog", "term", @@ -280,9 +484,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -297,42 +501,193 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "term" -version = "0.7.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "dirs-next", + "windows-sys 0.61.2", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", "rustversion", - "winapi", + "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] -name = "thiserror" -version = "1.0.31" +name = "wasm-bindgen-macro" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ - "thiserror-impl", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "thiserror-impl" -version = "1.0.31" +name = "wasm-bindgen-macro-support" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -340,113 +695,62 @@ dependencies = [ ] [[package]] -name = "thread_local" -version = "1.1.4" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "once_cell", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "time" -version = "0.3.11" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "itoa", - "libc", - "num_threads", - "time-macros", + "windows-link", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - -[[package]] -name = "unicode-ident" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "wit-bindgen-rt", + "windows-link", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" -dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows-link", ] [[package]] @@ -455,78 +759,36 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" - [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" - [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -534,16 +796,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.5" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ - "bitflags 2.9.1", + "anyhow", + "heck", + "wit-parser", ] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index 40e1a7a60fd..3e400552cc0 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.18.2" +version = "0.19.0" authors = ["Microsoft "] build = "build.rs" @@ -9,7 +9,7 @@ byteorder = "1.4.3" crc = "3.0.1" slog = "2.7.0" slog-async = "2.7.0" -slog-term = "2.9.1" +slog-term = "2.9.2" tempfile = "3.5.0" [target.'cfg(windows)'.dependencies.windows-sys] From 432c85934c1cb4935467f130db3824d61feeaa34 Mon Sep 17 00:00:00 2001 From: andysharman Date: Thu, 5 Mar 2026 09:29:29 -0800 Subject: [PATCH 245/448] Add chatMode and sessionType to interactiveSessionProviderInvoked telemetry (#299364) Track the active chat mode and session type on each chat request. chatMode uses getModeNameForTelemetry for proper identification of extension-contributed modes like Plan, with hashing for user-created agents. sessionType captures the session resource scheme. Co-authored-by: Alex Dima --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 3 ++- .../contrib/chat/common/chatService/chatServiceTelemetry.ts | 6 ++++++ src/vs/workbench/contrib/chat/common/model/chatModel.ts | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index e91a0f81269..77c5bc149f5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -82,7 +82,7 @@ import { InlineChatConfigKeys } from '../../../../inlineChat/common/inlineChat.j import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; -import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; +import { ChatMode, getModeNameForTelemetry, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, validateChatMode } from '../../../common/constants.js'; @@ -444,6 +444,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge isBuiltin: mode.isBuiltin } : undefined, modeId: modeId, + modeName: getModeNameForTelemetry(mode), applyCodeBlockSuggestionId: undefined, permissionLevel: this._currentPermissionLevel.get(), }; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts index 8dfb8f5333d..d524128a238 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts @@ -150,6 +150,8 @@ export type ChatProviderInvokedEvent = { attachmentKinds: string[]; model: string | undefined; permissionLevel: ChatPermissionLevel | undefined; + chatMode: string | undefined; + sessionType: string | undefined; }; export type ChatProviderInvokedClassification = { @@ -169,6 +171,8 @@ export type ChatProviderInvokedClassification = { attachmentKinds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The types of variables/attachments that the user included with their query.' }; model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model used to generate the response.' }; permissionLevel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The tool auto-approval permission level selected in the permission picker (default, autoApprove, or autopilot). Undefined when the picker is not applicable (e.g. ask mode or API-driven requests).' }; + chatMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat mode used for the request. Built-in modes (ask, agent, edit), extension-contributed names (e.g. Plan), or a hashed identifier for user-created custom agents.' }; + sessionType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The session type scheme (e.g. vscodeLocalChatSession for local, or remote session scheme).' }; owner: 'roblourens'; comment: 'Provides insight into the performance of Chat agents.'; }; @@ -306,6 +310,8 @@ export class ChatRequestTelemetry { attachmentKinds: this.attachmentKindsForTelemetry(request.variableData), model: this.resolveModelId(this.opts.options?.userSelectedModelId), permissionLevel: this.opts.options?.modeInfo?.kind === ChatModeKind.Ask ? undefined : this.opts.options?.modeInfo?.permissionLevel, + chatMode: this.opts.options?.modeInfo?.modeName ?? this.opts.options?.modeInfo?.modeId, + sessionType: this.opts.sessionResource.scheme, }); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index a08574a75c8..b7d2f749317 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -313,6 +313,7 @@ export interface IChatRequestModeInfo { isBuiltin: boolean; modeInstructions: IChatRequestModeInstructions | undefined; modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; + modeName?: string; applyCodeBlockSuggestionId: EditSuggestionId | undefined; permissionLevel?: ChatPermissionLevel; } From 3487365a09898056546f68899ee94f286a3ca915 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:11:06 +0000 Subject: [PATCH 246/448] Bump hono from 4.12.3 to 4.12.5 in /test/mcp (#299285) Bumps [hono](https://github.com/honojs/hono) from 4.12.3 to 4.12.5. - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.3...v4.12.5) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- test/mcp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index f67a5570734..f480569530b 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -702,9 +702,9 @@ } }, "node_modules/hono": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", - "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", "engines": { "node": ">=16.9.0" From 65ecbd4b562239b4a882ebba0980a3099da5e251 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:35:22 -0800 Subject: [PATCH 247/448] Persist sessions sidebar section collapse state (#299355) * Persist sessions sidebar section collapse state * Address PR feedback: guard programmatic collapse and validate JSON --- .../agentSessions/agentSessionsControl.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 6f09474087b..b5335cbe676 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -39,6 +39,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { IChatWidget } from '../chat.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions { readonly overrideStyles: IStyleOverride; @@ -78,6 +79,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo private sessionsList: WorkbenchCompressibleAsyncDataTree | undefined; private sessionsListFindIsOpen = false; + private _isProgrammaticCollapseChange = false; private readonly updateSessionsListThrottler = this._register(new Throttler()); @@ -103,6 +105,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IEditorService private readonly editorService: IEditorService, + @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -173,9 +176,49 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo })); } + private static readonly SECTION_COLLAPSE_STATE_KEY = 'agentSessions.sectionCollapseState'; + + private getSavedCollapseState(section: AgentSessionSection): boolean | undefined { + const raw = this.storageService.get(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, StorageScope.PROFILE); + if (raw) { + try { + const state: Record = JSON.parse(raw); + if (typeof state[section] === 'boolean') { + return state[section]; + } + } catch { + // ignore corrupt data + } + } + return undefined; + } + + private saveSectionCollapseState(section: AgentSessionSection, collapsed: boolean): void { + let state: Record = {}; + const raw = this.storageService.get(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, StorageScope.PROFILE); + if (raw) { + try { + const parsed = JSON.parse(raw); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + state = parsed; + } + } catch { + // ignore corrupt data + } + } + state[section] = collapsed; + this.storageService.store(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, JSON.stringify(state), StorageScope.PROFILE, StorageTarget.USER); + } + private createList(container: HTMLElement): void { const collapseByDefault = (element: unknown) => { if (isAgentSessionSection(element)) { + // Check for persisted user preference first + const saved = this.getSavedCollapseState(element.section); + if (saved !== undefined) { + return saved; + } + if (element.section === AgentSessionSection.More && !this.options.filter.getExcludes().read) { return true; // More section is always collapsed unless only showing unread } @@ -285,6 +328,16 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.updateSectionCollapseStates(); })); + + this._register(list.onDidChangeCollapseState(e => { + if (this._isProgrammaticCollapseChange) { + return; + } + const element = e.node.element?.element; + if (element && isAgentSessionSection(element)) { + this.saveSectionCollapseState(element.section, e.node.collapsed); + } + })); } private updateEmpty(isEmpty: boolean): void { @@ -396,6 +449,19 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo return; } + this._isProgrammaticCollapseChange = true; + try { + this._updateSectionCollapseStatesCore(); + } finally { + this._isProgrammaticCollapseChange = false; + } + } + + private _updateSectionCollapseStatesCore(): void { + if (!this.sessionsList) { + return; + } + const model = this.agentSessionsService.model; for (const child of this.sessionsList.getNode(model).children) { if (!isAgentSessionSection(child.element)) { From 343b4af2e45658477bca25833c924c15f72ba455 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:47:48 -0800 Subject: [PATCH 248/448] [MCP_Sandboxing]: Add ripgrep path to env variables (#299320) include ripgrep path --- src/vs/workbench/api/node/extHostMcpNode.ts | 5 ++++ .../contrib/mcp/common/mcpSandboxService.ts | 30 ++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/node/extHostMcpNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts index 1b961f66ae1..c08fc0af221 100644 --- a/src/vs/workbench/api/node/extHostMcpNode.ts +++ b/src/vs/workbench/api/node/extHostMcpNode.ts @@ -73,6 +73,11 @@ export class NodeExtHostMpcService extends ExtHostMcpService { } } for (const [key, value] of Object.entries(launch.env)) { + // For PATH, we want to append to the existing PATH instead of overwriting it. + if (key.toUpperCase() === 'PATH' && value !== null) { + env[key] = env[key] ? `${env[key]}${path.delimiter}${String(value)}` : String(value); + continue; + } env[key] = value === null ? undefined : String(value); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts index 24fac57f1de..7f930eabb54 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts @@ -20,7 +20,8 @@ import { IMcpResourceScannerService, McpResourceTarget } from '../../../../platf import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpSandboxConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; -import { IMcpPotentialSandboxBlock, McpServerDefinition, McpServerLaunch, McpServerTransportType } from './mcpTypes.js'; +import { IMcpPotentialSandboxBlock, McpServerDefinition, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from './mcpTypes.js'; + export const IMcpSandboxService = createDecorator('mcpSandboxService'); @@ -45,6 +46,7 @@ type SandboxConfigSuggestionResult = { type SandboxLaunchDetails = { execPath: string | undefined; srtPath: string | undefined; + rgPath: string | undefined; sandboxConfigPath: string | undefined; tempDir: URI | undefined; }; @@ -86,15 +88,14 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService this._logService.trace(`McpSandboxService: Launching with config target ${configTarget}`); const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, launch.sandbox, launch.cwd); const sandboxArgs = this._getSandboxCommandArgs(launch.command, launch.args, launchDetails.sandboxConfigPath); - const sandboxEnv = this._getSandboxEnvVariables(launchDetails.tempDir, remoteAuthority); + const sandboxEnv = await this._getSandboxEnvVariables(launch.env, launchDetails.tempDir, launchDetails.rgPath, remoteAuthority); if (launchDetails.srtPath) { - const envWithSandbox = sandboxEnv ? { ...launch.env, ...sandboxEnv } : launch.env; if (launchDetails.execPath) { return { ...launch, command: launchDetails.execPath, args: [launchDetails.srtPath, ...sandboxArgs], - env: envWithSandbox, + env: sandboxEnv, type: McpServerTransportType.Stdio, }; } else { @@ -102,7 +103,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService ...launch, command: launchDetails.srtPath, args: sandboxArgs, - env: envWithSandbox, + env: sandboxEnv, type: McpServerTransportType.Stdio, }; } @@ -252,16 +253,17 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService private async _resolveSandboxLaunchDetails(configTarget: ConfigurationTarget, remoteAuthority?: string, sandboxConfig?: IMcpSandboxConfiguration, launchCwd?: string): Promise { const os = await this._getOperatingSystem(remoteAuthority); if (os === OperatingSystem.Windows) { - return { execPath: undefined, srtPath: undefined, sandboxConfigPath: undefined, tempDir: undefined }; + return { execPath: undefined, srtPath: undefined, rgPath: undefined, sandboxConfigPath: undefined, tempDir: undefined }; } const appRoot = await this._getAppRoot(remoteAuthority); const execPath = await this._getExecPath(os, appRoot, remoteAuthority); const tempDir = await this._getTempDir(remoteAuthority); const srtPath = this._pathJoin(os, appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js'); + const rgPath = this._pathJoin(os, appRoot, 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg'); const sandboxConfigPath = tempDir ? await this._updateSandboxConfig(tempDir, configTarget, sandboxConfig, launchCwd) : undefined; this._logService.debug(`McpSandboxService: Updated sandbox config path: ${sandboxConfigPath}`); - return { execPath, srtPath, sandboxConfigPath, tempDir }; + return { execPath, srtPath, rgPath, sandboxConfigPath, tempDir }; } private async _getExecPath(os: OperatingSystem, appRoot: string, remoteAuthority?: string): Promise { @@ -271,10 +273,13 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService return undefined; // Use Electron executable as the default exec path for local development, which will run the sandbox runtime wrapper with Electron in node mode. For remote, we need to specify the node executable to ensure it runs with Node.js. } - private _getSandboxEnvVariables(tempDir: URI | undefined, remoteAuthority?: string): Record | undefined { - let env: Record = {}; + private async _getSandboxEnvVariables(baseEnv: McpServerTransportStdio['env'], tempDir: URI | undefined, rgPath: string | undefined, remoteAuthority?: string): Promise { + let env: McpServerTransportStdio['env'] = { ...baseEnv }; if (tempDir) { - env = { TMPDIR: tempDir.path, SRT_DEBUG: 'true' }; + env = { ...env, TMPDIR: tempDir.path, SRT_DEBUG: 'true' }; + } + if (rgPath) { + env = { ...env, PATH: env['PATH'] ? `${env['PATH']}${await this._getPathDelimiter(remoteAuthority)}${dirname(rgPath)}` : dirname(rgPath) }; } if (!remoteAuthority) { // Add any remote-specific environment variables here @@ -391,4 +396,9 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService return path.join(...segments); }; + private _getPathDelimiter = async (remoteAuthority?: string) => { + const os = await this._getOperatingSystem(remoteAuthority); + return os === OperatingSystem.Windows ? win32.delimiter : posix.delimiter; + }; + } From daf369a0ea570f172ffd4a30e13faf0617d1bbe5 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:55:26 -0800 Subject: [PATCH 249/448] Validate URLs in open browser tool (#299555) --- .../browserView/electron-browser/tools/openBrowserTool.ts | 5 ++++- .../electron-browser/tools/openBrowserToolNonAgentic.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts index 24420493758..a4090c87c4c 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts @@ -25,7 +25,7 @@ export const OpenBrowserToolData: IToolData = { properties: { url: { type: 'string', - description: 'The URL to open in the browser.' + description: 'The full URL to open in the browser.' }, }, required: ['url'], @@ -60,6 +60,9 @@ export class OpenBrowserTool implements IToolImpl { if (!params.url) { return errorResult('The "url" parameter is required.'); } + if (!URL.parse(params.url)) { + return errorResult('You must provide a complete, valid URL.'); + } const { pageId, summary } = await this.playwrightService.openPage(params.url); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts index 444e2a483b0..40e6abcf62b 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts @@ -43,6 +43,9 @@ export class OpenBrowserToolNonAgentic implements IToolImpl { if (!params.url) { return errorResult('The "url" parameter is required.'); } + if (!URL.parse(params.url)) { + return errorResult('You must provide a complete, valid URL.'); + } logBrowserOpen(this.telemetryService, 'chatTool'); From 412a817d88227041ef16c3a377b36bfd5c83c4b2 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 5 Mar 2026 13:59:56 -0500 Subject: [PATCH 250/448] fix chat question height (#299557) fix #299556 --- .../chatQuestionCarouselPart.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 753e518fb2d..47d517ef19a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -331,6 +331,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return; } + // Clear stale size constraints first so this step can shrink after + // navigating from a taller question. + if (scrollableNode.style.height !== '' || scrollableNode.style.maxHeight !== '') { + scrollableNode.style.height = ''; + scrollableNode.style.maxHeight = ''; + } + if (scrollableContent.style.height !== '' || scrollableContent.style.maxHeight !== '') { + scrollableContent.style.height = ''; + scrollableContent.style.maxHeight = ''; + } + // Use the flex-resolved container height (constrained by CSS max-height) // instead of window.innerHeight, so the scroll viewport tracks actual chat space. const maxContainerHeight = this._questionContainer.clientHeight; @@ -345,12 +356,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent .reduce((sum, child) => sum + (child as HTMLElement).offsetHeight, 0); const availableScrollableHeight = Math.floor(maxContainerHeight - contentVerticalPadding - nonScrollableContentHeight); - const constrainedScrollableHeight = Math.max(0, availableScrollableHeight); + + const contentScrollableHeight = scrollableContent.scrollHeight; + const constrainedScrollableHeight = Math.max(0, Math.min(availableScrollableHeight, contentScrollableHeight)); const constrainedScrollableHeightPx = `${constrainedScrollableHeight}px`; + // Constrain wrapper + content so no stale flex sizing survives between steps. + if (scrollableNode.style.height !== constrainedScrollableHeightPx || scrollableNode.style.maxHeight !== constrainedScrollableHeightPx) { + scrollableNode.style.height = constrainedScrollableHeightPx; + scrollableNode.style.maxHeight = constrainedScrollableHeightPx; + } + // Constrain the content element (DomScrollableElement._element) so that // scanDomNode sees clientHeight < scrollHeight and enables scrolling. - // The wrapper inherits the same constraint via CSS flex. if (scrollableContent.style.height !== constrainedScrollableHeightPx || scrollableContent.style.maxHeight !== constrainedScrollableHeightPx) { scrollableContent.style.height = constrainedScrollableHeightPx; scrollableContent.style.maxHeight = constrainedScrollableHeightPx; @@ -582,6 +600,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollableNode.classList.add('chat-question-input-scrollable'); this._questionContainer.appendChild(inputScrollableNode); + // Render footer before first layout so the scrollable area is measured against + // its final available height and does not visibly resize twice. + if (!isSingleQuestion) { + this.renderFooter(); + } else { + this.renderSingleQuestionFooter(); + } + let relayoutScheduled = false; const relayoutScheduler = questionRenderStore.add(new MutableDisposable()); const scheduleLayoutInputScrollable = () => { @@ -600,7 +626,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent questionRenderStore.add(inputResizeObserver.observe(inputScrollableNode)); questionRenderStore.add(inputResizeObserver.observe(inputContainer)); scheduleLayoutInputScrollable(); - this.layoutInputScrollable(inputScrollable); questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => { inputContainer.scrollTop = 0; inputContainer.scrollLeft = 0; @@ -608,13 +633,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollable.scanDomNode(); })); - // Render footer for multi-question carousels or single-question carousels. - if (!isSingleQuestion) { - this.renderFooter(); - } else { - this.renderSingleQuestionFooter(); - } - // Update aria-label to reflect the current question this._updateAriaLabel(); From b290036d2484899b65de291b2864324a4182d02c Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:15:11 -0800 Subject: [PATCH 251/448] chore: test gif transfer (#299560) From e1116427ee2f187e51701b7e14e36a293d806bf9 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:28:32 -0800 Subject: [PATCH 252/448] Restore old package-lock exact version --- package-lock.json | 470 +++++++++++++++++++--------------------------- 1 file changed, 196 insertions(+), 274 deletions(-) diff --git a/package-lock.json b/package-lock.json index 479fab9ed18..85a5fbe2605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -856,11 +856,10 @@ "dev": true }, "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", + "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -1106,6 +1105,29 @@ "node": ">=0.4.0" } }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, "node_modules/@gulp-sourcemaps/map-sources": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", @@ -2680,7 +2702,6 @@ "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "tapable": "^2.2.0", @@ -3130,9 +3151,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-12", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-12.tgz", - "integrity": "sha512-omdtI6hEzpa901Q1s53ndM2vp3ROIVFFCGdz8I6hl4DZ/eKQzEdGYlY09Lnxfh+r9PfSDoyafChGIMIXmNnsRQ==", + "version": "0.0.45-13", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-13.tgz", + "integrity": "sha512-Q0oIp4r0aBMpvf5MyTqZycs7A7CGQqCmiJvmQ2pEa4HmAqLKHz+o4xzK0zqNfbQB6y35dFY1jJn5QRIi43eTwg==", "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { @@ -4272,7 +4293,6 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -4286,7 +4306,6 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -4300,7 +4319,6 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -4457,9 +4475,9 @@ "license": "Apache-2.0" }, "node_modules/@zip.js/zip.js": { - "version": "2.8.22", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.22.tgz", - "integrity": "sha512-0KlzbVR6r8irIX2o3zvUlosBDef62VDl47oUfa1U/qgEs67h4/eGBrX/6HWa1RQbt+J6sAeVmtyFKbTHNdF8qQ==", + "version": "2.8.23", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.23.tgz", + "integrity": "sha512-RB+RLnxPJFPrGvQ9rgO+4JOcsob6lD32OcF0QE0yg24oeW9q8KnTTNlugcDaIveEcCbclobJcZP+fLQ++sH0bw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4515,9 +4533,9 @@ } }, "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", + "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", "dev": true, "license": "MIT", "engines": { @@ -4598,7 +4616,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -4632,20 +4649,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, "peerDependencies": { - "ajv": "^8.8.2" + "ajv": "^6.9.1" } }, "node_modules/amdefine": { @@ -5586,7 +5598,6 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } @@ -6141,15 +6152,23 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", "dev": true, - "license": "MIT", + "dependencies": { + "tslib": "^1.9.0" + }, "engines": { "node": ">=6.0" } }, + "node_modules/chrome-trace-event/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -6353,7 +6372,6 @@ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, - "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -6368,7 +6386,6 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -6381,7 +6398,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6498,11 +6514,10 @@ } }, "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -6819,7 +6834,6 @@ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", "dev": true, - "license": "MIT", "dependencies": { "fast-glob": "^3.2.11", "glob-parent": "^6.0.1", @@ -6844,7 +6858,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -6852,6 +6865,37 @@ "node": ">=10.13.0" } }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", + "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -7065,17 +7109,16 @@ } }, "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz", + "integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==", "dev": true, - "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" @@ -7088,45 +7131,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" } }, "node_modules/css-select": { @@ -7188,7 +7193,6 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, - "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -7604,7 +7608,6 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -7912,7 +7915,6 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -7963,9 +7965,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "license": "MIT", "dependencies": { @@ -7998,11 +8000,10 @@ } }, "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true, - "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -8015,7 +8016,6 @@ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, - "license": "MIT", "dependencies": { "prr": "~1.0.1" }, @@ -9070,14 +9070,10 @@ } }, "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true }, "node_modules/fastq": { "version": "1.9.0", @@ -9131,7 +9127,6 @@ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", "dev": true, - "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" @@ -9147,24 +9142,13 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", + "@types/json-schema": "^7.0.6", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" }, @@ -10360,26 +10344,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glogg": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", @@ -12190,7 +12154,6 @@ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, - "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -12250,11 +12213,10 @@ } }, "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", "dev": true, - "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -12264,9 +12226,6 @@ }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/import-meta-resolve": { @@ -13083,8 +13042,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -13560,7 +13518,6 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, - "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -13643,10 +13600,8 @@ "node_modules/lodash.clone": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", - "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", - "deprecated": "This package is deprecated. Use structuredClone instead.", - "dev": true, - "license": "MIT" + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "dev": true }, "node_modules/lodash.clonedeep": { "version": "4.5.0", @@ -13669,9 +13624,8 @@ "node_modules/lodash.some": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", - "dev": true, - "license": "MIT" + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true }, "node_modules/lodash.zip": { "version": "4.2.0", @@ -14099,7 +14053,6 @@ "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dev": true, - "license": "MIT", "dependencies": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -14109,11 +14062,10 @@ } }, "node_modules/memory-fs/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -14722,8 +14674,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/netmask": { "version": "2.0.2", @@ -15973,7 +15924,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -16083,7 +16033,6 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -16096,7 +16045,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -16110,7 +16058,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -16123,7 +16070,6 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -16139,7 +16085,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -16243,29 +16188,39 @@ } }, "node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", "dev": true, - "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -16274,14 +16229,13 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, - "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", + "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -16292,13 +16246,12 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, - "license": "ISC", "dependencies": { - "postcss-selector-parser": "^7.0.0" + "postcss-selector-parser": "^6.0.4" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -16312,7 +16265,6 @@ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, - "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, @@ -16324,9 +16276,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -16341,15 +16293,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/prebuild-install": { "version": "7.1.2", @@ -16517,9 +16461,8 @@ "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "license": "MIT" + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY= sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true }, "node_modules/pseudo-localization": { "version": "2.4.0", @@ -17146,7 +17089,6 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, - "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -17159,7 +17101,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -17563,12 +17504,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/semver": { "version": "7.7.4", @@ -17812,7 +17764,6 @@ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, - "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -17825,7 +17776,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -18049,19 +17999,6 @@ "node": ">=8" } }, - "node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/slashes": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", @@ -18841,11 +18778,10 @@ "license": "MIT" }, "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", + "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 12.13.0" }, @@ -19171,15 +19107,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", - "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -19550,11 +19487,10 @@ } }, "node_modules/ts-loader": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", - "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", "dev": true, - "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", @@ -19571,13 +19507,12 @@ } }, "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { - "node": ">= 12" + "node": ">= 8" } }, "node_modules/ts-morph": { @@ -20540,9 +20475,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.105.4", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", - "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "version": "5.105.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", + "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", "dev": true, "license": "MIT", "dependencies": { @@ -20556,7 +20491,7 @@ "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", + "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -20568,7 +20503,7 @@ "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", + "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, @@ -20593,7 +20528,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -20639,7 +20573,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } @@ -20649,7 +20582,6 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -20659,7 +20591,6 @@ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, - "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, @@ -20668,14 +20599,12 @@ } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, - "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", - "flat": "^5.0.2", "wildcard": "^2.0.0" }, "engines": { @@ -20697,7 +20626,6 @@ "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", "dev": true, - "license": "MIT", "dependencies": { "fancy-log": "^1.3.3", "lodash.clone": "^4.3.2", @@ -20720,7 +20648,6 @@ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.10" } @@ -20730,7 +20657,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -20746,7 +20672,6 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", "dev": true, - "license": "MIT", "dependencies": { "clone": "^2.1.1", "clone-buffer": "^1.0.0", @@ -20771,7 +20696,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -20785,7 +20709,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -20878,11 +20801,10 @@ } }, "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true }, "node_modules/windows-foreground-love": { "version": "0.6.1", From a5876c850f4e78b9016664301d32e956de945070 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 5 Mar 2026 16:44:14 -0500 Subject: [PATCH 253/448] fix some issues with tips, remove some (#299608) --- .../contrib/chat/browser/chatTipCatalog.ts | 45 +------- .../contrib/chat/browser/chatTipService.ts | 31 ------ .../chat/browser/chatTipStorageKeys.ts | 2 - .../chat/test/browser/chatTipService.test.ts | 103 +++++------------- 4 files changed, 28 insertions(+), 153 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index 4bd1c8fd3c6..f112d72e2cd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -127,7 +127,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.switchToAuto', - "Using gpt-4.1? Try switching to [Auto](command:workbench.action.chat.openModelPicker) in the model picker for better coding performance." + "Using GPT-4.1? Try switching to [Auto](command:workbench.action.chat.openModelPicker) in the model picker for better coding performance." ) ); }, @@ -220,25 +220,6 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ TipTrackingCommands.CreateSkillUsed, ], }, - { - id: 'tip.agentMode', - tier: ChatTipTier.Foundational, - priority: 10, - buildMessage(ctx) { - const label = getCommandLabel('workbench.action.chat.openEditSession'); - const kb = formatKeybinding(ctx, 'workbench.action.chat.openEditSession'); - return new MarkdownString( - localize( - 'tip.agentMode', - "Try [{0}](command:workbench.action.chat.openEditSession){1} to make edits across your project and run commands.", - label, - kb - ) - ); - }, - when: ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Agent), - excludeWhenModesUsed: [ChatModeKind.Agent], - }, { id: 'tip.planMode', tier: ChatTipTier.Foundational, @@ -289,7 +270,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ tier: ChatTipTier.Qol, buildMessage() { return new MarkdownString( - localize('tip.undoChanges', "Select \"Restore Checkpoint\" to undo changes after that point in the chat conversation.") + localize('tip.undoChanges', "Hover a previous request and select \"Restore Checkpoint\" to undo changes after that point in the chat conversation.") ); }, when: ContextKeyExpr.and( @@ -333,26 +314,6 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ TipTrackingCommands.ForkConversationUsed, ], }, - { - id: 'tip.yoloMode', - tier: ChatTipTier.Qol, - buildMessage() { - return new MarkdownString( - localize( - 'tip.yoloMode', - "Enable [{0}](command:workbench.action.openSettings?%5B%22{1}%22%5D) to give the agent full control without manual confirmation.", - 'auto approve', - ChatConfiguration.GlobalAutoApprove - ) - ); - }, - when: ContextKeyExpr.and( - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), - ContextKeyExpr.notEquals('config.chat.tools.global.autoApprove', true), - ), - excludeWhenSettingsChanged: [ChatConfiguration.GlobalAutoApprove], - dismissWhenCommandsClicked: ['workbench.action.openSettings'], - }, { id: 'tip.agenticBrowser', tier: ChatTipTier.Qol, @@ -377,7 +338,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ tier: ChatTipTier.Qol, buildMessage() { return new MarkdownString( - localize('tip.mermaid', "Ask the agent to draw an architectural diagram or flow chart; it can render Mermaid diagrams directly in chat.") + localize('tip.mermaid', "Ask the agent to draw an architectural diagram or flow chart. It can render Mermaid diagrams directly in chat.") ); }, when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 593df2b7657..7dcf497e4fe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -191,7 +191,6 @@ export class ChatTipService extends Disposable implements IChatTipService { private readonly _tracker: TipEligibilityTracker; private readonly _createSlashCommandsUsageTracker: CreateSlashCommandsUsageTracker; - private _yoloModeEverEnabled: boolean; private _thinkingPhrasesEverModified: boolean; private _tipsHiddenForSession = false; private readonly _tipCommandListener = this._register(new MutableDisposable()); @@ -233,25 +232,6 @@ export class ChatTipService extends Disposable implements IChatTipService { } })); - // Track whether yolo mode was ever enabled - this._yoloModeEverEnabled = this._storageService.getBoolean(ChatTipStorageKeys.YoloModeEverEnabled, StorageScope.APPLICATION, false); - if (!this._yoloModeEverEnabled && this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove)) { - this._yoloModeEverEnabled = true; - this._storageService.store(ChatTipStorageKeys.YoloModeEverEnabled, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - } - if (!this._yoloModeEverEnabled) { - const configListener = this._register(new MutableDisposable()); - configListener.value = this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.GlobalAutoApprove)) { - if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove)) { - this._yoloModeEverEnabled = true; - this._storageService.store(ChatTipStorageKeys.YoloModeEverEnabled, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - configListener.clear(); - } - } - }); - } - this._thinkingPhrasesEverModified = this._storageService.getBoolean(ChatTipStorageKeys.ThinkingPhrasesEverModified, StorageScope.APPLICATION, false); if (!this._thinkingPhrasesEverModified && this._isSettingModified(ChatConfiguration.ThinkingPhrases)) { this._thinkingPhrasesEverModified = true; @@ -714,17 +694,6 @@ export class ChatTipService extends Disposable implements IChatTipService { if (this._tracker.isExcluded(tip)) { return false; } - if (tip.id === 'tip.yoloMode') { - if (this._yoloModeEverEnabled) { - this._logService.debug('#ChatTips: tip excluded because yolo mode was previously enabled', tip.id); - return false; - } - const inspected = this._configurationService.inspect(ChatConfiguration.GlobalAutoApprove); - if (inspected.policyValue === false) { - this._logService.debug('#ChatTips: tip excluded because policy restricts auto-approve', tip.id); - return false; - } - } if (tip.id === 'tip.thinkingPhrases' && this._thinkingPhrasesEverModified) { this._logService.debug('#ChatTips: tip excluded because thinking phrases setting was previously modified', tip.id); return false; diff --git a/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts b/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts index 547cb11b836..f16af0caf0d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts @@ -11,8 +11,6 @@ export const ChatTipStorageKeys = { DismissedTips: 'chat.tip.dismissed', /** The ID of the last tip that was shown, for round-robin selection. */ LastTipId: 'chat.tip.lastTipId', - /** Whether the user has ever enabled global auto-approve (yolo mode). */ - YoloModeEverEnabled: 'chat.tip.yoloModeEverEnabled', /** Whether the user has ever modified the thinking phrases setting. */ ThinkingPhrasesEverModified: 'chat.tip.thinkingPhrasesEverModified', }; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index f0946ccfb22..447a1bac044 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -21,7 +21,7 @@ import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGEN import { AgentFileType, IPromptPath, IPromptsService, IResolvedAgentFile, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { MockLanguageModelToolsService } from '../common/tools/mockLanguageModelToolsService.js'; @@ -253,6 +253,7 @@ suite('ChatTipService', () => { assert.ok(tip); assert.strictEqual(tip.id, 'tip.switchToAuto'); + assert.ok(tip.content.value.includes('GPT-4.1')); }); test('does not return Auto switch tip when current model is not gpt-4.1', () => { @@ -676,6 +677,29 @@ suite('ChatTipService', () => { assert.ok(storageService.get('chat.tip.dismissed', StorageScope.APPLICATION), 'Expected dismissed tips to migrate to application storage'); }); + test('tip.undoChanges describes where to find restore checkpoint', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType); + contextKeyService.createKey(ChatContextKeys.chatModeKind.key, ChatModeKind.Agent); + + const tip = findTipById(service, 'tip.undoChanges'); + + assert.ok(tip); + assert.ok(tip.content.value.includes('Hover a previous request')); + assert.ok(tip.content.value.includes('Restore Checkpoint')); + }); + + test('tip.mermaid uses sentence punctuation in display text', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModeKind.key, ChatModeKind.Agent); + + const tip = findTipById(service, 'tip.mermaid'); + + assert.ok(tip); + assert.ok(tip.content.value.includes('flow chart. It can render Mermaid diagrams directly in chat.')); + assert.ok(!tip.content.value.includes('flow chart; it can render Mermaid diagrams directly in chat.')); + }); + function createMockPromptsService( agentInstructions: IResolvedAgentFile[] = [], promptInstructions: IPromptPath[] = [], @@ -1269,81 +1293,6 @@ suite('ChatTipService', () => { } }); - test('does not show tip.yoloMode after auto-approve has ever been enabled', () => { - const service = createService(); - contextKeyService.createKey(ChatContextKeys.chatModeKind.key, ChatModeKind.Agent); - - // Enable auto-approve so the service records yoloModeEverEnabled - configurationService.setUserConfiguration(ChatConfiguration.GlobalAutoApprove, true); - (configurationService as TestConfigurationService).onDidChangeConfigurationEmitter.fire({ - affectsConfiguration: (key: string) => key === ChatConfiguration.GlobalAutoApprove, - affectedKeys: new Set([ChatConfiguration.GlobalAutoApprove]), - change: { keys: [], overrides: [] }, - source: ConfigurationTarget.USER, - }); - - // Turn auto-approve back off - configurationService.setUserConfiguration(ChatConfiguration.GlobalAutoApprove, false); - - // The yoloMode tip should never appear since it was ever enabled - for (let i = 0; i < 100; i++) { - const tip = service.getWelcomeTip(contextKeyService); - if (!tip) { - break; - } - assert.notStrictEqual(tip.id, 'tip.yoloMode', 'tip.yoloMode should not be shown after auto-approve was ever enabled'); - service.dismissTip(); - } - - // Verify the flag was persisted - assert.strictEqual( - storageService.getBoolean('chat.tip.yoloModeEverEnabled', StorageScope.APPLICATION, false), - true, - 'yoloModeEverEnabled should be persisted in application storage', - ); - }); - - test('does not show tip.yoloMode when yoloModeEverEnabled is already persisted in storage', () => { - // Simulate a previous session having set the flag - storageService.store('chat.tip.yoloModeEverEnabled', true, StorageScope.APPLICATION, StorageTarget.MACHINE); - - const service = createService(); - contextKeyService.createKey(ChatContextKeys.chatModeKind.key, ChatModeKind.Agent); - - for (let i = 0; i < 100; i++) { - const tip = service.getWelcomeTip(contextKeyService); - if (!tip) { - break; - } - assert.notStrictEqual(tip.id, 'tip.yoloMode', 'tip.yoloMode should not be shown when yoloModeEverEnabled is already in storage'); - service.dismissTip(); - } - }); - - test('does not show tip.yoloMode when policy restricts auto-approve', () => { - const policyConfigService = new TestConfigurationService(); - const originalInspect = policyConfigService.inspect.bind(policyConfigService); - policyConfigService.inspect = (key: string, overrides?: any) => { - if (key === ChatConfiguration.GlobalAutoApprove) { - return { ...originalInspect(key, overrides), policyValue: false } as unknown as T; - } - return originalInspect(key, overrides); - }; - configurationService = policyConfigService; - instantiationService.stub(IConfigurationService, configurationService); - - const service = createService(); - contextKeyService.createKey(ChatContextKeys.chatModeKind.key, ChatModeKind.Agent); - - for (let i = 0; i < 100; i++) { - const tip = service.getWelcomeTip(contextKeyService); - if (!tip) { - break; - } - assert.notStrictEqual(tip.id, 'tip.yoloMode', 'tip.yoloMode should not be shown when policy restricts auto-approve'); - service.dismissTip(); - } - }); function findTipById(service: ChatTipService, tipId: string, ckService: MockContextKeyServiceWithRulesMatching = contextKeyService): IChatTip | undefined { for (let i = 0; i < 100; i++) { @@ -1371,7 +1320,6 @@ suite('ChatTipService', () => { } for (const { tipId, settingKey } of [ - { tipId: 'tip.yoloMode', settingKey: ChatConfiguration.GlobalAutoApprove }, { tipId: 'tip.thinkingPhrases', settingKey: 'chat.agent.thinking.phrases' }, { tipId: 'tip.agenticBrowser', settingKey: 'workbench.browser.enableChatTools' }, ]) { @@ -1397,7 +1345,6 @@ suite('ChatTipService', () => { } for (const tipId of [ - 'tip.yoloMode', 'tip.thinkingPhrases', 'tip.agenticBrowser', ]) { From 655fe3f4c11f063e3755d5653a9485774e9bb824 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:47:28 +0000 Subject: [PATCH 254/448] Initial plan From 002eadd84c81c5f9dd4579c70e2159de0179de41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:52:35 +0000 Subject: [PATCH 255/448] fix: correct deprecated TypeScript format.enable setting name in tooltip Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- extensions/typescript-language-features/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 40c4081de54..28b65dc0736 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -64,7 +64,7 @@ "format.semicolons.remove": "Remove unnecessary semicolons.", "format.indentSwitchCase": "Indent case clauses in switch statements. Requires using TypeScript 5.1+ in the workspace.", "format.enable": "Enable/disable the default JavaScript and TypeScript formatter.", - "configuration.format.enable.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.enable#` instead.", + "configuration.format.enable.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.enabled#` instead.", "configuration.format.insertSpaceAfterCommaDelimiter.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.insertSpaceAfterCommaDelimiter#` instead.", "configuration.format.insertSpaceAfterConstructor.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.insertSpaceAfterConstructor#` instead.", "configuration.format.insertSpaceAfterSemicolonInForStatements.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.insertSpaceAfterSemicolonInForStatements#` instead.", From 2252c1911d8c3c58de9c69a2024cdb89504cd398 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 5 Mar 2026 16:57:33 -0500 Subject: [PATCH 256/448] fix close button positioning (#299611) fixes #299579 --- .../chatQuestionCarouselPart.ts | 13 +++------ .../media/chatQuestionCarousel.css | 28 ------------------- .../chatQuestionCarouselPart.test.ts | 17 +++++++++++ 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 47d517ef19a..b47b64499f4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -152,12 +152,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._skipAllButton = skipAllButton; } - const isSingleQuestion = this.carousel.questions.length === 1; - - if (!isSingleQuestion && this._closeButtonContainer) { - this.domNode.insertBefore(this._closeButtonContainer, this._questionContainer!); - } - // Register event listeners if (this._skipAllButton) { interactiveStore.add(this._skipAllButton.onDidClick(() => this.ignore())); @@ -578,9 +572,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent headerRow.appendChild(titleRow); - // For single-question carousels, add close button inside the title row - const isSingleQuestion = this.carousel.questions.length === 1; - if (isSingleQuestion && this._closeButtonContainer) { + // Always keep the close button in the title row so it does not overlap content. + if (this._closeButtonContainer) { titleRow.appendChild(this._closeButtonContainer); } @@ -600,6 +593,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollableNode.classList.add('chat-question-input-scrollable'); this._questionContainer.appendChild(inputScrollableNode); + const isSingleQuestion = this.carousel.questions.length === 1; + // Render footer before first layout so the scrollable area is measured against // its final available height and does not visibly resize twice. if (!isSingleQuestion) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 59814a560f8..477ce894745 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -120,11 +120,6 @@ } } -/* Extra right padding when close button is absolutely positioned (multi-question) */ -.interactive-session .chat-question-carousel-container:has(> .chat-question-close-container) .chat-question-title-row { - padding-right: 36px; -} - /* questions list and freeform area */ .interactive-session .chat-question-carousel-container .chat-question-input-container { display: flex; @@ -320,29 +315,6 @@ overscroll-behavior: contain; } -/* close button for multi-question carousels (positioned top-right) */ -.interactive-session .chat-question-carousel-container > .chat-question-close-container { - position: absolute; - top: 6px; - right: 8px; - z-index: 1; - - .monaco-button.chat-question-close { - min-width: 22px; - width: 22px; - height: 22px; - padding: 0; - border: none !important; - box-shadow: none !important; - background: transparent !important; - color: var(--vscode-icon-foreground) !important; - } - - .monaco-button.chat-question-close:hover:not(.disabled) { - background: var(--vscode-toolbar-hoverBackground) !important; - } -} - /* footer with nav arrows, step indicator, and submit */ .interactive-session .chat-question-carousel-container .chat-question-footer-row { display: flex; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 919df6c4893..8b3dd5551d7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -132,6 +132,23 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(stepIndicator?.textContent?.includes('1')); assert.ok(stepIndicator?.textContent?.includes('3')); }); + + test('renders close button in title row for multi-question carousels', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } + ], true); + createWidget(carousel); + + const titleRow = widget.domNode.querySelector('.chat-question-title-row'); + assert.ok(titleRow, 'title row should exist'); + + const closeContainer = titleRow?.querySelector('.chat-question-close-container'); + assert.ok(closeContainer, 'close button container should be rendered in the title row'); + + const directChildCloseContainer = widget.domNode.querySelector(':scope > .chat-question-close-container'); + assert.strictEqual(directChildCloseContainer, null, 'close button container should not be positioned as a direct child of the carousel container'); + }); }); suite('Question Types', () => { From 7165ef63e3b440bfeb388fbebf8e239c6e633c9d Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:37 -0800 Subject: [PATCH 257/448] Use older zip.js --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85a5fbe2605..eadb4002ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4475,9 +4475,9 @@ "license": "Apache-2.0" }, "node_modules/@zip.js/zip.js": { - "version": "2.8.23", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.23.tgz", - "integrity": "sha512-RB+RLnxPJFPrGvQ9rgO+4JOcsob6lD32OcF0QE0yg24oeW9q8KnTTNlugcDaIveEcCbclobJcZP+fLQ++sH0bw==", + "version": "2.8.21", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.21.tgz", + "integrity": "sha512-fkyzXISE3IMrstDO1AgPkJCx14MYHP/suIGiAovEYEuBjq3mffsuL6aMV7ohOSjW4rXtuACuUfpA3GtITgdtYg==", "dev": true, "license": "BSD-3-Clause", "engines": { From 8a6956e503b7dc3c146c47c92f6abfa6dc023588 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Thu, 5 Mar 2026 14:58:50 -0800 Subject: [PATCH 258/448] steer uses up arrow (same as send default) --- .../contrib/chat/browser/actions/chatQueueActions.ts | 4 ++-- .../browser/widget/input/chatQueuePickerActionItem.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index 6606748d653..0ad56d59669 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -87,7 +87,7 @@ export class ChatSteerWithMessageAction extends Action2 { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), tooltip: localize('chat.steerWithMessage.tooltip', "Send this message at the next opportunity, signaling the current request to yield"), - icon: Codicon.arrowRight, + icon: Codicon.arrowUp, f1: false, category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( @@ -271,7 +271,7 @@ export function registerChatQueueActions(): void { order: 1, }); MenuRegistry.appendMenuItem(MenuId.ChatExecuteQueue, { - command: { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), icon: Codicon.arrowRight }, + command: { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), icon: Codicon.arrowUp }, group: 'navigation', order: 2, }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts index 51c0ad382e5..d9beb3af8bb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts @@ -62,7 +62,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { this._primaryActionAction = this._register(new Action( 'chat.queuePickerPrimary', isSteerDefault ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"), - ThemeIcon.asClassName(isSteerDefault ? Codicon.arrowRight : Codicon.add), + ThemeIcon.asClassName(isSteerDefault ? Codicon.arrowUp : Codicon.add), !!contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key), () => this._runDefaultAction() )); @@ -116,7 +116,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { this._primaryActionAction.label = isSteer ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"); - this._primaryActionAction.class = ThemeIcon.asClassName(isSteer ? Codicon.arrowRight : Codicon.add); + this._primaryActionAction.class = ThemeIcon.asClassName(isSteer ? Codicon.arrowUp : Codicon.add); } private _runDefaultAction(): void { @@ -198,7 +198,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { tooltip: '', enabled: true, checked: isSteerDefault, - icon: Codicon.arrowRight, + icon: Codicon.arrowUp, class: undefined, hover: { content: localize('chat.steerWithMessage.hover', "Send this message at the next opportunity, signaling the current request to yield. The current response will stop and the new message will be sent immediately."), @@ -213,7 +213,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { label: localize('chat.sendImmediately', "Stop and Send"), tooltip: '', enabled: true, - icon: Codicon.arrowUp, + icon: Codicon.arrowRight, class: undefined, hover: { content: localize('chat.sendImmediately.hover', "Cancel the current request and send this message immediately."), From b24b147db46d9327251bf7454d1dfb321dd5301a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 5 Mar 2026 18:23:53 -0500 Subject: [PATCH 259/448] ensure slash command usage is tracked and causes tip to hide (#299596) fixes #299443 --- .../contrib/chat/browser/chatTipService.ts | 51 ++++++++- .../contrib/chat/browser/widget/chatWidget.ts | 4 + .../chat/test/browser/chatTipService.test.ts | 106 +++++++++++++++++- 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 7dcf497e4fe..fc596d0a732 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -20,7 +20,7 @@ import { IChatService } from '../common/chatService/chatService.js'; import { CreateSlashCommandsUsageTracker } from './createSlashCommandsUsageTracker.js'; import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, IParsedChatRequest } from '../common/requestParser/chatParserTypes.js'; +import { ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, IParsedChatRequest } from '../common/requestParser/chatParserTypes.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { TipEligibilityTracker } from './chatTipEligibilityTracker.js'; import { ChatTipTier, extractCommandIds, ITipBuildContext, ITipDefinition, TIP_CATALOG } from './chatTipCatalog.js'; @@ -146,6 +146,12 @@ export interface IChatTipService { */ hasMultipleTips(): boolean; + /** + * Records usage of a slash command to update tip eligibility for flows where + * the slash command text is transformed before request submission. + */ + recordSlashCommandUsage(command: string): void; + /** * Clears all dismissed tips so they can be shown again. */ @@ -230,6 +236,8 @@ export class ChatTipService extends Disposable implements IChatTipService { if (slashCommandTrackingId) { this._tracker.recordCommandExecuted(slashCommandTrackingId); } + + this._hideShownTipIfNowIneligible(); })); this._thinkingPhrasesEverModified = this._storageService.getBoolean(ChatTipStorageKeys.ThinkingPhrasesEverModified, StorageScope.APPLICATION, false); @@ -264,10 +272,15 @@ export class ChatTipService extends Disposable implements IChatTipService { const slashCommand = (part as ChatRequestSlashCommandPart).slashCommand.command; return this._toSlashCommandTrackingId(slashCommand); } + + if (part.kind === ChatRequestAgentSubcommandPart.Kind) { + const subCommand = (part as ChatRequestAgentSubcommandPart).command.name; + return this._toSlashCommandTrackingId(subCommand); + } } const trimmed = message.text.trimStart(); - const match = /^\/(init|create-(?:instructions|prompt|agent|skill)|fork)(?:\s|$)/.exec(trimmed); + const match = /^(?:@\S+\s+)?\/(init|create-(?:instructions|prompt|agent|skill)|fork)(?:\s|$)/.exec(trimmed); return match ? this._toSlashCommandTrackingId(match[1]) : undefined; } @@ -289,6 +302,16 @@ export class ChatTipService extends Disposable implements IChatTipService { } } + recordSlashCommandUsage(command: string): void { + const trackingId = this._toSlashCommandTrackingId(command); + if (!trackingId) { + return; + } + + this._tracker.recordCommandExecuted(trackingId); + this._hideShownTipIfNowIneligible(); + } + resetSession(): void { this._shownTip = undefined; this._tipRequestId = undefined; @@ -445,6 +468,11 @@ export class ChatTipService extends Disposable implements IChatTipService { } if (!this._isEligible(this._shownTip, contextKeyService)) { + if (this._tracker.isExcluded(this._shownTip)) { + this.hideTip(); + return undefined; + } + const nextTip = this._findNextEligibleTip(this._shownTip.id, contextKeyService); if (nextTip) { this._shownTip = nextTip; @@ -453,6 +481,9 @@ export class ChatTipService extends Disposable implements IChatTipService { this._onDidNavigateTip.fire(tip); return tip; } + + this.hideTip(); + return undefined; } return this._createTip(this._shownTip); } @@ -481,6 +512,22 @@ export class ChatTipService extends Disposable implements IChatTipService { return undefined; } + private _hideShownTipIfNowIneligible(): void { + if (!this._shownTip || !this._contextKeyService) { + return; + } + + if (this._tipsHiddenForSession) { + return; + } + + if (this._isEligible(this._shownTip, this._contextKeyService)) { + return; + } + + this.hideTip(); + } + private _pickTip(sourceId: string, contextKeyService: IContextKeyService): IChatTip | undefined { this._createSlashCommandsUsageTracker.syncContextKey(contextKeyService); // Record the current mode for future eligibility decisions. diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index eb97584dd41..a26b7b4efb6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2155,6 +2155,10 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } + // Prompt slash commands are transformed out of the input before sendRequest. + // Track them now so tip exclusions still update for commands like /init. + this.chatTipService.recordSlashCommandUsage(agentSlashPromptPart.name); + // need to resolve the slash command to get the prompt file const slashCommand = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.name, CancellationToken.None); if (!slashCommand) { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 447a1bac044..66009e7549b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -25,7 +25,7 @@ import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { MockLanguageModelToolsService } from '../common/tools/mockLanguageModelToolsService.js'; -import { ChatTipTier } from '../../browser/chatTipCatalog.js'; +import { ChatTipTier, TIP_CATALOG } from '../../browser/chatTipCatalog.js'; import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { TestChatEntitlementService } from '../../../../test/common/workbenchTestServices.js'; import { IChatService } from '../../common/chatService/chatService.js'; @@ -220,6 +220,110 @@ suite('ChatTipService', () => { assert.ok(!executedCommands.includes(FORK_CONVERSATION_TRACKING_COMMAND)); }); + + test('hides shown slash tip after submitted slash command without clicking tip link', () => { + const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>()); + instantiationService.stub(IChatService, { + onDidSubmitRequest: submitRequestEmitter.event, + getSession: () => undefined, + } as Partial as IChatService); + + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType); + + let tip = service.getWelcomeTip(contextKeyService); + assert.ok(tip); + + for (let i = 0; i < TIP_CATALOG.length && tip?.id !== 'tip.init'; i++) { + tip = service.navigateToNextTip(); + } + + assert.ok(tip); + assert.strictEqual(tip.id, 'tip.init', 'Expected to navigate to the init tip before submitting /init'); + + let didHide = false; + testDisposables.add(service.onDidHideTip(() => didHide = true)); + + submitRequestEmitter.fire({ + chatSessionResource: URI.parse('chat:session-advance-init'), + message: { + text: '/init', + parts: [], + }, + }); + + assert.ok(didHide, 'Expected slash tip to hide after submitting /init'); + assert.notStrictEqual(service.getWelcomeTip(contextKeyService)?.id, 'tip.init', 'Expected init tip to stay excluded after slash usage'); + }); + + test('removes slash tip from rotation after submitted slash command via eligibility tracking', () => { + const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>()); + instantiationService.stub(IChatService, { + onDidSubmitRequest: submitRequestEmitter.event, + getSession: () => undefined, + } as Partial as IChatService); + + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType); + + let tip = service.getWelcomeTip(contextKeyService); + assert.ok(tip); + + for (let i = 0; i < TIP_CATALOG.length && tip?.id !== 'tip.init'; i++) { + tip = service.navigateToNextTip(); + } + + assert.ok(tip); + assert.strictEqual(tip.id, 'tip.init'); + + submitRequestEmitter.fire({ + chatSessionResource: URI.parse('chat:session-rotate-init'), + message: { + text: '/init', + parts: [], + }, + }); + + for (let i = 0; i < TIP_CATALOG.length; i++) { + tip = service.navigateToNextTip(); + if (!tip) { + break; + } + assert.notStrictEqual(tip.id, 'tip.init', 'Expected init tip to be removed from tip rotation'); + } + + const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[]; + assert.ok(executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND), 'Expected slash usage to be tracked in executed command exclusions'); + }); + + test('removes slash tip from rotation when slash usage is recorded before input transformation', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType); + + let tip = service.getWelcomeTip(contextKeyService); + assert.ok(tip); + + for (let i = 0; i < TIP_CATALOG.length && tip?.id !== 'tip.init'; i++) { + tip = service.navigateToNextTip(); + } + + assert.ok(tip); + assert.strictEqual(tip.id, 'tip.init'); + + service.recordSlashCommandUsage('init'); + + for (let i = 0; i < TIP_CATALOG.length; i++) { + tip = service.navigateToNextTip(); + if (!tip) { + break; + } + assert.notStrictEqual(tip.id, 'tip.init', 'Expected init tip to be removed from tip rotation'); + } + + const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[]; + assert.ok(executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND), 'Expected slash usage to be tracked in executed command exclusions'); + }); + test('records fork tip usage for submitted /fork command', () => { const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>()); instantiationService.stub(IChatService, { From 8d20dd52102f43494efd38e51dc8515b01e80127 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 5 Mar 2026 18:25:42 -0500 Subject: [PATCH 260/448] Fix model swap (#299621) --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 77c5bc149f5..4ce0b157087 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1946,11 +1946,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (e.currentSessionResource && newSessionType !== this._currentSessionType) { this._currentSessionType = newSessionType; this.initSelectedModel(); + this.checkModelInSessionPool(); } - // Validate that the current model belongs to the new session's pool - this.checkModelInSessionPool(); - // For contributed sessions with history, pre-select the model // from the last request so the user resumes with the same model. this.preselectModelFromSessionHistory(); From 868871202bd9c1e3002bdc969d3ee25021dc7bea Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:19:22 -0800 Subject: [PATCH 261/448] fix terminal autopilot + some description language (#299562) * fix terminal autopilot + some description language * fix ci * use last focused widget: * autoreply in autopilot * better wording around task complete tool + auto reply for ask questions --- .../actionWidget/browser/actionWidget.css | 2 +- .../tools/languageModelToolsService.ts | 37 ++++- .../chat/browser/widget/chatListRenderer.ts | 19 ++- .../browser/widget/input/chatInputPart.ts | 5 +- .../input/permissionPickerActionItem.ts | 4 +- .../tools/builtinTools/taskCompleteTool.ts | 6 +- .../tools/languageModelToolsService.test.ts | 144 +++++++++++++++++- .../browser/tools/monitoring/outputMonitor.ts | 41 ++++- .../browser/tools/runInTerminalTool.ts | 32 +++- .../runInTerminalTool.test.ts | 3 +- 10 files changed, 270 insertions(+), 23 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 3c2022bd29d..31c753580b2 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -232,7 +232,7 @@ display: block !important; width: 100%; margin-left: 0; - padding-left: 18px; + padding-left: 20px; font-size: 11px; line-height: 14px; opacity: 0.8; diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index d9bc284672e..27b43d79cc7 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -49,6 +49,7 @@ import { HookType } from '../../common/promptSyntax/hookTypes.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, IExternalPreToolUseHookResult, ILanguageModelToolsService, IPreparedToolInvocation, isToolSet, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolResultInputOutputDetails, IToolSet, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolInvocationPresentation, toolMatchesModel, ToolSet, ToolSetForModel, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; +import { IChatWidgetService } from '../chat.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -129,6 +130,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo @IStorageService private readonly _storageService: IStorageService, @ILanguageModelToolsConfirmationService private readonly _confirmationService: ILanguageModelToolsConfirmationService, @ICommandService private readonly _commandService: ICommandService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { super(); @@ -586,7 +588,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo dto.toolSpecificData = toolInvocation?.toolSpecificData; if (preparedInvocation?.confirmationMessages?.title) { if (!IChatToolInvocation.executionConfirmedOrDenied(toolInvocation) && !autoConfirmed) { - this.playAccessibilitySignal([toolInvocation]); + this.playAccessibilitySignal([toolInvocation], dto.context?.sessionResource); } const userConfirmed = await IChatToolInvocation.awaitConfirmation(toolInvocation, token); if (userConfirmed.type === ToolConfirmKind.Denied) { @@ -943,12 +945,21 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } - private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void { + private playAccessibilitySignal(toolInvocations: ChatToolInvocation[], chatSessionResource: URI | undefined): void { const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove); if (autoApproved) { return; } + // Autopilot/auto-approve permission levels auto-approve all tools, skip signal + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource)) { + return; + } + } + // Filter out any tool invocations that have already been confirmed/denied. // This is a defensive check - normally the call site should prevent this, // but tools may be auto-approved through various mechanisms (per-session rules, @@ -1005,6 +1016,17 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return inspected.policyValue === false; } + /** + * Returns true if the session's current (live) permission picker level is auto-approve. + * This checks the widget's current state, not what was stamped on the request, + * so switching to Autopilot mid-session takes effect immediately. + */ + private _isSessionLiveAutoApproveLevel(chatSessionResource: URI): boolean { + const widget = this._chatWidgetService.getWidgetBySessionResource(chatSessionResource) + ?? this._chatWidgetService.lastFocusedWidget; + return !!widget && isAutoApproveLevel(widget.input.currentModeInfo.permissionLevel); + } + private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined { if (toolData.id === 'vscode_fetchWebPage_internal') { return 'fetch'; @@ -1057,10 +1079,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Auto-Approve All permission level bypasses all tool confirmations, // unless enterprise policy has explicitly disabled global auto-approve. - if (chatSessionResource) { + // Check both the request-stamped level AND the live picker level so that + // switching to Autopilot mid-session takes effect immediately. + if (chatSessionResource && !this._isAutoApprovePolicyRestricted()) { const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); - if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } @@ -1099,10 +1123,11 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { // Auto-Approve All permission level bypasses all post-execution confirmations, // unless enterprise policy has explicitly disabled global auto-approve. - if (chatSessionResource) { + // Check both the request-stamped level AND the live picker level. + if (chatSessionResource && !this._isAutoApprovePolicyRestricted()) { const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); - if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index efd1ff467d6..ef9919d8f84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -2336,7 +2336,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { // always autoreply in autopilot mode. - const isAutopilot = isResponseVM(context.element) && context.element.model.request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot; + const isAutopilot = this._isAutopilotForContext(context); if (!shouldAutoReply && !isAutopilot) { // Roll back the in-progress mark if auto-reply is not enabled. if (stableKey) { @@ -2365,6 +2365,23 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer(ChatConfiguration.GlobalAutoApprove)) { + // or the current permission level is already auto-approve/autopilot + if (chatSessionIsEmpty && !this.configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && !isAutoApproveLevel(this._currentPermissionLevel.get())) { this._currentPermissionLevel.set(ChatPermissionLevel.Default, undefined); this.permissionLevelKey.set(ChatPermissionLevel.Default); this.permissionWidget?.refresh(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 26f6954832c..46161115022 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -135,7 +135,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.autopilot', label: localize('permissions.autopilot', "Autopilot (Preview)"), - description: localize('permissions.autopilot.subtext', "Copilot handles it from start to finish"), + description: localize('permissions.autopilot.subtext', "Autonomously iterates from start to finish"), icon: ThemeIcon.fromId(Codicon.rocket.id), checked: currentLevel === ChatPermissionLevel.Autopilot, enabled: !policyRestricted, @@ -187,7 +187,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { super(action, { actionProvider, reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, - listOptions: { descriptionBelow: true, minWidth: 232 }, + listOptions: { descriptionBelow: true, minWidth: 255 }, }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts index 74879d30d21..15094b0ce1a 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -21,6 +21,8 @@ export const AUTOPILOT_CONTINUATION_MESSAGE = '- You have open questions or ambiguities — make good decisions and keep working\n' + '- You encountered an error — try to resolve it or find an alternative approach\n' + '- There are remaining steps — complete them first\n\n' + + 'When you ARE done, first provide a brief text summary of what was accomplished, then call task_complete. ' + + 'Both the summary message and the tool call are required.\n\n' + 'Keep working autonomously until the task is truly finished, then call task_complete.'; export const TaskCompleteToolData: IToolData = { @@ -29,8 +31,10 @@ export const TaskCompleteToolData: IToolData = { modelDescription: 'Signal that the user\'s task is fully done. You MUST call this tool when your work is complete — ' + 'whether you made code changes, answered a question, or completed any other kind of task. ' + - 'Provide a brief summary of what was accomplished. If the summary is trivial (e.g. answering a question), omit it. ' + + 'Provide a brief summary of what was accomplished. ' + 'Do not restate the summary in your message text — it is shown to the user directly.\n\n' + + 'IMPORTANT: Before calling this tool, you MUST output a brief text message summarizing what was done. ' + + 'The task is not complete until both your summary message AND this tool call are present.\n\n' + 'When to call:\n' + '- After answering the user\'s question or completing a conversational request\n' + '- After you have completed ALL requested changes\n' + diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index f8ee29ce85f..87928cde0e3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -24,7 +24,7 @@ import { workbenchInstantiationService } from '../../../../../test/browser/workb import { LanguageModelToolsService } from '../../../browser/tools/languageModelToolsService.js'; import { ChatModel, IChatModel } from '../../../common/model/chatModel.js'; import { IChatService, IChatToolInputInvocationData, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; +import { ChatConfiguration, ChatPermissionLevel } from '../../../common/constants.js'; import { SpecedToolAliases, isToolResultInputOutputDetails, IToolData, IToolImpl, IToolInvocation, ToolDataSource, ToolSet, IToolResultTextPart } from '../../../common/tools/languageModelToolsService.js'; import { MockChatService } from '../../common/chatService/mockChatService.js'; import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -83,13 +83,13 @@ function registerToolForTest(service: LanguageModelToolsService, store: any, id: }; } -function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any } }): IChatModel { +function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any }; modeInfo?: { permissionLevel?: ChatPermissionLevel } }): IChatModel { const requestId = options?.requestId ?? 'requestId'; const capture = options?.capture; const fakeModel = { sessionId, sessionResource: LocalChatSessionUri.forSession(sessionId), - getRequests: () => [{ id: requestId, modelId: 'test-model' }], + getRequests: () => [{ id: requestId, modelId: 'test-model', modeInfo: options?.modeInfo }], } as ChatModel; chatService.addSession(fakeModel); chatService.appendProgress = (request, progress) => { @@ -1453,6 +1453,144 @@ suite('LanguageModelToolsService', () => { assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'accessibility signal should not be played when auto-approve is enabled'); }); + test('autopilot permission level bypasses global auto-approve check', async () => { + // When autopilot is on, tools should auto-approve without needing global auto-approve enabled + const { service: testService, chatService: testChatService } = createTestToolsService(store, { + configureServices: config => { + config.setUserConfiguration('chat.tools.global.autoApprove', false); // Global OFF + } + }); + + const tool = registerToolForTest(testService, store, 'autopilotTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm?', message: 'Should be auto-approved by autopilot' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'autopilot approved' }] }) + }); + + const sessionId = 'test-autopilot'; + stubGetSession(testChatService, sessionId, { + requestId: 'req1', + modeInfo: { permissionLevel: ChatPermissionLevel.Autopilot }, + }); + + // Tool should be auto-approved even though global auto-approve is off + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'autopilot approved'); + }); + + test('autopilot finds correct request by chatRequestId', async () => { + // When chatRequestId is provided, the exact request should be matched + const { service: testService, chatService: testChatService } = createTestToolsService(store, { + configureServices: config => { + config.setUserConfiguration('chat.tools.global.autoApprove', false); + } + }); + + const tool = registerToolForTest(testService, store, 'autopilotIdTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm?', message: 'Test' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'found by id' }] }) + }); + + const sessionId = 'test-autopilot-id'; + const fakeModel = { + sessionId, + sessionResource: LocalChatSessionUri.forSession(sessionId), + getRequests: () => [ + { id: 'req-old', modelId: 'test-model', modeInfo: undefined }, + { id: 'req-autopilot', modelId: 'test-model', modeInfo: { permissionLevel: ChatPermissionLevel.Autopilot } }, + ], + } as ChatModel; + testChatService.addSession(fakeModel); + + const dto = tool.makeDto({ test: 1 }, { sessionId }); + dto.chatRequestId = 'req-autopilot'; + + const result = await testService.invokeTool(dto, async () => 0, CancellationToken.None); + assert.strictEqual(result.content[0].value, 'found by id'); + }); + + test('autopilot auto-approves terminal tool with confirmation messages', async () => { + // Terminal tools always return confirmationMessages when their own auto-approve is off. + // In autopilot mode, shouldAutoConfirm should still auto-approve the tool. + const { service: testService, chatService: testChatService } = createTestToolsService(store, { + configureServices: config => { + config.setUserConfiguration('chat.tools.global.autoApprove', false); + } + }); + + const tool = registerToolForTest(testService, store, 'terminalTool', { + prepareToolInvocation: async () => ({ + confirmationMessages: { + title: 'Run shell command?', + message: 'echo hello', + }, + toolSpecificData: { + kind: 'terminal' as const, + terminalToolSessionId: 'test', + terminalCommandId: 'cmd-1', + commandLine: { original: 'echo hello' }, + language: 'sh', + }, + }), + invoke: async () => ({ content: [{ kind: 'text', value: 'terminal executed' }] }) + }); + + const sessionId = 'test-autopilot-terminal'; + stubGetSession(testChatService, sessionId, { + requestId: 'req1', + modeInfo: { permissionLevel: ChatPermissionLevel.Autopilot }, + }); + + // Terminal tool should be auto-approved by autopilot even without terminal auto-approve enabled + const result = await testService.invokeTool( + tool.makeDto({ command: 'echo hello', explanation: 'test', goal: 'test', isBackground: false }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'terminal executed'); + }); + + test('bypass approvals auto-approves terminal tool with confirmation messages', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store, { + configureServices: config => { + config.setUserConfiguration('chat.tools.global.autoApprove', false); + } + }); + + const tool = registerToolForTest(testService, store, 'terminalToolBypass', { + prepareToolInvocation: async () => ({ + confirmationMessages: { + title: 'Run shell command?', + message: 'ls -la', + }, + toolSpecificData: { + kind: 'terminal' as const, + terminalToolSessionId: 'test', + terminalCommandId: 'cmd-2', + commandLine: { original: 'ls -la' }, + language: 'sh', + }, + }), + invoke: async () => ({ content: [{ kind: 'text', value: 'bypass executed' }] }) + }); + + const sessionId = 'test-bypass-terminal'; + stubGetSession(testChatService, sessionId, { + requestId: 'req1', + modeInfo: { permissionLevel: ChatPermissionLevel.AutoApprove }, + }); + + const result = await testService.invokeTool( + tool.makeDto({ command: 'ls -la', explanation: 'test', goal: 'test', isBackground: false }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'bypass executed'); + }); + test('shouldAutoConfirm with basic configuration', async () => { // Test basic shouldAutoConfirm behavior with simple configuration const { service: testService, chatService: testChatService } = createTestToolsService(store, { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index eef1a76329b..d0af9560728 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -17,7 +17,7 @@ import { IChatWidgetService } from '../../../../../chat/browser/chat.js'; import { ChatElicitationRequestPart } from '../../../../../chat/common/model/chatProgressTypes/chatElicitationRequestPart.js'; import { ChatModel } from '../../../../../chat/common/model/chatModel.js'; import { ElicitationState, IChatService } from '../../../../../chat/common/chatService/chatService.js'; -import { ChatAgentLocation } from '../../../../../chat/common/constants.js'; +import { ChatAgentLocation, ChatPermissionLevel } from '../../../../../chat/common/constants.js'; import { ChatMessageRole, getTextResponseFromStream, ILanguageModelsService } from '../../../../../chat/common/languageModels.js'; import { IToolInvocationContext } from '../../../../../chat/common/tools/languageModelToolsService.js'; import { ITaskService } from '../../../../../tasks/common/taskService.js'; @@ -108,6 +108,9 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { private readonly _onDidFinishCommand = this._register(new Emitter()); readonly onDidFinishCommand: Event = this._onDidFinishCommand.event; + /** The chat session resource for this tool invocation, used to check permission level. */ + private readonly _sessionResource: URI | undefined; + constructor( private readonly _execution: IExecution, private readonly _pollFn: ((execution: IExecution, token: CancellationToken, taskService: ITaskService) => Promise) | undefined, @@ -124,6 +127,8 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { ) { super(); + this._sessionResource = invocationContext?.sessionResource; + // Start async to ensure listeners are set up timeout(0).then(() => { this._startMonitoring(command, invocationContext, token); @@ -237,7 +242,14 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { // Check for generic "press any key" prompts from scripts. if ((!isTask || !isTaskInactive) && detectsGenericPressAnyKeyPattern(output)) { - this._logService.trace('OutputMonitor: Idle -> generic "press any key" detected, requesting free-form input'); + this._logService.trace('OutputMonitor: Idle -> generic "press any key" detected'); + // In autopilot mode, auto-reply to "press any key" prompts + if (this._isAutopilotMode()) { + this._logService.trace('OutputMonitor: Autopilot mode -> auto-replying to "press any key"'); + await this._execution.instance.sendText('', true); + return { shouldContinuePollling: true }; + } + this._logService.trace('OutputMonitor: Requesting free-form input for "press any key"'); // Register a marker to track this prompt position so we don't re-detect it const currentMarker = this._execution.instance.registerMarker(); if (currentMarker) { @@ -286,7 +298,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { this._cleanupIdleInputListener(); return { shouldContinuePollling: true }; } - const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts); + const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts) || this._isAutopilotMode(); if (autoReply && !this._isSensitivePrompt(confirmationPrompt.prompt)) { const explicitInput = confirmationPrompt.suggestedInput ?? this._extractExplicitInputFromPrompt(confirmationPrompt.prompt); const normalizedInput = this._normalizeAutoReplyInput(explicitInput); @@ -588,6 +600,27 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { return /(password|passphrase|token|api\s*key|secret)/i.test(prompt); } + /** + * Returns true if the current session is in Autopilot mode (not Bypass Approvals). + * In Autopilot, terminal prompts should be auto-replied to so the agent can + * work autonomously from start to finish. + */ + private _isAutopilotMode(): boolean { + if (!this._sessionResource) { + return false; + } + // Check the live widget picker level + const widget = this._chatWidgetService.getWidgetBySessionResource(this._sessionResource) + ?? this._chatWidgetService.lastFocusedWidget; + if (widget?.input.currentModeInfo.permissionLevel === ChatPermissionLevel.Autopilot) { + return true; + } + // Fall back to the request-stamped level + const model = this._chatService.getSession(this._sessionResource); + const request = model?.getRequests().at(-1); + return request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot; + } + private _normalizeAutoReplyInput(input: string | undefined): string | undefined { if (!input) { return undefined; @@ -626,7 +659,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { if (!confirmationPrompt?.options.length) { return undefined; } - const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts); + const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts) || this._isAutopilotMode(); let model = this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)[0]?.input.currentLanguageModel; if (model) { const models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot', family: model.replaceAll('copilot/', '') }); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 93f3b0c40d0..fd8fcd1f37a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -63,7 +63,7 @@ import { IWorkspaceContextService } from '../../../../../../platform/workspace/c import { IHistoryService } from '../../../../../services/history/common/history.js'; import { TerminalCommandArtifactCollector } from './terminalCommandArtifactCollector.js'; import { isNumber, isString } from '../../../../../../base/common/types.js'; -import { ChatConfiguration } from '../../../../chat/common/constants.js'; +import { ChatConfiguration, isAutoApproveLevel } from '../../../../chat/common/constants.js'; import { IChatWidgetService } from '../../../../chat/browser/chat.js'; import { TerminalChatCommandId } from '../../../chat/browser/terminalChat.js'; import { clamp } from '../../../../../../base/common/numbers.js'; @@ -644,8 +644,12 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } + // Check if the session's permission level (Autopilot/Bypass Approvals) auto-approves all tools. + // When active, skip terminal confirmation entirely since the user has opted into full auto-approval. + const isSessionAutoApproved = chatSessionResource && this._isSessionAutoApproveLevel(chatSessionResource); + // If forceConfirmationReason is set, always show confirmation regardless of auto-approval - const shouldShowConfirmation = !isFinalAutoApproved || context.forceConfirmationReason !== undefined; + const shouldShowConfirmation = (!isFinalAutoApproved && !isSessionAutoApproved) || context.forceConfirmationReason !== undefined; const confirmationMessages = shouldShowConfirmation ? { title: confirmationTitle, message: new MarkdownString(localize('runInTerminal.confirmationMessage', "Explanation: {0}\n\nGoal: {1}", args.explanation, args.goal)), @@ -659,6 +663,30 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }; } + /** + * Returns true if the chat session's permission level (Autopilot/Bypass Approvals) + * auto-approves all tool calls, unless enterprise policy restricts it. + * Checks both the request-stamped level and the live picker level. + */ + private _isSessionAutoApproveLevel(chatSessionResource: URI): boolean { + const inspected = this._configurationService.inspect(ChatConfiguration.GlobalAutoApprove); + if (inspected.policyValue === false) { + return false; + } + // Check the live widget picker level (handles mid-session switches). + // Fall back to lastFocusedWidget if the session-specific widget isn't found + // (e.g., widget was backgrounded or URI mismatch). + const widget = this._chatWidgetService.getWidgetBySessionResource(chatSessionResource) + ?? this._chatWidgetService.lastFocusedWidget; + if (widget && isAutoApproveLevel(widget.input.currentModeInfo.permissionLevel)) { + return true; + } + // Fall back to the request-stamped level + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + return isAutoApproveLevel(request?.modeInfo?.permissionLevel); + } + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { const toolSpecificData = invocation.toolSpecificData as IChatTerminalToolInvocationData | undefined; if (!toolSpecificData) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index e362782b44b..48ff9c22554 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -86,7 +86,8 @@ suite('RunInTerminalTool', () => { }, store); instantiationService.stub(IChatService, { - onDidDisposeSession: chatServiceDisposeEmitter.event + onDidDisposeSession: chatServiceDisposeEmitter.event, + getSession: () => undefined, }); instantiationService.stub(ITerminalChatService, store.add(instantiationService.createInstance(TerminalChatService))); instantiationService.stub(IWorkspaceContextService, workspaceContextService); From ea3dfdd3ae7e8f6c221b3a9a2590e62e5cef207b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 5 Mar 2026 16:35:24 -0800 Subject: [PATCH 262/448] mcp: fix sampling model selection to exclude CLI models (#299635) mcp: fix default model selection to exclude CLI models - Changes _findMatchingModel to use a new _getDefaultModels helper instead of filtering for models with ChatAgentLocation.Chat default location - The new _getDefaultModels method filters for free models (no multiplier numeric or target chat session type), preventing CLI-only models from being selected as defaults by default - Also removes unused ChatAgentLocation import Fixes #299336 (Commit message generated by Copilot) --- .../contrib/mcp/common/mcpSamplingService.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts b/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts index a49f79b70dc..363f8a12c5f 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts @@ -17,7 +17,7 @@ import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } fr import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { ChatAgentLocation, ChatConfiguration } from '../../chat/common/constants.js'; +import { ChatConfiguration } from '../../chat/common/constants.js'; import { ChatImageMimeType, ChatMessageRole, IChatMessage, IChatMessagePart, ILanguageModelsService } from '../../chat/common/languageModels.js'; import { McpCommandIds } from './mcpCommandIds.js'; import { IMcpServerSamplingConfiguration, mcpServerSamplingSection } from './mcpConfiguration.js'; @@ -241,11 +241,8 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic return config.allowedOutsideChat === undefined ? ModelMatch.UnsureAllowedOutsideChat : ModelMatch.NotAllowed; } - // 2. Get the configured models, or the default model(s) - const foundModelIdsDeep = config.allowedModels?.filter(m => !!this._languageModelsService.lookupLanguageModel(m)) || this._languageModelsService.getLanguageModelIds().filter(m => this._languageModelsService.lookupLanguageModel(m)?.isDefaultForLocation[ChatAgentLocation.Chat]); - - const foundModelIds = foundModelIdsDeep.flat().sort((a, b) => b.length - a.length); // Sort by length to prefer most specific - + // 2. Get the configured models, or the default free model(s) + const foundModelIds = config.allowedModels?.filter(m => !!this._languageModelsService.lookupLanguageModel(m)) || this._getDefaultModels(); if (!foundModelIds.length) { return ModelMatch.NoMatchingModel; } @@ -261,6 +258,20 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic return foundModelIds[0]; // Return the first matching model } + private _getDefaultModels() { + const candidates = this._languageModelsService.getLanguageModelIds().map(m => { + const model = this._languageModelsService.lookupLanguageModel(m); + return model && !model.multiplierNumeric && !model.targetChatSessionType ? { model, id: m } : undefined; + }).filter(isDefined); + + const someDefault = candidates.findIndex(c => Object.values(c.model.isDefaultForLocation).some(Boolean)); + if (someDefault !== -1) { + [candidates[0], candidates[someDefault]] = [candidates[someDefault], candidates[0]]; + } + + return candidates.map(c => c.id); + } + private _configKey(server: IMcpServer) { return `${server.collection.label}: ${server.definition.label}`; } From 1775c2e2c9aee2e557660072110556d27b57d55d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:40:51 +0000 Subject: [PATCH 263/448] Initial plan From 4743e19d32a7a9184caeb7d090f7c9f06f0da070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:47:05 +0000 Subject: [PATCH 264/448] fix: use vscode codicon as theme icon for Release Notes tab Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- .../contrib/update/browser/media/releasenoteseditor.css | 5 ----- .../workbench/contrib/update/browser/releaseNotesEditor.ts | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css index 4210055bfeb..a4a092d8349 100644 --- a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css +++ b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css @@ -2,8 +2,3 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -.file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before { - content: ' '; - background-image: url('../../../../browser/media/code-icon.svg'); -} diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 550329a85ef..59967d3cd39 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/releasenoteseditor.css'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { escapeMarkdownSyntaxTokens } from '../../../../base/common/htmlContent.js'; import { KeybindingParser } from '../../../../base/common/keybindingParser.js'; @@ -20,6 +20,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { asTextOrError, IRequestService } from '../../../../platform/request/common/request.js'; +import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js'; import { WebviewInput } from '../../webviewPanel/browser/webviewEditorInput.js'; import { IWebviewWorkbenchService } from '../../webviewPanel/browser/webviewWorkbenchService.js'; @@ -39,6 +40,8 @@ import { asWebviewUri } from '../../webview/common/webview.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +const ReleaseNotesEditorIcon = registerIcon('release-notes-view-icon', Codicon.vscode, nls.localize('releaseNotesViewIcon', 'Icon of the release notes editor.')); + export class ReleaseNotesManager extends Disposable { private readonly _simpleSettingRenderer: SimpleSettingRenderer; private readonly _releaseNotesCache = new Map>(); @@ -124,7 +127,7 @@ export class ReleaseNotesManager extends Disposable { }, 'releaseNotes', title, - undefined, + ReleaseNotesEditorIcon, { group: ACTIVE_GROUP, preserveFocus: false }); const disposables = new DisposableStore(); From 3a1f21945b3cf0d01f049057d6e8187ccbc0440c Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:08:57 -0800 Subject: [PATCH 265/448] Chat input: allow narrower sidebar before icon-only collapse, stop hiding tools button (#299644) * chat: delay picker collapse and keep tools in toolbar overflow * rename constant to CHAT_INPUT_PICKER_COLLAPSE_WIDTH per review --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 3 ++- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 11816f29bfd..4f54ec7cfe3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -135,6 +135,7 @@ const INPUT_EDITOR_MAX_HEIGHT = 250; const INPUT_EDITOR_LINE_HEIGHT = 20; const INPUT_EDITOR_PADDING = { compact: { top: 2, bottom: 2 }, default: { top: 12, bottom: 12 } }; const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2'; +const CHAT_INPUT_PICKER_COLLAPSE_WIDTH = 320; export interface IChatInputStyles { overlayBackground: string; @@ -2185,7 +2186,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const pickerOptions: IChatInputPickerOptions = { getOverflowAnchor: () => this.inputActionsToolbar.getElement(), actionContext: { widget }, - hideChevrons: derived(reader => this._stableInputPartWidth.read(reader) < 400), + hideChevrons: derived(reader => this._stableInputPartWidth.read(reader) < CHAT_INPUT_PICKER_COLLAPSE_WIDTH), hoverPosition: { forcePosition: true, hoverPosition: location === ChatWidgetLocation.SidebarRight && !isMaximized ? HoverPosition.LEFT : HoverPosition.RIGHT diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index e152c00e58a..d99f33cca29 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1533,11 +1533,6 @@ have to be updated for changes to the rules above, or to support more deeply nes } -/* Hide the tools button when the toolbar is in collapsed state */ -.interactive-session .chat-input-toolbar:has(.hide-chevrons) .action-item:has(.codicon-settings) { - display: none; -} - /* Add context button icon sizing */ .interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .action-label { display: flex; From 56bb369855258dd75a4e5ada69298fc0d5723518 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:09:19 +0000 Subject: [PATCH 266/448] Initial plan From d004864553a79de680058ec8691a34f044689cce Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 5 Mar 2026 17:13:04 -0800 Subject: [PATCH 267/448] Check workspace trust for hooks (#299638) --- .../promptSyntax/service/promptsService.ts | 3 +- .../service/promptsServiceImpl.ts | 20 +++++ .../computeAutomaticInstructions.test.ts | 5 +- .../service/promptsService.test.ts | 89 ++++++++++++++++++- 4 files changed, 114 insertions(+), 3 deletions(-) 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 3e04c2fed58..6eac3cb02b9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -304,7 +304,8 @@ export type PromptFileSkipReason = | 'parse-error' | 'disabled' | 'all-hooks-disabled' - | 'claude-hooks-disabled'; + | 'claude-hooks-disabled' + | 'workspace-untrusted'; /** * Result of discovering a single prompt file. 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 63f3e0b0e41..9599b40394a 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 { ChatRequestHooks, IHookCommand, parseSubagentHooksFromYaml } from '../h import { HookType } from '../hookTypes.js'; import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptFileAttributes.js'; import { StopWatch } from '../../../../../../base/common/stopwatch.js'; @@ -173,6 +174,7 @@ export class PromptsService extends Disposable implements IPromptsService { @IPathService private readonly pathService: IPathService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, ) { super(); @@ -223,6 +225,7 @@ export class PromptsService extends Disposable implements IPromptsService { this.getFileLocatorEvent(PromptsType.hook), Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CHAT_HOOKS) || e.affectsConfiguration(PromptsConfig.USE_CLAUDE_HOOKS)), this._onDidPluginHooksChange.event, + this.workspaceTrustService.onDidChangeTrust, ) )); @@ -1245,6 +1248,10 @@ export class PromptsService extends Disposable implements IPromptsService { return undefined; } + if (!this.workspaceTrustService.isWorkspaceTrusted()) { + return undefined; + } + const useClaudeHooks = this.configurationService.getValue(PromptsConfig.USE_CLAUDE_HOOKS); const hookFiles = await this.listPromptFiles(PromptsType.hook, token); @@ -1630,6 +1637,19 @@ export class PromptsService extends Disposable implements IPromptsService { const extensionId = promptPath.extension?.identifier?.value; const name = basename(uri); + // Ignored if workspace is untrusted + if (!this.workspaceTrustService.isWorkspaceTrusted()) { + files.push({ + uri: promptPath.uri, + storage: promptPath.storage, + status: 'skipped', + skipReason: 'workspace-untrusted', + name: basename(promptPath.uri), + extensionId: promptPath.extension?.identifier?.value, + }); + continue; + } + // Skip Claude hooks when the setting is disabled if (getHookSourceFormat(uri) === HookSourceFormat.Claude && useClaudeHooks === false) { files.push({ 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 8a0dcd1bef3..e4a970b5271 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 @@ -24,9 +24,10 @@ import { ILogService, NullLogService } from '../../../../../../platform/log/comm import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { NullTelemetryService } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js'; import { testWorkspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; -import { TestContextService, TestUserDataProfileService } from '../../../../../test/common/workbenchTestServices.js'; +import { TestContextService, TestUserDataProfileService, TestWorkspaceTrustManagementService } from '../../../../../test/common/workbenchTestServices.js'; import { ChatRequestVariableSet, isPromptFileVariableEntry, isPromptTextVariableEntry, toFileVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { ComputeAutomaticInstructions, getFilePath, InstructionsCollectionEvent } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; @@ -181,6 +182,8 @@ suite('ComputeAutomaticInstructions', () => { instaService.stub(IContextKeyService, new MockContextKeyService()); + instaService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); + instaService.stub(IAgentPluginService, { plugins: observableValue('testPlugins', []), allPlugins: observableValue('testAllPlugins', []), 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 a5e0efdd72b..2889362b11f 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 @@ -35,7 +35,7 @@ import { IWorkbenchEnvironmentService } from '../../../../../../services/environ import { IFilesConfigurationService } from '../../../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { IUserDataProfileService } from '../../../../../../services/userDataProfile/common/userDataProfile.js'; import { toUserDataProfile } from '../../../../../../../platform/userDataProfile/common/userDataProfile.js'; -import { TestContextService, TestUserDataProfileService } from '../../../../../../test/common/workbenchTestServices.js'; +import { TestContextService, TestUserDataProfileService, TestWorkspaceTrustManagementService } from '../../../../../../test/common/workbenchTestServices.js'; import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '../../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; @@ -54,6 +54,7 @@ import { HookType } from '../../../../common/promptSyntax/hookTypes.js'; import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js'; +import { IWorkspaceTrustManagementService } from '../../../../../../../platform/workspace/common/workspaceTrust.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -65,6 +66,7 @@ suite('PromptsService', () => { let fileService: IFileService; let testPluginsObservable: ISettableObservable; let testAllPluginsObservable: ISettableObservable; + let workspaceTrustService: TestWorkspaceTrustManagementService; setup(async () => { instaService = disposables.add(new TestInstantiationService()); @@ -166,6 +168,9 @@ suite('PromptsService', () => { instaService.stub(IContextKeyService, new MockContextKeyService()); + workspaceTrustService = disposables.add(new TestWorkspaceTrustManagementService()); + instaService.stub(IWorkspaceTrustManagementService, workspaceTrustService); + testPluginsObservable = observableValue('testPlugins', []); testAllPluginsObservable = observableValue('testAllPlugins', []); @@ -3633,5 +3638,87 @@ suite('PromptsService', () => { assert.ok(after, 'Expected hooks result after plugin update'); assert.deepStrictEqual(after.hooks[HookType.PreToolUse], [{ type: 'command', command: 'echo after' }]); }); + + test('returns undefined when workspace is untrusted', async function () { + workspaceContextService.setWorkspace(testWorkspace(URI.file('/test-workspace'))); + testConfigService.setUserConfiguration(PromptsConfig.USE_CHAT_HOOKS, true); + testConfigService.setUserConfiguration(PromptsConfig.HOOKS_LOCATION_KEY, { [HOOKS_SOURCE_FOLDER]: true }); + + await mockFiles(fileService, [ + { + path: '/test-workspace/.github/hooks/my-hook.json', + contents: [ + JSON.stringify({ + hooks: { + [HookType.PreToolUse]: [ + { type: 'command', command: 'echo test' }, + ], + }, + }), + ], + }, + ]); + + // Trusted workspace should return hooks + const trustedResult = await service.getHooks(CancellationToken.None); + assert.ok(trustedResult, 'Expected hooks when workspace is trusted'); + assert.strictEqual(trustedResult.hooks[HookType.PreToolUse]?.length, 1); + + // Untrusted workspace should return undefined + await workspaceTrustService.setWorkspaceTrust(false); + const untrustedResult = await service.getHooks(CancellationToken.None); + assert.strictEqual(untrustedResult, undefined, 'Expected undefined hooks when workspace is untrusted'); + + // Re-trusting should return hooks again + await workspaceTrustService.setWorkspaceTrust(true); + const reTrustedResult = await service.getHooks(CancellationToken.None); + assert.ok(reTrustedResult, 'Expected hooks after workspace becomes trusted again'); + assert.strictEqual(reTrustedResult.hooks[HookType.PreToolUse]?.length, 1); + }); + + test('discovery info marks hooks as skipped when workspace is untrusted', async function () { + workspaceContextService.setWorkspace(testWorkspace(URI.file('/test-workspace'))); + testConfigService.setUserConfiguration(PromptsConfig.USE_CHAT_HOOKS, true); + testConfigService.setUserConfiguration(PromptsConfig.HOOKS_LOCATION_KEY, { [HOOKS_SOURCE_FOLDER]: true }); + + await mockFiles(fileService, [ + { + path: '/test-workspace/.github/hooks/my-hook.json', + contents: [ + JSON.stringify({ + hooks: { + [HookType.PreToolUse]: [ + { type: 'command', command: 'echo test' }, + ], + }, + }), + ], + }, + ]); + + await workspaceTrustService.setWorkspaceTrust(false); + const discoveryInfo = await service.getPromptDiscoveryInfo(PromptsType.hook, CancellationToken.None); + assert.strictEqual(discoveryInfo.files.length, 1, 'Expected one discovery result'); + assert.strictEqual(discoveryInfo.files[0].status, 'skipped'); + assert.strictEqual(discoveryInfo.files[0].skipReason, 'workspace-untrusted'); + }); + + test('suppresses plugin hooks when workspace is untrusted', async function () { + testConfigService.setUserConfiguration(PromptsConfig.USE_CHAT_HOOKS, true); + testConfigService.setUserConfiguration(PromptsConfig.HOOKS_LOCATION_KEY, {}); + + const { plugin } = createTestPlugin('/plugins/test-plugin', [{ + type: HookType.PreToolUse, + originalId: 'plugin-pre-tool-use', + hooks: [{ type: 'command', command: 'echo from-plugin' }], + }]); + + testPluginsObservable.set([plugin], undefined); + testAllPluginsObservable.set([plugin], undefined); + + await workspaceTrustService.setWorkspaceTrust(false); + const result = await service.getHooks(CancellationToken.None); + assert.strictEqual(result, undefined, 'Expected undefined hooks when workspace is untrusted, even with plugin hooks'); + }); }); }); From d8bad4f1d4385d3cbb16f18b9c0a6a68b7316038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:17:56 +0000 Subject: [PATCH 268/448] Fix file:// links showing ugly URL-encoded hover tooltip in markdown renderer Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- src/vs/base/browser/markdownRenderer.ts | 25 +++++++++++++++++ .../test/browser/markdownRenderer.test.ts | 27 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index e3f20d96726..52f99538b11 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -66,6 +66,25 @@ export interface MarkdownSanitizerConfig { readonly remoteImageIsAllowed?: (uri: URI) => boolean; } +/** + * Returns a human-readable tooltip string for a link href. + * For file:// URIs, converts to a decoded OS file system path to avoid + * showing raw URL-encoded paths (e.g. "C:\Users\..." instead of "file:///c%3A/Users/..."). + */ +function getLinkTitle(href: string): string { + try { + const parsed = URI.parse(href); + if (parsed.scheme === Schemas.file) { + const path = parsed.fsPath; + const fragment = parsed.fragment; + return escapeDoubleQuotes(fragment ? `${path}#${fragment}` : path); + } + } catch { + // fall through + } + return ''; +} + const defaultMarkedRenderers = Object.freeze({ image: ({ href, title, text }: marked.Tokens.Image): string => { let dimensions: string[] = []; @@ -104,6 +123,12 @@ const defaultMarkedRenderers = Object.freeze({ title = typeof title === 'string' ? escapeDoubleQuotes(removeMarkdownEscapes(title)) : ''; href = removeMarkdownEscapes(href); + // For file:// URIs without an explicit title, show the decoded OS path instead of + // the raw URL-encoded URI (e.g. display "C:\Users\..." instead of "file:///c%3A/Users/...") + if (!title && href.startsWith(`${Schemas.file}:`)) { + title = getLinkTitle(href); + } + // HTML Encode href href = href.replace(/&/g, '&') .replace(/ { assert.strictEqual(result.innerHTML, `

text bar

`); }); + test('Should use decoded file path as title for file:// links', () => { + const md = new MarkdownString(`[log](file:///home/user/project/lib.d.ts)`, {}); + + const result = store.add(renderMarkdown(md)).element; + const anchor = result.querySelector('a')!; + assert.ok(anchor); + assert.strictEqual(anchor.title, '/home/user/project/lib.d.ts'); + }); + + test('Should include fragment in title for file:// links with line numbers', () => { + const md = new MarkdownString(`[log](file:///home/user/project/lib.d.ts#L42)`, {}); + + const result = store.add(renderMarkdown(md)).element; + const anchor = result.querySelector('a')!; + assert.ok(anchor); + assert.strictEqual(anchor.title, '/home/user/project/lib.d.ts#L42'); + }); + + test('Should not override explicit title for file:// links', () => { + const md = new MarkdownString(`[log](file:///home/user/project/lib.d.ts "Go to definition")`, {}); + + const result = store.add(renderMarkdown(md)).element; + const anchor = result.querySelector('a')!; + assert.ok(anchor); + assert.strictEqual(anchor.title, 'Go to definition'); + }); + suite('PlaintextMarkdownRender', () => { test('test code, blockquote, heading, list, listitem, paragraph, table, tablerow, tablecell, strong, em, br, del, text are rendered plaintext', () => { From 295e194e0e323f50ec99b787390cf7219709781f Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:18:27 -0800 Subject: [PATCH 269/448] Dileep y/299224 (#299656) * code changes * updating tmp folder based on OS * fixing the bug when file system setting is empty --- .../chatAgentTools/common/terminalSandboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts index 3ff42709b51..0fff7114eb0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalSandboxService.ts @@ -165,8 +165,8 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb : {}; const configFileUri = URI.joinPath(this._tempDir, `vscode-sandbox-settings-${this._sandboxSettingsId}.json`); const defaultAllowWrite = [...this._defaultWritePaths]; - const linuxAllowWrite = [...new Set([...defaultAllowWrite, ...(linuxFileSystemSetting.allowWrite ?? [])])]; - const macAllowWrite = [...new Set([...defaultAllowWrite, ...(macFileSystemSetting.allowWrite ?? [])])]; + const linuxAllowWrite = [...new Set([...(linuxFileSystemSetting.allowWrite ?? []), ...defaultAllowWrite])]; + const macAllowWrite = [...new Set([...(macFileSystemSetting.allowWrite ?? []), ...defaultAllowWrite])]; let allowedDomains = networkSetting.allowedDomains ?? []; if (networkSetting.allowTrustedDomains) { From 199e9f97594f3c5ff90fbcd1e8b8c2025583499f Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 5 Mar 2026 17:23:30 -0800 Subject: [PATCH 270/448] Use debug as attachment when clicking debug agent panel button (#299610) --- .../chat/browser/actions/chatContext.ts | 54 +++++++++- .../browser/chatDebug/chatDebugAttachment.ts | 96 +++++++++++++++++ .../chat/browser/chatDebug/chatDebugEditor.ts | 8 +- .../browser/chatDebug/chatDebugLogsView.ts | 21 +++- .../contrib/chat/browser/chatSlashCommands.ts | 102 +----------------- .../input/editor/chatInputCompletions.ts | 40 +++++++ .../chat/common/actions/chatContextKeys.ts | 2 +- .../contrib/chat/common/chatDebugService.ts | 25 +++++ .../chat/common/chatDebugServiceImpl.ts | 26 +++++ .../resolveDebugEventDetailsTool.ts | 2 +- .../test/common/chatDebugServiceImpl.test.ts | 64 +++++++++++ 11 files changed, 328 insertions(+), 112 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugAttachment.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts index fb17967129b..6401df034be 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts @@ -21,11 +21,15 @@ import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput. import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../attachments/chatContextPickService.js'; import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; -import { isToolSet, ToolDataSource } from '../../common/tools/languageModelToolsService.js'; -import { IChatWidget } from '../chat.js'; +import { ILanguageModelToolsService, isToolSet, ToolDataSource } from '../../common/tools/languageModelToolsService.js'; +import { IChatWidget, IChatWidgetService } from '../chat.js'; import { imageToHash, isImage } from '../widget/input/editor/chatPasteProviders.js'; import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js'; import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js'; +import { createDebugEventsAttachment } from '../chatDebug/chatDebugAttachment.js'; +import { IChatDebugService } from '../../common/chatDebugService.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { URI } from '../../../../../base/common/uri.js'; import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; @@ -38,9 +42,29 @@ export class ChatContextContributions extends Disposable implements IWorkbenchCo constructor( @IInstantiationService instantiationService: IInstantiationService, @IChatContextPickService contextPickService: IChatContextPickService, + @IChatDebugService chatDebugService: IChatDebugService, + @IContextKeyService contextKeyService: IContextKeyService, + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @IChatWidgetService chatWidgetService: IChatWidgetService, ) { super(); + // Bind at the global context key service level so the tools service can evaluate it. + // Widget-scoped keys are not reliably visible to singleton services during async request processing. + const hasAttachedDebugDataKey = ChatContextKeys.chatSessionHasAttachedDebugData.bindTo(contextKeyService); + this._store.add(chatWidgetService.onDidChangeFocusedSession(() => { + const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource; + hasAttachedDebugDataKey.set(!!sessionResource && chatDebugService.hasAttachedDebugData(sessionResource)); + languageModelToolsService.flushToolUpdates(); + })); + this._store.add(chatDebugService.onDidAttachDebugData(sessionResource => { + const focusedSession = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource; + if (focusedSession && focusedSession.toString() === sessionResource.toString()) { + hasAttachedDebugDataKey.set(true); + languageModelToolsService.flushToolUpdates(); + } + })); + // ############################################################################################### // // Default context picks/values which are "native" to chat. This is NOT the complete list @@ -54,6 +78,7 @@ export class ChatContextContributions extends Disposable implements IWorkbenchCo this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(OpenEditorContextValuePick))); this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ClipboardImageContextValuePick))); this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick))); + this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(DebugEventsSnapshotContextValuePick))); } } @@ -285,3 +310,28 @@ class ScreenshotContextValuePick implements IChatContextValueItem { return blob && convertBufferToScreenshotVariable(blob); } } + +class DebugEventsSnapshotContextValuePick implements IChatContextValueItem { + + readonly type = 'valuePick'; + readonly icon = Codicon.output; + readonly label = localize('chatContext.debugEventsSnapshot', 'Debug Events Snapshot'); + readonly ordinal = -600; + + constructor( + @IChatDebugService private readonly _chatDebugService: IChatDebugService, + ) { } + + isEnabled(widget: IChatWidget): boolean { + const sessionResource = widget.viewModel?.sessionResource; + return !!sessionResource && this._chatDebugService.getEvents(sessionResource).length > 0; + } + + async asAttachment(widget: IChatWidget): Promise { + const sessionResource = widget.viewModel?.sessionResource; + if (!sessionResource) { + return undefined; + } + return createDebugEventsAttachment(sessionResource, this._chatDebugService); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugAttachment.ts new file mode 100644 index 00000000000..2d847f283a9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugAttachment.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../base/common/codicons.js'; +import { URI } from '../../../../../base/common/uri.js'; +import * as nls from '../../../../../nls.js'; +import { IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js'; +import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; + +/** + * Descriptions of each debug event kind for the model. Adding a new event kind + * to {@link IChatDebugEvent} without adding an entry here will cause a compile error. + */ +const debugEventKindDescriptions: Record = { + generic: '- generic (category: "discovery"): File discovery for instructions, skills, agents, hooks. Resolving returns a fileList with full file paths, load status, skip reasons, and source folders. Always resolve these for questions about customization files.\n' + + '- generic (other): Miscellaneous logs. Resolving returns additional text details.', + toolCall: '- toolCall: A tool invocation. Resolving returns tool name, input, output, status, and duration.', + modelTurn: '- modelTurn: An LLM round-trip. Resolving returns model name, token usage, timing, errors, and prompt sections.', + subagentInvocation: '- subagentInvocation: A sub-agent spawn. Resolving returns agent name, status, duration, and counts.', + userMessage: '- userMessage: The full prompt sent to the model. Resolving returns the complete message and all prompt sections (system prompt, instructions, context). Essential for understanding what the model received.', + agentResponse: '- agentResponse: The model\'s response. Resolving returns the full response text and sections.', +}; + +function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string { + const lines: string[] = []; + for (const event of events) { + const ts = event.created.toISOString(); + const id = event.id ? ` [id=${event.id}]` : ''; + switch (event.kind) { + case 'generic': + lines.push(`[${ts}]${id} ${event.level >= 3 ? 'ERROR' : event.level >= 2 ? 'WARN' : 'INFO'}: ${event.name}${event.details ? ' - ' + event.details : ''}${event.category ? ' (category: ' + event.category + ')' : ''}`); + break; + case 'toolCall': + lines.push(`[${ts}]${id} TOOL_CALL: ${event.toolName}${event.result ? ' result=' + event.result : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'modelTurn': + lines.push(`[${ts}]${id} MODEL_TURN: ${event.requestName ?? 'unknown'}${event.model ? ' model=' + event.model : ''}${event.inputTokens !== undefined ? ' tokens(in=' + event.inputTokens + ',out=' + (event.outputTokens ?? '?') + ')' : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'subagentInvocation': + lines.push(`[${ts}]${id} SUBAGENT: ${event.agentName}${event.status ? ' status=' + event.status : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); + break; + case 'userMessage': + lines.push(`[${ts}]${id} USER_MESSAGE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); + break; + case 'agentResponse': + lines.push(`[${ts}]${id} AGENT_RESPONSE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); + break; + default: { + const _: never = event; + void _; + break; + } + } + } + return lines.join('\n'); +} + +/** + * Creates a debug events attachment for a chat session. + * This can be used to attach debug logs to a chat request. + */ +export async function createDebugEventsAttachment( + sessionResource: URI, + chatDebugService: IChatDebugService +): Promise { + chatDebugService.markDebugDataAttached(sessionResource); + if (!chatDebugService.hasInvokedProviders(sessionResource)) { + await chatDebugService.invokeProviders(sessionResource); + } + const events = chatDebugService.getEvents(sessionResource); + const summary = events.length > 0 + ? formatDebugEventsForContext(events) + : nls.localize('debugEventsSnapshot.noEvents', "No debug events found for this conversation."); + + return { + id: 'chatDebugEvents', + name: nls.localize('debugEventsSnapshot.contextName', "Debug Events Snapshot"), + icon: Codicon.output, + kind: 'debugEvents', + snapshotTime: Date.now(), + sessionResource, + value: summary, + modelDescription: 'These are the debug event logs from the current chat conversation. Analyze them to help answer the user\'s troubleshooting question.\n' + + '\n' + + 'CRITICAL INSTRUCTION: You MUST call the resolveDebugEventDetails tool on relevant events BEFORE answering. The log lines below are only summaries — they do NOT contain the actual data (file paths, prompt content, tool I/O, etc.). The real information is only available by resolving events. Never answer based solely on the summary lines. Always resolve first, then answer.\n' + + '\n' + + 'Call resolveDebugEventDetails in parallel on all events that could be relevant to the user\'s question. When in doubt, resolve more events rather than fewer.\n' + + '\n' + + 'IMPORTANT: Do NOT mention event IDs, tool resolution steps, or internal debug mechanics in your response. The user does not know about debug events or event IDs. Present your findings directly and naturally, as if you simply know the answer. Never say things like "I need to resolve events" or show event IDs.\n' + + '\n' + + 'Event types and what resolving them returns:\n' + + Object.values(debugEventKindDescriptions).join('\n'), + }; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index ec403b30f95..8276d701b2a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -250,7 +250,9 @@ export class ChatDebugEditor extends EditorPane { } this.chatDebugService.activeSessionResource = sessionResource; - this.chatDebugService.invokeProviders(sessionResource); + if (!this.chatDebugService.hasInvokedProviders(sessionResource)) { + this.chatDebugService.invokeProviders(sessionResource); + } this.trackSessionModelChanges(sessionResource); this.overviewView?.setSession(sessionResource); @@ -327,7 +329,9 @@ export class ChatDebugEditor extends EditorPane { this.savedSessionResource = undefined; if (sessionResource) { this.chatDebugService.activeSessionResource = sessionResource; - this.chatDebugService.invokeProviders(sessionResource); + if (!this.chatDebugService.hasInvokedProviders(sessionResource)) { + this.chatDebugService.invokeProviders(sessionResource); + } } else { this.showView(ViewState.Home); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index a9bbec0b6d5..8cbf8c1a90d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -10,7 +10,7 @@ import { Button } from '../../../../../base/browser/ui/button/button.js'; import { IObjectTreeElement } from '../../../../../base/browser/ui/tree/tree.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -29,6 +29,7 @@ import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem, LogsViewMode } f import { ChatDebugFilterState, bindFilterContextKeys } from './chatDebugFilters.js'; import { ChatDebugDetailPanel } from './chatDebugDetailPanel.js'; import { IChatWidgetService } from '../chat.js'; +import { createDebugEventsAttachment } from './chatDebugAttachment.js'; const $ = DOM.$; @@ -131,9 +132,8 @@ export class ChatDebugLogsView extends Disposable { } const widget = await this.chatWidgetService.openSession(this.currentSessionResource); if (widget) { - const value = '/troubleshoot '; - widget.inputEditor.setValue(value); - widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + const attachment = await createDebugEventsAttachment(this.currentSessionResource, this.chatDebugService); + widget.attachmentModel.addContext(attachment); widget.focusInput(); } })); @@ -389,12 +389,23 @@ export class ChatDebugLogsView extends Disposable { private loadEvents(): void { this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)]; - this.eventListener.value = this.chatDebugService.onDidAddEvent(e => { + + const addEventDisposable = this.chatDebugService.onDidAddEvent(e => { if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) { this.events.push(e); this.refreshList(); } }); + + // Reload events when provider events are cleared (before re-invoking providers) + const clearEventsDisposable = this.chatDebugService.onDidClearProviderEvents(sessionResource => { + if (!this.currentSessionResource || sessionResource.toString() === this.currentSessionResource.toString()) { + this.events = [...this.chatDebugService.getEvents(this.currentSessionResource || undefined)]; + this.refreshList(); + } + }); + + this.eventListener.value = combinedDisposable(addEventDisposable, clearEventsDisposable); this.updateBreadcrumb(); this.trackSessionState(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 97e45f02809..4d183f09c9e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -15,11 +15,9 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IChatAgentService } from '../common/participants/chatAgents.js'; -import { IChatDebugEvent, IChatDebugService } from '../common/chatDebugService.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; -import { ChatRequestQueueKind, IChatService } from '../common/chatService/chatService.js'; +import { IChatService } from '../common/chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js'; -import { IChatRequestVariableEntry } from '../common/attachments/chatVariableEntries.js'; import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; import { ChatSubmitAction, OpenModePickerAction, OpenModelPickerAction } from './actions/chatExecuteActions.js'; import { ManagePluginsAction } from './actions/chatPluginActions.js'; @@ -34,12 +32,8 @@ import { globalAutoApproveDescription, } from './tools/languageModelToolsService.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; -import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { Target } from '../common/promptSyntax/promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { IChatWidgetService } from './chat.js'; export class ChatSlashCommandsContribution extends Disposable { @@ -52,25 +46,14 @@ export class ChatSlashCommandsContribution extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, - @IChatDebugService chatDebugService: IChatDebugService, @IConfigurationService configurationService: IConfigurationService, @IDialogService dialogService: IDialogService, @INotificationService notificationService: INotificationService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, - @IChatWidgetService chatWidgetService: IChatWidgetService, ) { super(); - const troubleshootSessions = new Set(); - const hasTroubleshootDataKey = ChatContextKeys.chatSessionHasTroubleshootData.bindTo(this.contextKeyService); - this._store.add(chatWidgetService.onDidChangeFocusedSession(() => { - const sessionResource = chatWidgetService.lastFocusedWidget?.viewModel?.sessionResource; - hasTroubleshootDataKey.set(!!sessionResource && troubleshootSessions.has(sessionResource.toString())); - languageModelToolsService.flushToolUpdates(); - })); this._store.add(slashCommandService.registerSlashCommand({ command: 'clear', detail: nls.localize('clear', "Start a new chat and archive the current one"), @@ -137,54 +120,6 @@ export class ChatSlashCommandsContribution extends Disposable { await commandService.executeCommand('github.copilot.debug.showChatLogView'); })); } - this._store.add(slashCommandService.registerSlashCommand({ - command: 'troubleshoot', - detail: nls.localize('troubleshoot', "Troubleshoot with a snapshot of debug events from the conversation so far (run again to refresh)"), - sortText: 'z3_troubleshoot', - executeImmediately: false, - silent: true, - locations: [ChatAgentLocation.Chat], - }, async (prompt, _progress, _history, _location, sessionResource, _token, options) => { - troubleshootSessions.add(sessionResource.toString()); - hasTroubleshootDataKey.set(true); - languageModelToolsService.flushToolUpdates(); - await chatDebugService.invokeProviders(sessionResource); - const events = chatDebugService.getEvents(sessionResource); - const summary = events.length > 0 - ? formatDebugEventsForContext(events) - : nls.localize('troubleshoot.noEvents', "No debug events found for this conversation."); - - const attachedContext: IChatRequestVariableEntry[] = [{ - id: 'chatDebugEvents', - name: nls.localize('troubleshoot.contextName', "Debug Events Snapshot"), - kind: 'debugEvents', - snapshotTime: Date.now(), - sessionResource, - value: summary, - modelDescription: 'These are the debug event logs from the current chat conversation. Analyze them to help answer the user\'s troubleshooting question.\n' - + '\n' - + 'CRITICAL INSTRUCTION: You MUST call the resolveDebugEventDetails tool on relevant events BEFORE answering. The log lines below are only summaries — they do NOT contain the actual data (file paths, prompt content, tool I/O, etc.). The real information is only available by resolving events. Never answer based solely on the summary lines. Always resolve first, then answer.\n' - + '\n' - + 'Call resolveDebugEventDetails in parallel on all events that could be relevant to the user\'s question. When in doubt, resolve more events rather than fewer.\n' - + '\n' - + 'IMPORTANT: Do NOT mention event IDs, tool resolution steps, or internal debug mechanics in your response. The user does not know about debug events or event IDs. Present your findings directly and naturally, as if you simply know the answer. Never say things like "I need to resolve events" or show event IDs.\n' - + '\n' - + 'Event types and what resolving them returns:\n' - + '- generic (category: "discovery"): File discovery for instructions, skills, agents, hooks. Resolving returns a fileList with full file paths, load status, skip reasons, and source folders. Always resolve these for questions about customization files.\n' - + '- userMessage: The full prompt sent to the model. Resolving returns the complete message and all prompt sections (system prompt, instructions, context). Essential for understanding what the model received.\n' - + '- agentResponse: The model\'s response. Resolving returns the full response text and sections.\n' - + '- modelTurn: An LLM round-trip. Resolving returns model name, token usage, timing, errors, and prompt sections.\n' - + '- toolCall: A tool invocation. Resolving returns tool name, input, output, status, and duration.\n' - + '- subagentInvocation: A sub-agent spawn. Resolving returns agent name, status, duration, and counts.\n' - + '- generic (other): Miscellaneous logs. Resolving returns additional text details.', - }]; - - chatService.sendRequest(sessionResource, prompt, { - ...options, - queue: ChatRequestQueueKind.Queued, - attachedContext, - }); - })); this._store.add(slashCommandService.registerSlashCommand({ command: 'agents', detail: nls.localize('agents', "Configure custom agents"), @@ -413,38 +348,3 @@ export class ChatSlashCommandsContribution extends Disposable { })); } } - -function formatDebugEventsForContext(events: readonly IChatDebugEvent[]): string { - const lines: string[] = []; - for (const event of events) { - const ts = event.created.toISOString(); - const id = event.id ? ` [id=${event.id}]` : ''; - switch (event.kind) { - case 'generic': - lines.push(`[${ts}]${id} ${event.level >= 3 ? 'ERROR' : event.level >= 2 ? 'WARN' : 'INFO'}: ${event.name}${event.details ? ' - ' + event.details : ''}${event.category ? ' (category: ' + event.category + ')' : ''}`); - break; - case 'toolCall': - lines.push(`[${ts}]${id} TOOL_CALL: ${event.toolName}${event.result ? ' result=' + event.result : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); - break; - case 'modelTurn': - lines.push(`[${ts}]${id} MODEL_TURN: ${event.requestName ?? 'unknown'}${event.model ? ' model=' + event.model : ''}${event.inputTokens !== undefined ? ' tokens(in=' + event.inputTokens + ',out=' + (event.outputTokens ?? '?') + ')' : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); - break; - case 'subagentInvocation': - lines.push(`[${ts}]${id} SUBAGENT: ${event.agentName}${event.status ? ' status=' + event.status : ''}${event.durationInMillis !== undefined ? ' duration=' + event.durationInMillis + 'ms' : ''}`); - break; - case 'userMessage': - lines.push(`[${ts}]${id} USER_MESSAGE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); - break; - case 'agentResponse': - lines.push(`[${ts}]${id} AGENT_RESPONSE: ${event.message.substring(0, 200)}${event.message.length > 200 ? '...' : ''} (${event.sections.length} sections)`); - break; - default: { - const _: never = event; - void _; - break; - } - } - } - return lines.join('\n'); -} - diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index ba97afb5e43..f72ccab3f56 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -64,6 +64,8 @@ import { IChatWidget, IChatWidgetService } from '../../../chat.js'; import { resizeImage } from '../../../chatImageUtils.js'; import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; +import { IChatDebugService } from '../../../../common/chatDebugService.js'; +import { createDebugEventsAttachment } from '../../../chatDebug/chatDebugAttachment.js'; import { getPromptFileType } from '../../../../common/promptSyntax/config/promptFileLocations.js'; /** @@ -848,6 +850,7 @@ interface IVariableCompletionsDetails { class BuiltinDynamicCompletions extends Disposable { private static readonly addReferenceCommand = '_addReferenceCmd'; + private static readonly addDebugEventsSnapshotCommand = '_addDebugEventsSnapshotCmd'; private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}[\\w:-]*`, 'g'); // MUST be using `g`-flag @@ -864,6 +867,7 @@ class BuiltinDynamicCompletions extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatDebugService private readonly chatDebugService: IChatDebugService, ) { super(); @@ -949,10 +953,46 @@ class BuiltinDynamicCompletions extends Disposable { return result; }); + // Debug Events Snapshot completion + this.registerVariableCompletions('debugEventsSnapshot', ({ widget, range }) => { + if (widget.location !== ChatAgentLocation.Chat) { + return; + } + + const sessionResource = widget.viewModel?.sessionResource; + if (!sessionResource || this.chatDebugService.getEvents(sessionResource).length === 0) { + return; + } + + const text = `${chatVariableLeader}debugEventsSnapshot`; + const result: CompletionList = { suggestions: [] }; + result.suggestions.push({ + label: { label: text, description: localize('debugEventsSnapshot.description', 'Attach debug events snapshot') }, + filterText: text, + insertText: '', + range, + kind: CompletionItemKind.Text, + sortText: 'z', + command: { + id: BuiltinDynamicCompletions.addDebugEventsSnapshotCommand, title: '', arguments: [widget] + } + }); + return result; + }); + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => { assertType(arg instanceof ReferenceArgument); return this.cmdAddReference(arg); })); + + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addDebugEventsSnapshotCommand, async (_services, widget: IChatWidget) => { + const sessionResource = widget.viewModel?.sessionResource; + if (!sessionResource) { + return; + } + const attachment = await createDebugEventsAttachment(sessionResource, this.chatDebugService); + widget.attachmentModel.addContext(attachment); + })); } private findActiveCodeEditor(): ICodeEditor | undefined { diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index cce18bceca5..33f7a4a9f7a 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -90,7 +90,7 @@ export namespace ChatContextKeys { export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); export const hasPendingRequests = new RawContextKey('chatHasPendingRequests', false, { type: 'boolean', description: localize('chatHasPendingRequests', "True when there are pending requests in the queue.") }); export const chatSessionHasDebugData = new RawContextKey('chatSessionHasDebugData', false, { type: 'boolean', description: localize('chatSessionHasDebugData', "True when the current chat session has debug log data.") }); - export const chatSessionHasTroubleshootData = new RawContextKey('chatSessionHasTroubleshootData', false, { type: 'boolean', description: localize('chatSessionHasTroubleshootData', "True when the /troubleshoot slash command has been run in the current chat session.") }); + export const chatSessionHasAttachedDebugData = new RawContextKey('chatSessionHasAttachedDebugData', false, { type: 'boolean', description: localize('chatSessionHasAttachedDebugData', "True when a debug events snapshot has been attached in the current chat session.") }); export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); diff --git a/src/vs/workbench/contrib/chat/common/chatDebugService.ts b/src/vs/workbench/contrib/chat/common/chatDebugService.ts index a50c09b18a1..a059f0aa836 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugService.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugService.ts @@ -125,6 +125,11 @@ export interface IChatDebugService extends IDisposable { */ readonly onDidAddEvent: Event; + /** + * Fired when provider events are cleared for a session (before re-invoking providers). + */ + readonly onDidClearProviderEvents: Event; + /** * Log a generic event to the debug service. */ @@ -167,6 +172,11 @@ export interface IChatDebugService extends IDisposable { */ registerProvider(provider: IChatDebugLogProvider): IDisposable; + /** + * Check whether providers have already been invoked for a given session. + */ + hasInvokedProviders(sessionResource: URI): boolean; + /** * Invoke all registered providers for a given session resource. * Called when the Debug View is opened to fetch events from extensions. @@ -185,6 +195,21 @@ export interface IChatDebugService extends IDisposable { * Delegates to the registered provider's resolveChatDebugLogEvent. */ resolveEvent(eventId: string): Promise; + + /** + * Fired when debug data is attached to a session. + */ + readonly onDidAttachDebugData: Event; + + /** + * Mark a session as having debug data attached. + */ + markDebugDataAttached(sessionResource: URI): void; + + /** + * Check whether a session has had debug data attached. + */ + hasAttachedDebugData(sessionResource: URI): boolean; } /** diff --git a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts index cce684e6d5b..9cdd711a311 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts @@ -25,6 +25,14 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic private readonly _onDidAddEvent = this._register(new Emitter()); readonly onDidAddEvent: Event = this._onDidAddEvent.event; + private readonly _onDidClearProviderEvents = this._register(new Emitter()); + readonly onDidClearProviderEvents: Event = this._onDidClearProviderEvents.event; + + private readonly _onDidAttachDebugData = this._register(new Emitter()); + readonly onDidAttachDebugData: Event = this._onDidAttachDebugData.event; + + private readonly _debugDataAttachedSessions = new ResourceMap(); + private readonly _providers = new Set(); private readonly _invocationCts = new ResourceMap(); @@ -102,6 +110,7 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic this._buffer.fill(undefined); this._head = 0; this._size = 0; + this._debugDataAttachedSessions.clear(); } registerProvider(provider: IChatDebugLogProvider): IDisposable { @@ -121,6 +130,10 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic }); } + hasInvokedProviders(sessionResource: URI): boolean { + return this._invocationCts.has(sessionResource); + } + async invokeProviders(sessionResource: URI): Promise { if (!LocalChatSessionUri.isLocalSession(sessionResource)) { return; @@ -180,6 +193,7 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic cts.dispose(); this._invocationCts.delete(sessionResource); } + this._debugDataAttachedSessions.delete(sessionResource); } private _clearProviderEvents(sessionResource: URI): void { @@ -203,6 +217,18 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic this._buffer[(this._head + i) % ChatDebugServiceImpl.MAX_EVENTS] = undefined; } this._size = write; + this._onDidClearProviderEvents.fire(sessionResource); + } + + markDebugDataAttached(sessionResource: URI): void { + if (!this._debugDataAttachedSessions.has(sessionResource)) { + this._debugDataAttachedSessions.set(sessionResource, true); + this._onDidAttachDebugData.fire(sessionResource); + } + } + + hasAttachedDebugData(sessionResource: URI): boolean { + return this._debugDataAttachedSessions.has(sessionResource); } async resolveEvent(eventId: string): Promise { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts index b3ff4593a43..dbf7e4e7105 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -15,7 +15,7 @@ export const ResolveDebugEventDetailsToolData: IToolData = { id: ResolveDebugEventDetailsToolId, toolReferenceName: 'resolveDebugEventDetails', displayName: localize('resolveDebugEventDetails.displayName', "Resolve Debug Event Details"), - when: ChatContextKeys.chatSessionHasTroubleshootData, + when: ChatContextKeys.chatSessionHasAttachedDebugData, canBeReferencedInPrompt: false, modelDescription: 'Resolves the full details for a specific chat debug event by its event ID. Use this tool to get detailed information about a debug event such as tool call input/output, model turn details, user message sections, or file lists. The event ID can be found in the debug event log summary provided in the conversation context.', source: ToolDataSource.Internal, diff --git a/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts b/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts index 5a247eaea99..9b9b0aac42a 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatDebugServiceImpl.test.ts @@ -214,6 +214,44 @@ suite('ChatDebugServiceImpl', () => { }); }); + suite('markDebugDataAttached', () => { + test('should track attached debug data per session', () => { + assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), false); + + const fired: URI[] = []; + disposables.add(service.onDidAttachDebugData(uri => fired.push(uri))); + + service.markDebugDataAttached(sessionGeneric); + assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), true); + assert.strictEqual(fired.length, 1); + assert.strictEqual(fired[0].toString(), sessionGeneric.toString()); + + // Idempotent — second call should not fire again + service.markDebugDataAttached(sessionGeneric); + assert.strictEqual(fired.length, 1); + + // Other sessions remain unaffected + assert.strictEqual(service.hasAttachedDebugData(sessionA), false); + }); + + test('should clear attached debug data on endSession', () => { + service.markDebugDataAttached(sessionGeneric); + assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), true); + + service.endSession(sessionGeneric); + assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), false); + }); + + test('should clear attached debug data on clear', () => { + service.markDebugDataAttached(sessionA); + service.markDebugDataAttached(sessionB); + + service.clear(); + assert.strictEqual(service.hasAttachedDebugData(sessionA), false); + assert.strictEqual(service.hasAttachedDebugData(sessionB), false); + }); + }); + suite('registerProvider', () => { test('should register and unregister a provider', async () => { const extSession = URI.parse('vscode-chat-session://local/ext-session'); @@ -312,6 +350,32 @@ suite('ChatDebugServiceImpl', () => { assert.strictEqual(firstToken.isCancellationRequested, true); }); + test('should fire onDidClearProviderEvents when clearing provider events', async () => { + const clearedSessions: URI[] = []; + disposables.add(service.onDidClearProviderEvents(sessionResource => clearedSessions.push(sessionResource))); + + const provider: IChatDebugLogProvider = { + provideChatDebugLog: async (sessionResource) => [{ + kind: 'generic', + sessionResource, + created: new Date(), + name: 'provider-event', + level: ChatDebugLogLevel.Info, + }], + }; + + disposables.add(service.registerProvider(provider)); + + // First invocation clears empty set and fires clear event + await service.invokeProviders(sessionGeneric); + assert.strictEqual(clearedSessions.length, 1, 'Clear event should fire on first invocation'); + + // Second invocation clears provider events from first invocation + await service.invokeProviders(sessionGeneric); + assert.strictEqual(clearedSessions.length, 2, 'Clear event should fire on second invocation'); + assert.strictEqual(clearedSessions[1].toString(), sessionGeneric.toString()); + }); + test('should not cancel invocations for different sessions', async () => { const tokens: Map = new Map(); From 68c79600df6599265b24e7958bd0753a04eb76a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:45:41 -0800 Subject: [PATCH 271/448] Bump @tootallnate/once from 3.0.0 to 3.0.1 in /remote (#299311) Bumps [@tootallnate/once](https://github.com/TooTallNate/once) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/TooTallNate/once/releases) - [Changelog](https://github.com/TooTallNate/once/blob/master/CHANGELOG.md) - [Commits](https://github.com/TooTallNate/once/compare/3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: "@tootallnate/once" dependency-version: 3.0.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- remote/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/remote/package-lock.json b/remote/package-lock.json index bd515187f0d..d936270a66b 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -421,9 +421,10 @@ "license": "MIT" }, "node_modules/@tootallnate/once": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.0.tgz", - "integrity": "sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.1.tgz", + "integrity": "sha512-VyMVKRrpHTT8PnotUeV8L/mDaMwD5DaAKCFLP73zAqAtvF0FCqky+Ki7BYbFCYQmqFyTe9316Ed5zS70QUR9eg==", + "license": "MIT", "engines": { "node": ">= 10" } From 22c9742134040d2411495354a6911c5858a54208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:46:35 -0800 Subject: [PATCH 272/448] Bump @vscode/component-explorer from 0.1.1-19 to 0.1.1-20 in /build/vite (#299463) Bumps [@vscode/component-explorer](https://github.com/microsoft/vscode-packages/tree/HEAD/js-component-explorer/packages/explorer) from 0.1.1-19 to 0.1.1-20. - [Commits](https://github.com/microsoft/vscode-packages/commits/HEAD/js-component-explorer/packages/explorer) --- updated-dependencies: - dependency-name: "@vscode/component-explorer" dependency-version: 0.1.1-20 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/vite/package-lock.json | 8 ++++---- build/vite/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index b7e27044aef..b9b854eacf2 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,7 +8,7 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer": "^0.1.1-20", "@vscode/component-explorer-vite-plugin": "^0.1.1-19", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", @@ -683,9 +683,9 @@ "license": "MIT" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-19", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-19.tgz", - "integrity": "sha512-wvcjw1A8wSH/oR5q+lZrBSyOQZfvXtLPYkQJBj11FBKu35iHko0FTIPMG25Ee+TpT2/BWLd29dWwiJODDQbC8w==", + "version": "0.1.1-20", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-20.tgz", + "integrity": "sha512-HvMWH+wK0SWC+eKZ2cL2LSsWnXiQjyQRURUgW2FBd8SM1G99+kKce0ESTYSr4b0tNJ1/FONE0ixADFlSRduzTg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/build/vite/package.json b/build/vite/package.json index 14f6ad51c57..67e2f227e40 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer": "^0.1.1-20", "@vscode/component-explorer-vite-plugin": "^0.1.1-19", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", From e85cb66e53b053a727c25c65e4c954d32a8d7217 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:46:45 -0800 Subject: [PATCH 273/448] Bump tar from 7.5.9 to 7.5.10 in /build/npm/gyp (#299352) Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.9 to 7.5.10. - [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/v7.5.9...v7.5.10) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/npm/gyp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index e2785131796..3142db6e89d 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -1069,9 +1069,9 @@ } }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { From 795b1858a156eb581210c7128b33c255f154462c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:47:05 -0800 Subject: [PATCH 274/448] Bump @tootallnate/once and jsdom in /extensions/notebook-renderers (#299312) Removes [@tootallnate/once](https://github.com/TooTallNate/once). It's no longer used after updating ancestor dependency [jsdom](https://github.com/jsdom/jsdom). These dependencies need to be updated together. Removes `@tootallnate/once` Updates `jsdom` from 21.1.1 to 28.1.0 - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/21.1.1...28.1.0) --- updated-dependencies: - dependency-name: "@tootallnate/once" dependency-version: dependency-type: indirect - dependency-name: jsdom dependency-version: 28.1.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../notebook-renderers/package-lock.json | 1046 +++++++---------- extensions/notebook-renderers/package.json | 2 +- 2 files changed, 415 insertions(+), 633 deletions(-) diff --git a/extensions/notebook-renderers/package-lock.json b/extensions/notebook-renderers/package-lock.json index beacde5ae21..7908a9bf049 100644 --- a/extensions/notebook-renderers/package-lock.json +++ b/extensions/notebook-renderers/package-lock.json @@ -12,19 +12,218 @@ "@types/jsdom": "^21.1.0", "@types/node": "^22.18.10", "@types/vscode-notebook-renderer": "^1.60.0", - "jsdom": "^21.1.1" + "jsdom": "^28.1.0" }, "engines": { "vscode": "^1.57.0" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, "engines": { - "node": ">= 10" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.29.tgz", + "integrity": "sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@types/jsdom": { @@ -60,121 +259,78 @@ "integrity": "sha512-u7TD2uuEZTVuitx0iijOJdKI0JLiQP6PsSBSRy2XmHXUOXcp5p1S56NrjOEDoF+PIHd3NL3eO6KTRSf5nukDqQ==", "dev": true }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true - }, - "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "dependencies": { - "debug": "4" - }, + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "require-from-string": "^2.0.2" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, + "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" }, "engines": { - "node": ">= 0.8" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", "dev": true, + "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.6.0" + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" }, "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, + "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=14" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -186,53 +342,11 @@ } }, "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, "node_modules/entities": { "version": "4.4.0", @@ -246,284 +360,45 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/form-data": { - "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": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, + "license": "MIT", "dependencies": { - "whatwg-encoding": "^2.0.0" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 14" } }, "node_modules/is-potential-custom-element-name": { @@ -533,43 +408,39 @@ "dev": true }, "node_modules/jsdom": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.1.tgz", - "integrity": "sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, + "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.2", - "acorn-globals": "^7.0.0", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -577,78 +448,55 @@ } } }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.8.0" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { - "mime-db": "1.52.0" + "entities": "^6.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", - "dev": true - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } + "license": "MIT" }, "node_modules/parse5": { "version": "7.1.2", @@ -662,53 +510,25 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", - "dev": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/saxes": { "version": "6.0.0", @@ -722,12 +542,12 @@ "node": ">=v12.22.7" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "optional": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -738,43 +558,60 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "node_modules/tldts": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.24.tgz", + "integrity": "sha512-1r6vQTTt1rUiJkI5vX7KG8PR342Ru/5Oh13kEQP2SMbRSZpOey9SrBe27IDxkoWulx8ShWu4K6C0BkctP8Z1bQ==", "dev": true, + "license": "MIT", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts-core": "^7.0.24" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.24.tgz", + "integrity": "sha512-pj7yygNMoMRqG7ML2SDQ0xNIOfN3IBDUcPVM2Sg6hP96oFNN2nqnzHreT3z9xLq85IWJyNTvD38O002DdOrPMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" }, "engines": { - "node": ">=6" + "node": ">=16" } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, + "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=20" } }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, + "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -784,117 +621,62 @@ "dev": true, "license": "MIT" }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, + "license": "MIT", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, + "license": "MIT", "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=14" - } - }, - "node_modules/word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { diff --git a/extensions/notebook-renderers/package.json b/extensions/notebook-renderers/package.json index e9890cad899..fad11bc9e30 100644 --- a/extensions/notebook-renderers/package.json +++ b/extensions/notebook-renderers/package.json @@ -50,7 +50,7 @@ "@types/jsdom": "^21.1.0", "@types/node": "^22.18.10", "@types/vscode-notebook-renderer": "^1.60.0", - "jsdom": "^21.1.1" + "jsdom": "^28.1.0" }, "repository": { "type": "git", From e528731081510888a62a1d79d4f8a2aa8ef3db65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:04:35 +0000 Subject: [PATCH 275/448] Bump dompurify from 3.2.7 to 3.3.2 in /extensions/mermaid-chat-features (#299626) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.2.7 to 3.3.2. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.2.7...3.3.2) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.3.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- extensions/mermaid-chat-features/package-lock.json | 11 +++++++---- extensions/mermaid-chat-features/package.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/extensions/mermaid-chat-features/package-lock.json b/extensions/mermaid-chat-features/package-lock.json index 0d0bb2a582b..23436bd9f00 100644 --- a/extensions/mermaid-chat-features/package-lock.json +++ b/extensions/mermaid-chat-features/package-lock.json @@ -9,7 +9,7 @@ "version": "10.0.0", "license": "MIT", "dependencies": { - "dompurify": "^3.2.7", + "dompurify": "^3.3.2", "mermaid": "^11.12.3" }, "devDependencies": { @@ -1016,10 +1016,13 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json index 811bff8076d..68f5271fef6 100644 --- a/extensions/mermaid-chat-features/package.json +++ b/extensions/mermaid-chat-features/package.json @@ -130,7 +130,7 @@ "@vscode/codicons": "^0.0.36" }, "dependencies": { - "dompurify": "^3.2.7", + "dompurify": "^3.3.2", "mermaid": "^11.12.3" } } From 6d53d3e19212860b894928edb532a07dd2ef3b6c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 10:02:48 +0000 Subject: [PATCH 276/448] Update toolbar hover background colors in 2026 dark and light themes --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index ce19af60842..cd8cce33b4d 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -60,7 +60,7 @@ "list.hoverBackground": "#262728", "list.hoverForeground": "#bfbfbf", "list.dropBackground": "#3994BC1A", - "toolbar.hoverBackground": "#262728", + "toolbar.hoverBackground": "#FFFFFF18", "list.focusBackground": "#3994BC26", "list.focusForeground": "#bfbfbf", "list.focusOutline": "#3994BCB3", @@ -192,7 +192,7 @@ "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", "tab.activeBorderTop": "#3994BC", - "tab.hoverBackground": "#262728", + "tab.hoverBackground": "#121314", "tab.hoverForeground": "#bfbfbf", "tab.unfocusedActiveBackground": "#121314", "tab.unfocusedActiveForeground": "#8C8C8C", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index a03d296a0c6..eb22e1c1c49 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -188,7 +188,7 @@ "statusBarItem.prominentBackground": "#0069CCDD", "statusBarItem.prominentForeground": "#FFFFFF", "statusBarItem.prominentHoverBackground": "#0069CC", - "toolbar.hoverBackground": "#DADADA4f", + "toolbar.hoverBackground": "#00000010", "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", "tab.inactiveBackground": "#FAFAFD", @@ -196,7 +196,7 @@ "tab.border": "#F0F1F2FF", "tab.lastPinnedBorder": "#F0F1F2FF", "tab.activeBorderTop": "#000000", - "tab.hoverBackground": "#DADADA4f", + "tab.hoverBackground": "#FFFFFF", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", "tab.unfocusedActiveForeground": "#606060", From 80c418069fe1996cb5cadfa8285df54bfc4163bd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 11:27:46 +0100 Subject: [PATCH 277/448] hide inline chat affordance when editor loses focus (#299716) Fixes #299616 --- .../contrib/inlineChat/browser/inlineChatAffordance.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index 961c1943e74..a5f6a2ad803 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -120,6 +120,13 @@ export class InlineChatAffordance extends Disposable { selectionData.set(undefined, undefined); })); + // Hide when the editor loses focus (e.g., switching tabs in notebooks) + this._store.add(autorun(r => { + if (!editorObs.isFocused.read(r)) { + selectionData.set(undefined, undefined); + } + })); + this._store.add(autorun(r => { const sel = selectionData.read(r); const mode = affordance.read(r); From c3c31a2ba5615dd48ba21e9cb10e71b17510d104 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 6 Mar 2026 11:30:50 +0100 Subject: [PATCH 278/448] fix claude pickers when in overflow menu --- .../chatSessions/chatSessionPickerActionItem.ts | 15 +++++++++++++++ .../searchableOptionPickerActionItem.ts | 4 +++- .../chat/browser/widget/input/chatInputPart.ts | 10 ++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts index a21e2483b8d..241eeb90adf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.ts @@ -7,6 +7,7 @@ import './media/chatSessionPickerActionItem.css'; import { IAction } from '../../../../../base/common/actions.js'; import { Event } from '../../../../../base/common/event.js'; import * as dom from '../../../../../base/browser/dom.js'; +import { getActiveWindow } from '../../../../../base/browser/dom.js'; import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction, IActionWidgetDropdownOptions } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -19,6 +20,7 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { renderLabelWithIcons, renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../../nls.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IChatInputPickerOptions } from '../widget/input/chatInputPickerActionItem.js'; export interface IChatSessionPickerDelegate { @@ -41,6 +43,7 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI action: IAction, initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined }, protected readonly delegate: IChatSessionPickerDelegate, + protected readonly _pickerOptions: IChatInputPickerOptions | undefined, @IActionWidgetService actionWidgetService: IActionWidgetService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -61,6 +64,7 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI }, actionBarActionProvider: undefined, reporter: { id: group.id, name: `ChatSession:${group.name}`, includeOptions: false }, + getAnchor: () => this._getAnchorElement(), }; super(actionWithLabel, sessionPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); @@ -153,6 +157,17 @@ export class ChatSessionPickerActionItem extends ActionWidgetDropdownActionViewI }; } + /** + * Returns the anchor element for the dropdown. + * Falls back to the overflow anchor if this element is not in the DOM. + */ + private _getAnchorElement(): HTMLElement { + if (this.element && getActiveWindow().document.contains(this.element)) { + return this.element; + } + return this._pickerOptions?.getOverflowAnchor?.() ?? this.element!; + } + protected override renderLabel(element: HTMLElement): IDisposable | null { const domChildren = []; element.classList.add('chat-session-option-picker'); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts index 9d22269371f..fd19654bcef 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts @@ -22,6 +22,7 @@ import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from './chatS import { ILogService } from '../../../../../platform/log/common/log.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IChatInputPickerOptions } from '../widget/input/chatInputPickerActionItem.js'; interface ISearchableOptionQuickPickItem extends IQuickPickItem { readonly optionItem: IChatSessionProviderOptionItem; @@ -43,6 +44,7 @@ export class SearchableOptionPickerActionItem extends ChatSessionPickerActionIte action: IAction, initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined }, delegate: IChatSessionPickerDelegate, + pickerOptions: IChatInputPickerOptions | undefined, @IActionWidgetService actionWidgetService: IActionWidgetService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -51,7 +53,7 @@ export class SearchableOptionPickerActionItem extends ChatSessionPickerActionIte @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(action, initialState, delegate, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService); + super(action, initialState, delegate, pickerOptions, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService); } protected override getDropdownActions(): IActionWidgetDropdownAction[] { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 4f54ec7cfe3..e54e7386159 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -381,6 +381,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatSessionPickerWidgets: Map = new Map(); private chatSessionPickerContainer: HTMLElement | undefined; private _lastSessionPickerAction: MenuItemAction | undefined; + private _lastSessionPickerOptions: IChatInputPickerOptions | undefined; private readonly _waitForPersistedLanguageModel: MutableDisposable = this._register(new MutableDisposable()); private readonly _chatSessionOptionEmitters: Map> = new Map(); @@ -838,8 +839,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Create picker widgets for all option groups available for the current session type. */ - private createChatSessionPickerWidgets(action: MenuItemAction): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { + private createChatSessionPickerWidgets(action: MenuItemAction, pickerOptions?: IChatInputPickerOptions): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { this._lastSessionPickerAction = action; + this._lastSessionPickerOptions = pickerOptions; const result = this.computeVisibleOptionGroups(); if (!result) { @@ -890,7 +892,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } }; - const widget = this.instantiationService.createInstance(optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, action, initialState, itemDelegate); + const widget = this.instantiationService.createInstance(optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, action, initialState, itemDelegate, pickerOptions); this.chatSessionPickerWidgets.set(optionGroup.id, widget); widgets.push(widget); } @@ -1720,7 +1722,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge !Array.from(visibleGroupIds).every(id => currentWidgetGroupIds.has(id)); if (needsRecreation && this._lastSessionPickerAction && this.chatSessionPickerContainer) { - const widgets = this.createChatSessionPickerWidgets(this._lastSessionPickerAction); + const widgets = this.createChatSessionPickerWidgets(this._lastSessionPickerAction, this._lastSessionPickerOptions); dom.clearNode(this.chatSessionPickerContainer); for (const widget of widgets) { const container = dom.$('.action-item.chat-sessionPicker-item'); @@ -2269,7 +2271,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { // Create all pickers and return a container action view item - const widgets = this.createChatSessionPickerWidgets(action); + const widgets = this.createChatSessionPickerWidgets(action, pickerOptions); if (widgets.length === 0) { return new HiddenActionViewItem(action); } From 176d771e8e0fbda8ad5ec4b717f4dc744796657b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 11:34:44 +0100 Subject: [PATCH 279/448] run oss-tool, update distro (#299717) --- ThirdPartyNotices.txt | 2 +- cglicenses.json | 9 -- cli/ThirdPartyNotices.txt | 170 ++------------------------------------ package.json | 2 +- 4 files changed, 8 insertions(+), 175 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 896b59001d6..0a15b3ff5fc 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -684,7 +684,7 @@ more details. --------------------------------------------------------- -go-syntax 0.8.5 - MIT +go-syntax 0.8.6 - MIT https://github.com/worlpaker/go-syntax MIT License diff --git a/cglicenses.json b/cglicenses.json index 37bba3145ba..48d2c3b093c 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -306,11 +306,6 @@ "name": "russh-keys", "fullLicenseTextUri": "https://raw.githubusercontent.com/warp-tech/russh/1da80d0d599b6ee2d257c544c0d6af4f649c9029/LICENSE-2.0.txt" }, - { - // Reason: license is in a subdirectory in repo - "name": "dirs-next", - "fullLicenseTextUri": "https://raw.githubusercontent.com/xdg-rs/dirs/af4aa39daba0ac68e222962a5aca17360158b7cc/dirs/LICENSE-MIT" - }, { // Reason: license is in a subdirectory in repo "name": "openssl", @@ -361,10 +356,6 @@ "name": "toml_datetime", "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml_datetime/LICENSE-MIT" }, - { // License is MIT/Apache and tool doesn't look in subfolders - "name": "dirs-sys-next", - "fullLicenseTextUri": "https://raw.githubusercontent.com/xdg-rs/dirs/master/dirs-sys/LICENSE-MIT" - }, { // License is MIT/Apache and gitlab API doesn't find the project "name": "libredox", "fullLicenseTextUri": "https://gitlab.redox-os.org/redox-os/libredox/-/raw/master/LICENSE" diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 9edb0ae9d23..6e21ddb3729 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -1666,7 +1666,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (crates) [`aead`]: ./aead -[`async‑signature`]: ./signature/async [`cipher`]: ./cipher [`crypto‑common`]: ./crypto-common [`crypto`]: ./crypto @@ -1828,7 +1827,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (crates) [`aead`]: ./aead -[`async‑signature`]: ./signature/async [`cipher`]: ./cipher [`crypto‑common`]: ./crypto-common [`crypto`]: ./crypto @@ -13671,33 +13669,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a zbus 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -13705,33 +13677,7 @@ DEALINGS IN THE SOFTWARE. zbus_macros 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -13739,33 +13685,7 @@ DEALINGS IN THE SOFTWARE. zbus_names 2.6.1 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -14211,33 +14131,7 @@ DEALINGS IN THE SOFTWARE. zvariant 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -14245,33 +14139,7 @@ DEALINGS IN THE SOFTWARE. zvariant_derive 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -14279,31 +14147,5 @@ DEALINGS IN THE SOFTWARE. zvariant_utils 1.0.1 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- \ No newline at end of file diff --git a/package.json b/package.json index 680a4bcd8cf..d9603fc5728 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.111.0", - "distro": "e802965a9da346fb619bb708f64e54e927167133", + "distro": "cd72f8f27b485d65c99f5020caa895a5ac5692eb", "author": { "name": "Microsoft Corporation" }, From 78e13de8de0a5b7be13b346aaf457f373e8ac221 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 10:42:45 +0000 Subject: [PATCH 280/448] Add minimap slider colors to 2026 dark and light themes --- extensions/theme-2026/themes/2026-dark.json | 5 ++++- extensions/theme-2026/themes/2026-light.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index cd8cce33b4d..8eb011c2443 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -268,7 +268,10 @@ "charts.orange": "#CD861A", "charts.green": "#86CF86", "charts.purple": "#AD80D7", - "inlineChat.border": "#00000000" + "inlineChat.border": "#00000000", + "minimapSlider.background": "#83848533", + "minimapSlider.hoverBackground": "#83848566", + "minimapSlider.activeBackground": "#83848599", }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index eb22e1c1c49..a0f14f5e5d3 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -272,7 +272,10 @@ "charts.green": "#388A34", "charts.purple": "#652D90", "agentStatusIndicator.background": "#FFFFFF", - "inlineChat.border": "#00000000" + "inlineChat.border": "#00000000", + "minimapSlider.background": "#99999926", + "minimapSlider.hoverBackground": "#99999940", + "minimapSlider.activeBackground": "#99999955", }, "tokenColors": [ { From 5b38f1d5296b42ae51049d452362b673ff714dbe Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 6 Mar 2026 21:48:45 +1100 Subject: [PATCH 281/448] Update default model selection to prioritize 'copilot' vendor in ExtHostLanguageModels (#298903) * Update default model selection to prioritize 'copilot' vendor in ExtHostLanguageModels * Fix tests * Fix tests --- extensions/vscode-api-tests/package.json | 4 ++++ .../vscode-api-tests/src/singlefolder-tests/chat.test.ts | 2 +- src/vs/workbench/api/common/extHostLanguageModels.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e5c6ce5f767..e5167429902 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -71,6 +71,10 @@ { "vendor": "test-lm-vendor", "displayName": "Test LM Vendor" + }, + { + "vendor": "copilot", + "displayName": "Test Copilot LM Vendor" } ], "chatParticipants": [ diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index ff5b49d9b69..6ed6c911718 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -17,7 +17,7 @@ suite('chat', () => { disposables = []; // Register a dummy default model which is required for a participant request to go through - disposables.push(lm.registerLanguageModelChatProvider('test-lm-vendor', { + disposables.push(lm.registerLanguageModelChatProvider('copilot', { async provideLanguageModelChatInformation(_options, _token) { return [{ id: 'test-lm', diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index b6bcbfdbb52..cc76961ab15 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -364,7 +364,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } for (const [modelIdentifier, modelData] of this._localModels) { - if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat] && !modelData.metadata.targetChatSessionType) { + if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat] && modelData.metadata.vendor === 'copilot') { defaultModelId = modelIdentifier; break; } From 13c7b019e2349828e8c90439d2187653dc0a141e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 10:57:11 +0000 Subject: [PATCH 282/448] Update widget and menu border colors in 2026 light theme Co-authored-by: Copilot --- extensions/theme-2026/themes/2026-light.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index a0f14f5e5d3..f963724d6b5 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -50,7 +50,7 @@ "inputValidation.errorForeground": "#202020", "scrollbar.shadow": "#00000000", "widget.shadow": "#00000000", - "widget.border": "#EEEEF1", + "widget.border": "#E2E2E5", "editorStickyScroll.shadow": "#00000000", "editorStickyScrollHover.background": "#F0F0F3", "editorStickyScroll.border": "#F0F1F2FF", @@ -105,7 +105,7 @@ "menu.selectionBackground": "#0069CC1A", "menu.selectionForeground": "#202020", "menu.separatorBackground": "#EEEEF1", - "menu.border": "#F0F1F2FF", + "menu.border": "#E4E5E6FF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", "commandCenter.background": "#FAFAFD", @@ -136,15 +136,15 @@ "editorBracketMatch.background": "#0069CC40", "editorBracketMatch.border": "#F0F1F2FF", "editorWidget.background": "#FAFAFD", - "editorWidget.border": "#F0F1F2FF", + "editorWidget.border": "#E4E5E6FF", "editorWidget.foreground": "#202020", "editorSuggestWidget.background": "#FAFAFD", - "editorSuggestWidget.border": "#F0F1F2FF", + "editorSuggestWidget.border": "#E4E5E6FF", "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", "editorHoverWidget.background": "#FAFAFD", - "editorHoverWidget.border": "#F0F1F2FF", + "editorHoverWidget.border": "#E4E5E6FF", "peekView.border": "#0069CC", "peekViewEditor.background": "#FAFAFD", "peekViewEditor.matchHighlightBackground": "#0069CC33", From a9a9436b2bca278d0c0c1912abe2f3a5e06085a6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 12:26:44 +0100 Subject: [PATCH 283/448] Bump version to 1.112.0 in package.json and package-lock.json (#299736) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eadb4002ba5..5fc1351c51f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-oss-dev", - "version": "1.111.0", + "version": "1.112.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.111.0", + "version": "1.112.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d9603fc5728..3f5bcc61b9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "1.111.0", + "version": "1.112.0", "distro": "cd72f8f27b485d65c99f5020caa895a5ac5692eb", "author": { "name": "Microsoft Corporation" From ae0eadc1ff3dd969913cb054e8c70fcd74625279 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 11:33:22 +0000 Subject: [PATCH 284/448] Update tab borders in 2026 dark and light themes --- extensions/theme-2026/themes/2026-dark.json | 4 ++-- extensions/theme-2026/themes/2026-light.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8eb011c2443..d204a5506b0 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -199,8 +199,8 @@ "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", "editorGroupHeader.tabsBackground": "#191A1B", - "tab.activeBorder": "#00000000", - "editorGroupHeader.tabsBorder": "#00000000", + "tab.activeBorder": "#121314", + "editorGroupHeader.tabsBorder": "#2A2B2CFF", "breadcrumb.foreground": "#8C8C8C", "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bfbfbf", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f963724d6b5..6df7171b0d2 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -203,8 +203,8 @@ "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", - "tab.activeBorder": "#00000000", - "editorGroupHeader.tabsBorder": "#00000000", + "tab.activeBorder": "#FFFFFF", + "editorGroupHeader.tabsBorder": "#F0F1F2FF", "breadcrumb.foreground": "#606060", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", From 66aa46c943596e0ebda6b36186b3b13cfd3b6c8b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 12:06:13 +0000 Subject: [PATCH 285/448] Add background color to resizable hover widget in hover.css Co-authored-by: Copilot --- src/vs/editor/contrib/hover/browser/hover.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index d9d64ffc216..269ee853c7e 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -11,6 +11,7 @@ border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: var(--vscode-cornerRadius-large); box-sizing: content-box; + background-color: var(--vscode-editorHoverWidget-background); } .monaco-editor .monaco-resizable-hover > .monaco-hover { From 6727bcbf51039b5ac3f4cdf8afcac7babd5303ec Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 12:51:28 +0000 Subject: [PATCH 286/448] Refactor dialog CSS styles for improved layout and spacing --- src/vs/base/browser/ui/dialog/dialog.css | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index d1d37dcff67..9702c74f1ae 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -25,13 +25,13 @@ display: flex; flex-direction: column-reverse; width: min-content; - min-width: 500px; + min-width: 480px; max-width: 90vw; max-height: 90vh; min-height: 75px; - padding: 10px; + padding: 8px; transform: translate3d(0px, 0px, 0px); - border-radius: var(--vscode-cornerRadius-large); + border-radius: var(--vscode-cornerRadius-xLarge); box-shadow: var(--vscode-shadow-xl); } @@ -41,7 +41,7 @@ /** Dialog: Title Actions Row */ .monaco-dialog-box .dialog-toolbar-row { - height: 22px; + height: 20px; padding-bottom: 4px; } @@ -55,7 +55,7 @@ display: flex; flex-grow: 1; align-items: center; - padding: 0 10px; + padding: 0 8px 0 12px; min-height: 0; /* allow flex item to shrink below content size */ overflow: hidden; } @@ -69,9 +69,9 @@ } .monaco-dialog-box .dialog-message-row > .dialog-icon.codicon { - flex: 0 0 48px; - height: 48px; - font-size: 48px; + flex: 0 0 24px; + height: 24px; + font-size: 24px; } .monaco-dialog-box.align-vertical .dialog-message-row > .dialog-icon.codicon { @@ -108,7 +108,7 @@ .monaco-dialog-box:not(.align-vertical) .dialog-message-row .dialog-message-container, .monaco-dialog-box:not(.align-vertical) .dialog-footer-row { - padding-left: 24px; + padding-left: 12px; } .monaco-dialog-box.align-vertical .dialog-message-row .dialog-message-container, @@ -124,20 +124,20 @@ /** Dialog: Message */ .monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message { - line-height: 22px; - font-size: 18px; + font-size: 14px; + font-weight: 600; flex: 1; /* let the message always grow */ white-space: normal; word-wrap: break-word; /* never overflow long words, but break to next line */ - min-height: 48px; /* matches icon height */ - margin-bottom: 8px; + min-height: 22px; /* matches icon height */ + margin-bottom: 4px; display: flex; align-items: center; } /** Dialog: Details */ .monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-detail { - line-height: 22px; + line-height: 20px; flex: 1; /* let the message always grow */ } @@ -185,7 +185,7 @@ .monaco-dialog-box > .dialog-buttons-row { display: flex; white-space: nowrap; - padding: 20px 10px 10px; + padding: 20px 0px 0px; } /** Dialog: Buttons */ @@ -209,8 +209,8 @@ .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button { overflow: hidden; text-overflow: ellipsis; - margin: 4px 5px; /* allows button focus outline to be visible */ - outline-offset: 2px !important; + margin: 4px; /* allows button focus outline to be visible */ + outline-offset: 1px !important; } .monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons > .monaco-button { From 4ed8d528f3a18c2de61c08523124ad9763439376 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 12:58:23 +0000 Subject: [PATCH 287/448] Update dialog shadow border radius to use xLarge corner radius --- src/vs/base/browser/ui/dialog/dialog.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 9702c74f1ae..ba2976b93b5 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -253,5 +253,5 @@ } .monaco-dialog-modal-block .dialog-shadow { - border-radius: var(--vscode-cornerRadius-large); + border-radius: var(--vscode-cornerRadius-xLarge); } From 979ed7c014b0016c6775c2ca77c6c3477ece4210 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 12:59:27 +0000 Subject: [PATCH 288/448] Fix dialog message container min-height comment for clarity --- src/vs/base/browser/ui/dialog/dialog.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index ba2976b93b5..1fe8bdb4295 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -129,7 +129,7 @@ flex: 1; /* let the message always grow */ white-space: normal; word-wrap: break-word; /* never overflow long words, but break to next line */ - min-height: 22px; /* matches icon height */ + min-height: 22px; margin-bottom: 4px; display: flex; align-items: center; From 235346a65c21daa0599d1a6fb3f3f12243e4ff55 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 6 Mar 2026 13:00:33 +0000 Subject: [PATCH 289/448] Remove unnecessary flex display from dialog buttons row for improved layout --- src/vs/base/browser/ui/dialog/dialog.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 1fe8bdb4295..cf35f8126c6 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -180,10 +180,6 @@ align-items: center; padding-right: 1px; overflow: hidden; /* buttons row should never overflow */ -} - -.monaco-dialog-box > .dialog-buttons-row { - display: flex; white-space: nowrap; padding: 20px 0px 0px; } From db73eef8c4b6a5306719878ff9e1ee3fd100e821 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:22:27 -0800 Subject: [PATCH 290/448] fix quick chat input not showing label (#299750) * fix quick chat input not showing label * don't use workaround --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 6 +++--- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 4f54ec7cfe3..9eb316cb94b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2021,7 +2021,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; this.secondaryToolbarContainer = elements.secondaryToolbar; - if (this.options.isSessionsWindow) { + if (this.options.isSessionsWindow || this.options.renderStyle === 'compact') { this.secondaryToolbarContainer.style.display = 'none'; } this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; @@ -2032,7 +2032,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; - if (this.options.isSessionsWindow) { + if (this.options.isSessionsWindow || this.options.renderStyle === 'compact') { toolbarsContainer.prepend(this.contextUsageWidgetContainer); } @@ -3147,7 +3147,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth(); const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * toolbarItemGap; const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * toolbarItemGap : 0; - const contextUsageWidth = 0;// dom.getTotalWidth(this.contextUsageWidgetContainer); + const contextUsageWidth = dom.getTotalWidth(this.contextUsageWidgetContainer); const inputToolbarsPadding = 12; // pdading between input toolbar/execute toolbar/contextUsage. return executeToolbarWidth + executeToolbarPadding + contextUsageWidth + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding + inputToolbarsPadding); }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index d99f33cca29..fbb9c530fbb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -818,8 +818,6 @@ have to be updated for changes to the rules above, or to support more deeply nes /* top padding is inside the editor widget */ width: 100%; position: relative; - /* Prevent contents from covering border corner */ - overflow: hidden; } /* Context usage widget container - positioned in the secondary toolbar below input */ From 81f2b5cd2fdf2c2ceb61899f79332db8551f2c35 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 6 Mar 2026 16:14:04 +0100 Subject: [PATCH 291/448] chore - Refactor inline chat classes to use private class fields (#299778) * Refactor inline chat affordance classes to use private class fields * native privates for inline chat --- .../inlineChat/browser/inlineChatActions.ts | 11 +- .../browser/inlineChatController.ts | 297 +++++++++-------- .../browser/inlineChatEditorAffordance.ts | 112 +++---- .../browser/inlineChatGutterAffordance.ts | 6 +- .../inlineChat/browser/inlineChatNotebook.ts | 6 +- .../browser/inlineChatOverlayWidget.ts | 299 +++++++++--------- .../browser/inlineChatSessionServiceImpl.ts | 87 ++--- .../inlineChat/browser/inlineChatWidget.ts | 178 ++++++----- .../browser/inlineChatZoneWidget.ts | 75 +++-- .../test/browser/testWorkerService.ts | 24 +- 10 files changed, 592 insertions(+), 503 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index aef9aeefc52..204ae20da8d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -91,11 +91,11 @@ export class StartSessionAction extends Action2 { logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize()); return; } - return this._runEditorCommand(editorAccessor, editor, ...args); + return this.#runEditorCommand(editorAccessor, editor, ...args); }); } - private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { + async #runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { const configServce = accessor.get(IConfigurationService); @@ -262,12 +262,15 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction { class KeepOrUndoSessionAction extends AbstractInlineChatAction { - constructor(private readonly _keep: boolean, desc: IAction2Options) { + readonly #keep: boolean; + + constructor(keep: boolean, desc: IAction2Options) { super(desc); + this.#keep = keep; } override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise { - if (this._keep) { + if (this.#keep) { await ctrl.acceptSession(); } else { await ctrl.rejectSession(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 76aa39da942..869084cff1d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -112,63 +112,93 @@ export class InlineChatController implements IEditorContribution { * Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session. * When set, this takes priority over the inlineChat.defaultModel setting. */ - private static _userSelectedModel: string | undefined; + static #userSelectedModel: string | undefined; - private readonly _store = new DisposableStore(); - private readonly _isActiveController = observableValue(this, false); - private readonly _renderMode: IObservable<'zone' | 'hover'>; - private readonly _zone: Lazy; + readonly #store = new DisposableStore(); + readonly #isActiveController = observableValue(this, false); + readonly #renderMode: IObservable<'zone' | 'hover'>; + readonly #zone: Lazy; readonly inputOverlayWidget: InlineChatAffordance; - private readonly _inputWidget: InlineChatInputWidget; + readonly #inputWidget: InlineChatInputWidget; - private readonly _currentSession: IObservable; + readonly #currentSession: IObservable; + + readonly #editor: ICodeEditor; + readonly #instaService: IInstantiationService; + readonly #notebookEditorService: INotebookEditorService; + readonly #inlineChatSessionService: IInlineChatSessionService; + readonly #configurationService: IConfigurationService; + readonly #webContentExtractorService: ISharedWebContentExtractorService; + readonly #fileService: IFileService; + readonly #chatAttachmentResolveService: IChatAttachmentResolveService; + readonly #editorService: IEditorService; + readonly #markerDecorationsService: IMarkerDecorationsService; + readonly #languageModelService: ILanguageModelsService; + readonly #logService: ILogService; + readonly #chatEditingService: IChatEditingService; + readonly #chatService: IChatService; get widget(): EditorBasedInlineChatWidget { - return this._zone.value.widget; + return this.#zone.value.widget; } get isActive() { - return Boolean(this._currentSession.get()); + return Boolean(this.#currentSession.get()); } get inputWidget(): InlineChatInputWidget { - return this._inputWidget; + return this.#inputWidget; } constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + editor: ICodeEditor, + @IInstantiationService instaService: IInstantiationService, + @INotebookEditorService notebookEditorService: INotebookEditorService, + @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, - @IFileService private readonly _fileService: IFileService, - @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService, - @IEditorService private readonly _editorService: IEditorService, - @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, - @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, - @ILogService private readonly _logService: ILogService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IChatService private readonly _chatService: IChatService, + @IConfigurationService configurationService: IConfigurationService, + @ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService, + @IFileService fileService: IFileService, + @IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService, + @IEditorService editorService: IEditorService, + @IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService, + @ILanguageModelsService languageModelService: ILanguageModelsService, + @ILogService logService: ILogService, + @IChatEditingService chatEditingService: IChatEditingService, + @IChatService chatService: IChatService, ) { - const editorObs = observableCodeEditor(_editor); + this.#editor = editor; + this.#instaService = instaService; + this.#notebookEditorService = notebookEditorService; + this.#inlineChatSessionService = inlineChatSessionService; + this.#configurationService = configurationService; + this.#webContentExtractorService = webContentExtractorService; + this.#fileService = fileService; + this.#chatAttachmentResolveService = chatAttachmentResolveService; + this.#editorService = editorService; + this.#markerDecorationsService = markerDecorationsService; + this.#languageModelService = languageModelService; + this.#logService = logService; + this.#chatEditingService = chatEditingService; + this.#chatService = chatService; + + const editorObs = observableCodeEditor(editor); const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const ctxPendingConfirmation = CTX_INLINE_CHAT_PENDING_CONFIRMATION.bindTo(contextKeyService); - const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); - this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); + const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService); + this.#renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this.#configurationService); // Track whether the current editor's file is being edited by any chat editing session - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const model = editorObs.model.read(r); if (!model) { ctxFileBelongsToChat.set(false); return; } - const sessions = this._chatEditingService.editingSessionsObs.read(r); + const sessions = this.#chatEditingService.editingSessionsObs.read(r); let hasEdits = false; for (const session of sessions) { const entries = session.entries.read(r); @@ -185,25 +215,25 @@ export class InlineChatController implements IEditorContribution { ctxFileBelongsToChat.set(hasEdits); })); - const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); - const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); - this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); + const overlayWidget = this.#inputWidget = this.#store.add(this.#instaService.createInstance(InlineChatInputWidget, editorObs)); + const sessionOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); + this.inputOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatAffordance, this.#editor, overlayWidget)); - this._zone = new Lazy(() => { + this.#zone = new Lazy(() => { - assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); + assertType(this.#editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); const location: IChatWidgetLocationOptions = { location: ChatAgentLocation.EditorInline, resolveData: () => { - assertType(this._editor.hasModel()); - const wholeRange = this._editor.getSelection(); - const document = this._editor.getModel().uri; + assertType(this.#editor.hasModel()); + const wholeRange = this.#editor.getSelection(); + const document = this.#editor.getModel().uri; return { type: ChatAgentLocation.EditorInline, - id: getEditorId(this._editor, this._editor.getModel()), - selection: this._editor.getSelection(), + id: getEditorId(this.#editor, this.#editor.getModel()), + selection: this.#editor.getSelection(), document, wholeRange }; @@ -213,22 +243,22 @@ export class InlineChatController implements IEditorContribution { // inline chat in notebooks // check if this editor is part of a notebook editor // if so, update the location and use the notebook specific widget - const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor); + const notebookEditor = this.#notebookEditorService.getNotebookForPossibleCell(this.#editor); if (!!notebookEditor) { location.location = ChatAgentLocation.Notebook; if (notebookAgentConfig.get()) { location.resolveData = () => { - assertType(this._editor.hasModel()); + assertType(this.#editor.hasModel()); return { type: ChatAgentLocation.Notebook, - sessionInputUri: this._editor.getModel().uri, + sessionInputUri: this.#editor.getModel().uri, }; }; } } - const result = this._instaService.createInstance(InlineChatZoneWidget, + const result = this.#instaService.createInstance(InlineChatZoneWidget, location, { enableWorkingSet: 'implicit', @@ -248,33 +278,33 @@ export class InlineChatController implements IEditorContribution { }, defaultMode: ChatMode.Ask }, - { editor: this._editor, notebookEditor }, + { editor: this.#editor, notebookEditor }, () => Promise.resolve(), ); - this._store.add(result); + this.#store.add(result); result.domNode.classList.add('inline-chat-2'); return result; }); - const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); + const sessionsSignal = observableSignalFromEvent(this, this.#inlineChatSessionService.onDidChangeSessions); - this._currentSession = derived(r => { + this.#currentSession = derived(r => { sessionsSignal.read(r); const model = editorObs.model.read(r); - const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri); + const session = model && this.#inlineChatSessionService.getSessionByTextModel(model.uri); return session ?? undefined; }); let lastSession: IInlineChatSession2 | undefined = undefined; - this._store.add(autorun(r => { - const session = this._currentSession.read(r); + this.#store.add(autorun(r => { + const session = this.#currentSession.read(r); if (!session) { - this._isActiveController.set(false, undefined); + this.#isActiveController.set(false, undefined); if (lastSession && !lastSession.chatModel.hasRequests) { const state = lastSession.chatModel.inputModel.state.read(undefined); @@ -290,23 +320,24 @@ export class InlineChatController implements IEditorContribution { let foundOne = false; for (const editor of codeEditorService.listCodeEditors()) { - if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) { + const ctrl = InlineChatController.get(editor); + if (ctrl && ctrl.#isActiveController.read(undefined)) { foundOne = true; break; } } if (!foundOne && editorObs.isFocused.read(r)) { - this._isActiveController.set(true, undefined); + this.#isActiveController.set(true, undefined); } })); const visibleSessionObs = observableValue(this, undefined); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const model = editorObs.model.read(r); - const session = this._currentSession.read(r); - const isActive = this._isActiveController.read(r); + const session = this.#currentSession.read(r); + const isActive = this.#isActiveController.read(r); if (!session || !isActive || !model) { visibleSessionObs.set(undefined, undefined); @@ -322,38 +353,38 @@ export class InlineChatController implements IEditorContribution { }); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { // HIDE/SHOW const session = visibleSessionObs.read(r); - const renderMode = this._renderMode.read(r); + const renderMode = this.#renderMode.read(r); if (!session) { - this._zone.rawValue?.hide(); - this._zone.rawValue?.widget.chatWidget.setModel(undefined); - _editor.focus(); + this.#zone.rawValue?.hide(); + this.#zone.rawValue?.widget.chatWidget.setModel(undefined); + this.#editor.focus(); ctxInlineChatVisible.reset(); } else if (renderMode === 'hover') { // hover mode: set model but don't show zone, keep focus in editor - this._zone.value.widget.chatWidget.setModel(session.chatModel); - this._zone.rawValue?.hide(); + this.#zone.value.widget.chatWidget.setModel(session.chatModel); + this.#zone.rawValue?.hide(); ctxInlineChatVisible.set(true); } else { ctxInlineChatVisible.set(true); - this._zone.value.widget.chatWidget.setModel(session.chatModel); - if (!this._zone.value.position) { - this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); - this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug - this._zone.value.show(session.initialPosition); + this.#zone.value.widget.chatWidget.setModel(session.chatModel); + if (!this.#zone.value.position) { + this.#zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); + this.#zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug + this.#zone.value.show(session.initialPosition); } - this._zone.value.reveal(this._zone.value.position!); - this._zone.value.widget.focus(); + this.#zone.value.reveal(this.#zone.value.position!); + this.#zone.value.widget.focus(); } })); // Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); - const renderMode = this._renderMode.read(r); + const renderMode = this.#renderMode.read(r); if (!session || renderMode !== 'hover') { ctxPendingConfirmation.set(false); sessionOverlayWidget.hide(); @@ -375,7 +406,7 @@ export class InlineChatController implements IEditorContribution { } })); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); if (session) { const entries = session.editingSession.entries.read(r); @@ -393,7 +424,7 @@ export class InlineChatController implements IEditorContribution { for (const entry of otherEntries) { // OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend // that modifies other files - this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); + this.#editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); } } })); @@ -414,36 +445,36 @@ export class InlineChatController implements IEditorContribution { }); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const response = lastResponseObs.read(r); - this._zone.rawValue?.widget.updateInfo(''); + this.#zone.rawValue?.widget.updateInfo(''); if (!response?.isInProgress.read(r)) { if (response?.result?.errorDetails) { // ERROR case - this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); + this.#zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); alert(response.result.errorDetails.message); } // no response or not in progress - this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); - this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); + this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); + this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); } else { - this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); + this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); let placeholder = response.request?.message.text; const lastProgress = lastResponseProgressObs.read(r); if (lastProgress) { placeholder = renderAsPlaintext(lastProgress.content); } - this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); + this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); } })); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); if (!session) { return; @@ -456,25 +487,25 @@ export class InlineChatController implements IEditorContribution { })); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const session = visibleSessionObs.read(r); const entry = session?.editingSession.readEntry(session.uri, r); // make sure there is an editor integration - const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor)); + const pane = this.#editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this.#editor || isNotebookWithCellEditor(candidate, this.#editor)); if (pane && entry) { entry?.getEditorIntegration(pane); } // make sure the ZONE isn't inbetween a diff and move above if so - if (entry?.diffInfo && this._zone.value.position) { - const { position } = this._zone.value; + if (entry?.diffInfo && this.#zone.value.position) { + const { position } = this.#zone.value; const diff = entry.diffInfo.read(r); for (const change of diff.changes) { if (change.modified.contains(position.lineNumber)) { - this._zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1)); + this.#zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1)); break; } } @@ -483,90 +514,90 @@ export class InlineChatController implements IEditorContribution { } dispose(): void { - this._store.dispose(); + this.#store.dispose(); } getWidgetPosition(): Position | undefined { - return this._zone.rawValue?.position; + return this.#zone.rawValue?.position; } focus() { - this._zone.rawValue?.widget.focus(); + this.#zone.rawValue?.widget.focus(); } async run(arg?: InlineChatRunOptions): Promise { - assertType(this._editor.hasModel()); - const uri = this._editor.getModel().uri; + assertType(this.#editor.hasModel()); + const uri = this.#editor.getModel().uri; - const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri); + const existingSession = this.#inlineChatSessionService.getSessionByTextModel(uri); if (existingSession) { await existingSession.editingSession.accept(); existingSession.dispose(); } - this._isActiveController.set(true, undefined); + this.#isActiveController.set(true, undefined); - const session = this._inlineChatSessionService.createSession(this._editor); + const session = this.#inlineChatSessionService.createSession(this.#editor); // Store for tracking model changes during this session const sessionStore = new DisposableStore(); try { - await this._applyModelDefaults(session, sessionStore); + await this.#applyModelDefaults(session, sessionStore); if (arg) { - arg.attachDiagnostics ??= this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone'; + arg.attachDiagnostics ??= this.#configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone'; } // ADD diagnostics (only when explicitly requested) if (arg?.attachDiagnostics) { const entries: IChatRequestVariableEntry[] = []; - for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) { - if (range.intersectRanges(this._editor.getSelection())) { + for (const [range, marker] of this.#markerDecorationsService.getLiveMarkers(uri)) { + if (range.intersectRanges(this.#editor.getSelection())) { const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter)); } } if (entries.length > 0) { - this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries); + this.#zone.value.widget.chatWidget.attachmentModel.addContext(...entries); const msg = entries.length > 1 ? localize('fixN', "Fix the attached problems") : localize('fix1', "Fix the attached problem"); - this._zone.value.widget.chatWidget.input.setValue(msg, true); + this.#zone.value.widget.chatWidget.input.setValue(msg, true); arg.message = msg; - this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + this.#zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } } // Check args if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { if (arg.initialRange) { - this._editor.revealRange(arg.initialRange); + this.#editor.revealRange(arg.initialRange); } if (arg.initialSelection) { - this._editor.setSelection(arg.initialSelection); + this.#editor.setSelection(arg.initialSelection); } if (arg.attachments) { await Promise.all(arg.attachments.map(async attachment => { - await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment); + await this.#zone.value.widget.chatWidget.attachmentModel.addFile(attachment); })); delete arg.attachments; } if (arg.modelSelector) { - const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); + const id = (await this.#languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); if (!id) { throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`); } - const model = this._languageModelService.lookupLanguageModel(id); + const model = this.#languageModelService.lookupLanguageModel(id); if (!model) { throw new Error(`Language model not loaded: ${id}.`); } - this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); + this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); } if (arg.message) { - this._zone.value.widget.chatWidget.setInput(arg.message); + this.#zone.value.widget.chatWidget.setInput(arg.message); if (arg.autoSend) { - await this._zone.value.widget.chatWidget.acceptInput(); + await this.#zone.value.widget.chatWidget.acceptInput(); } } } @@ -592,7 +623,7 @@ export class InlineChatController implements IEditorContribution { } async acceptSession() { - const session = this._currentSession.get(); + const session = this.#currentSession.get(); if (!session) { return; } @@ -601,23 +632,23 @@ export class InlineChatController implements IEditorContribution { } async rejectSession() { - const session = this._currentSession.get(); + const session = this.#currentSession.get(); if (!session) { return; } - this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); + this.#chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); await session.editingSession.reject(); session.dispose(); } - private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise { - const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get(); + async #selectVendorDefaultModel(session: IInlineChatSession2): Promise { + const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get(); if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { - const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); + const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); for (const identifier of ids) { - const candidate = this._languageModelService.lookupLanguageModel(identifier); + const candidate = this.#languageModelService.lookupLanguageModel(identifier); if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) { - this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); + this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); break; } } @@ -628,39 +659,39 @@ export class InlineChatController implements IEditorContribution { * Applies model defaults based on settings and tracks user model changes. * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ - private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { - const userSelectedModel = InlineChatController._userSelectedModel; - const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); + async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { + const userSelectedModel = InlineChatController.#userSelectedModel; + const defaultModelSetting = this.#configurationService.getValue(InlineChatConfigKeys.DefaultModel); let modelApplied = false; // 1. Try user's explicitly chosen model from a previous inline chat in the same session if (userSelectedModel) { - modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); + modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); if (!modelApplied) { // User's previously selected model is no longer available, clear it - InlineChatController._userSelectedModel = undefined; + InlineChatController.#userSelectedModel = undefined; } } // 2. Try inlineChat.defaultModel setting if (!modelApplied && defaultModelSetting) { - modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); + modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); if (!modelApplied) { - this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); + this.#logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); } } // 3. Fall back to vendor default if (!modelApplied) { - await this._selectVendorDefaultModel(session); + await this.#selectVendorDefaultModel(session); } // Track model changes - store user's explicit choice in the given sessions. // NOTE: This currently detects any model change, not just user-initiated ones. let initialModelId: string | undefined; sessionStore.add(autorun(r => { - const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); + const newModel = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); if (!newModel) { return; } @@ -670,25 +701,25 @@ export class InlineChatController implements IEditorContribution { } if (initialModelId !== newModel.identifier) { // User explicitly changed model, store their choice as qualified name - InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); + InlineChatController.#userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); initialModelId = newModel.identifier; } })); } async createImageAttachment(attachment: URI): Promise { - const value = this._currentSession.get(); + const value = this.#currentSession.get(); if (!value) { return undefined; } if (attachment.scheme === Schemas.file) { - if (await this._fileService.canHandleResource(attachment)) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); + if (await this.#fileService.canHandleResource(attachment)) { + return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); } } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); + const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None); if (extractedImages) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); + return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); } } return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index e7773395fce..361441642d6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -33,12 +33,13 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j class QuickFixActionViewItem extends MenuEntryActionViewItem { - private readonly _lightBulbStore = this._store.add(new MutableDisposable()); - private _currentTitle: string | undefined; + readonly #lightBulbStore = this._store.add(new MutableDisposable()); + readonly #editor: ICodeEditor; + #currentTitle: string | undefined; constructor( action: MenuItemAction, - private readonly _editor: ICodeEditor, + editor: ICodeEditor, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -55,7 +56,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { elementGetter: () => HTMLElement | undefined = () => undefined; override async run(...args: unknown[]): Promise { - const controller = CodeActionController.get(_editor); + const controller = CodeActionController.get(editor); const info = controller?.lightBulbState.get(); const element = this.elementGetter(); if (controller && info && element) { @@ -67,26 +68,27 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { super(wrappedAction, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); + this.#editor = editor; wrappedAction.elementGetter = () => this.element; } override render(container: HTMLElement): void { super.render(container); - this._updateFromLightBulb(); + this.#updateFromLightBulb(); } protected override getTooltip(): string { - return this._currentTitle ?? super.getTooltip(); + return this.#currentTitle ?? super.getTooltip(); } - private _updateFromLightBulb(): void { - const controller = CodeActionController.get(this._editor); + #updateFromLightBulb(): void { + const controller = CodeActionController.get(this.#editor); if (!controller) { return; } const store = new DisposableStore(); - this._lightBulbStore.value = store; + this.#lightBulbStore.value = store; store.add(autorun(reader => { const info = controller.lightBulbState.read(reader); @@ -99,7 +101,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { } // Update tooltip - this._currentTitle = info?.title; + this.#currentTitle = info?.title; this.updateTooltip(); })); } @@ -107,7 +109,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { - private readonly _kbLabel: string | undefined; + readonly #kbLabel: string | undefined; constructor( action: MenuItemAction, @@ -121,14 +123,14 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); this.options.label = true; this.options.icon = false; - this._kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined; + this.#kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined; } protected override updateLabel(): void { if (this.label) { dom.reset(this.label, this.action.label, - ...(this._kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this._kbLabel)] : []) + ...(this.#kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this.#kbLabel)] : []) ); } } @@ -140,38 +142,42 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { */ export class InlineChatEditorAffordance extends Disposable implements IContentWidget { - private static _idPool = 0; + static #idPool = 0; - private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`; - private readonly _domNode: HTMLElement; - private _position: IContentWidgetPosition | null = null; - private _isVisible = false; + readonly #id = `inline-chat-content-widget-${InlineChatEditorAffordance.#idPool++}`; + readonly #domNode: HTMLElement; + #position: IContentWidgetPosition | null = null; + #isVisible = false; - private readonly _onDidRunAction = this._store.add(new Emitter()); - readonly onDidRunAction: Event = this._onDidRunAction.event; + readonly #onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this.#onDidRunAction.event; readonly allowEditorOverflow = true; readonly suppressMouseDown = false; + readonly #editor: ICodeEditor; + constructor( - private readonly _editor: ICodeEditor, + editor: ICodeEditor, selection: IObservable, @IInstantiationService instantiationService: IInstantiationService, ) { super(); + this.#editor = editor; + // Create the widget DOM - this._domNode = dom.$('.inline-chat-content-widget'); + this.#domNode = dom.$('.inline-chat-content-widget'); // Create toolbar with the inline chat start action - const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, { + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#domNode, MenuId.InlineChatEditorAffordance, { telemetrySource: 'inlineChatEditorAffordance', hiddenItemStrategy: HiddenItemStrategy.Ignore, menuOptions: { renderShortTitle: true }, toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true }, actionViewItemProvider: (action: IAction) => { if (action instanceof MenuItemAction && action.id === quickFixCommandId) { - return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); + return instantiationService.createInstance(QuickFixActionViewItem, action, this.#editor); } if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT || action.id === 'inlineChat.fixDiagnostics')) { return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action); @@ -180,37 +186,37 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi } })); this._store.add(toolbar.actionRunner.onDidRun((e) => { - this._onDidRunAction.fire(e.action.id); - this._hide(); + this.#onDidRunAction.fire(e.action.id); + this.#hide(); })); this._store.add(autorun(r => { const sel = selection.read(r); if (sel) { - this._show(sel); + this.#show(sel); } else { - this._hide(); + this.#hide(); } })); } - private _show(selection: Selection): void { + #show(selection: Selection): void { if (selection.isEmpty()) { - this._showAtLineStart(selection.getPosition().lineNumber); + this.#showAtLineStart(selection.getPosition().lineNumber); } else { - this._showAtSelection(selection); + this.#showAtSelection(selection); } - if (this._isVisible) { - this._editor.layoutContentWidget(this); + if (this.#isVisible) { + this.#editor.layoutContentWidget(this); } else { - this._editor.addContentWidget(this); - this._isVisible = true; + this.#editor.addContentWidget(this); + this.#isVisible = true; } } - private _showAtSelection(selection: Selection): void { + #showAtSelection(selection: Selection): void { const cursorPosition = selection.getPosition(); const direction = selection.getDirection(); @@ -218,20 +224,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; - this._position = { + this.#position = { position: cursorPosition, preference: [preference], }; } - private _showAtLineStart(lineNumber: number): void { - const model = this._editor.getModel(); + #showAtLineStart(lineNumber: number): void { + const model = this.#editor.getModel(); if (!model) { return; } const tabSize = model.getOptions().tabSize; - const fontInfo = this._editor.getOptions().get(EditorOption.fontInfo); + const fontInfo = this.#editor.getOptions().get(EditorOption.fontInfo); const lineContent = model.getLineContent(lineNumber); const indent = computeIndentLevel(lineContent, tabSize); const lineHasSpace = indent < 0 ? true : fontInfo.spaceWidth * indent > 22; @@ -254,43 +260,43 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi const effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1; - this._position = { + this.#position = { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: [ContentWidgetPositionPreference.EXACT], }; } - private _hide(): void { - if (this._isVisible) { - this._isVisible = false; - this._editor.removeContentWidget(this); + #hide(): void { + if (this.#isVisible) { + this.#isVisible = false; + this.#editor.removeContentWidget(this); } } getId(): string { - return this._id; + return this.#id; } getDomNode(): HTMLElement { - return this._domNode; + return this.#domNode; } getPosition(): IContentWidgetPosition | null { - return this._position; + return this.#position; } beforeRender(): IDimension | null { - const position = this._editor.getPosition(); - const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight); + const position = this.#editor.getPosition(); + const lineHeight = position ? this.#editor.getLineHeightForPosition(position) : this.#editor.getOption(EditorOption.lineHeight); - this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); + this.#domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); return null; } override dispose(): void { - if (this._isVisible) { - this._editor.removeContentWidget(this); + if (this.#isVisible) { + this.#editor.removeContentWidget(this); } super.dispose(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 3d82cec90ec..03a77669046 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -26,8 +26,8 @@ import { IUserInteractionService } from '../../../../platform/userInteraction/br export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { - private readonly _onDidRunAction = this._store.add(new Emitter()); - readonly onDidRunAction: Event = this._onDidRunAction.event; + readonly #onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this.#onDidRunAction.event; constructor( myEditorObs: ObservableCodeEditor, @@ -108,6 +108,6 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { this._store.add(menu); - this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); + this._store.add(this.onDidCloseWithCommand(commandId => this.#onDidRunAction.fire(commandId))); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index 539e8197ee0..ca722843a32 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -14,7 +14,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js export class InlineChatNotebookContribution { - private readonly _store = new DisposableStore(); + readonly #store = new DisposableStore(); constructor( @IInlineChatSessionService sessionService: IInlineChatSessionService, @@ -22,7 +22,7 @@ export class InlineChatNotebookContribution { @INotebookEditorService notebookEditorService: INotebookEditorService, ) { - this._store.add(sessionService.onWillStartSession(newSessionEditor => { + this.#store.add(sessionService.onWillStartSession(newSessionEditor => { const candidate = CellUri.parse(newSessionEditor.getModel().uri); if (!candidate) { return; @@ -51,6 +51,6 @@ export class InlineChatNotebookContribution { } dispose(): void { - this._store.dispose(); + this.#store.dispose(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 04a76d7327a..5112e2fe443 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -44,51 +44,58 @@ import { assertType } from '../../../../base/common/types.js'; */ export class InlineChatInputWidget extends Disposable { - private readonly _domNode: HTMLElement; - private readonly _container: HTMLElement; - private readonly _inputContainer: HTMLElement; - private readonly _toolbarContainer: HTMLElement; - private readonly _input: IActiveCodeEditor; - private readonly _position = observableValue(this, null); - readonly position: IObservable = this._position; + readonly #domNode: HTMLElement; + readonly #container: HTMLElement; + readonly #inputContainer: HTMLElement; + readonly #toolbarContainer: HTMLElement; + readonly #input: IActiveCodeEditor; + readonly #position = observableValue(this, null); + readonly position: IObservable = this.#position; - private readonly _showStore = this._store.add(new DisposableStore()); - private readonly _stickyScrollHeight: IObservable; - private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; - private _anchorLineNumber: number = 0; - private _anchorLeft: number = 0; - private _anchorAbove: boolean = false; + readonly #showStore = this._store.add(new DisposableStore()); + readonly #stickyScrollHeight: IObservable; + readonly #layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; + #anchorLineNumber: number = 0; + #anchorLeft: number = 0; + #anchorAbove: boolean = false; + readonly #editorObs: ObservableCodeEditor; + readonly #contextKeyService: IContextKeyService; + readonly #menuService: IMenuService; constructor( - private readonly _editorObs: ObservableCodeEditor, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IMenuService private readonly _menuService: IMenuService, + editorObs: ObservableCodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, ) { super(); + this.#editorObs = editorObs; + this.#contextKeyService = contextKeyService; + this.#menuService = menuService; + // Create container - this._domNode = dom.$('.inline-chat-gutter-menu'); + this.#domNode = dom.$('.inline-chat-gutter-menu'); // Create inner container (background + focus border) - this._container = dom.append(this._domNode, dom.$('.inline-chat-gutter-container')); + this.#container = dom.append(this.#domNode, dom.$('.inline-chat-gutter-container')); // Create input editor container - this._inputContainer = dom.append(this._container, dom.$('.input')); + this.#inputContainer = dom.append(this.#container, dom.$('.input')); // Create toolbar container - this._toolbarContainer = dom.append(this._container, dom.$('.toolbar')); + this.#toolbarContainer = dom.append(this.#container, dom.$('.toolbar')); // Create vertical actions bar below the input container - const actionsContainer = dom.append(this._domNode, dom.$('.inline-chat-gutter-actions')); + const actionsContainer = dom.append(this.#domNode, dom.$('.inline-chat-gutter-actions')); const actionBar = this._store.add(new ActionBar(actionsContainer, { orientation: ActionsOrientation.VERTICAL, preventLoopNavigation: true, })); - const actionsMenu = this._store.add(this._menuService.createMenu(MenuId.ChatEditorInlineMenu, this._contextKeyService)); + const actionsMenu = this._store.add(this.#menuService.createMenu(MenuId.ChatEditorInlineMenu, this.#contextKeyService)); const updateActions = () => { const actions = getFlatActionBarActions(actionsMenu.getActions({ shouldForwardArgs: true })); actionBar.clear(); @@ -123,13 +130,13 @@ export class InlineChatInputWidget extends Disposable { ]) }; - this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + this.#input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this.#inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); - this._input.setModel(model); + this.#input.setModel(model); // Create toolbar - const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MenuId.InlineChatInput, { + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#toolbarContainer, MenuId.InlineChatInput, { telemetrySource: 'inlineChatInput.toolbar', hiddenItemStrategy: HiddenItemStrategy.NoHide, toolbarOptions: { @@ -139,8 +146,8 @@ export class InlineChatInputWidget extends Disposable { })); // Initialize sticky scroll height observable - const stickyScrollController = StickyScrollController.get(this._editorObs.editor); - this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + const stickyScrollController = StickyScrollController.get(this.#editorObs.editor); + this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); // Track toolbar width changes const toolbarWidth = observableValue(this, 0); @@ -150,24 +157,24 @@ export class InlineChatInputWidget extends Disposable { this._store.add(resizeObserver); this._store.add(resizeObserver.observe(toolbar.getElement())); - const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); - const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); + const contentWidth = observableFromEvent(this, this.#input.onDidChangeModelContent, () => this.#input.getContentWidth()); + const contentHeight = observableFromEvent(this, this.#input.onDidContentSizeChange, () => this.#input.getContentHeight()); - this._layoutData = derived(r => { + this.#layoutData = derived(r => { const editorPad = 6; const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); const minWidth = 220; const maxWidth = 600; - const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' + const clampedWidth = this.#input.getOption(EditorOption.wordWrap) === 'on' ? maxWidth : Math.max(minWidth, Math.min(totalWidth, maxWidth)); - const lineHeight = this._input.getOption(EditorOption.lineHeight); + const lineHeight = this.#input.getOption(EditorOption.lineHeight); const clampedHeight = Math.min(contentHeight.read(r), (3 * lineHeight)); if (totalWidth > clampedWidth) { // enable word wrap - this._input.updateOptions({ wordWrap: 'on', }); + this.#input.updateOptions({ wordWrap: 'on', }); } return { @@ -180,42 +187,42 @@ export class InlineChatInputWidget extends Disposable { // Update container width and editor layout when width changes this._store.add(autorun(r => { - const { editorPad, toolbarWidth, totalWidth, height } = this._layoutData.read(r); + const { editorPad, toolbarWidth, totalWidth, height } = this.#layoutData.read(r); const inputWidth = totalWidth - toolbarWidth - editorPad; - this._container.style.width = `${totalWidth}px`; - this._inputContainer.style.width = `${inputWidth}px`; - this._input.layout({ width: inputWidth, height }); + this.#container.style.width = `${totalWidth}px`; + this.#inputContainer.style.width = `${inputWidth}px`; + this.#input.layout({ width: inputWidth, height }); })); // Toggle focus class on the container - this._store.add(this._input.onDidFocusEditorText(() => this._container.classList.add('focused'))); - this._store.add(this._input.onDidBlurEditorText(() => this._container.classList.remove('focused'))); + this._store.add(this.#input.onDidFocusEditorText(() => this.#container.classList.add('focused'))); + this._store.add(this.#input.onDidBlurEditorText(() => this.#container.classList.remove('focused'))); // Toggle scroll decoration on the toolbar - this._store.add(this._input.onDidScrollChange(e => { - this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); + this._store.add(this.#input.onDidScrollChange(e => { + this.#toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); })); // Track input text for context key and adjust width based on content - const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService); - this._store.add(this._input.onDidChangeModelContent(() => { - inputHasText.set(this._input.getModel().getValue().trim().length > 0); + const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this.#contextKeyService); + this._store.add(this.#input.onDidChangeModelContent(() => { + inputHasText.set(this.#input.getModel().getValue().trim().length > 0); })); this._store.add(toDisposable(() => inputHasText.reset())); // Track focus state - const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService); - this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); - this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); + const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this.#contextKeyService); + this._store.add(this.#input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); + this._store.add(this.#input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); this._store.add(toDisposable(() => inputWidgetFocused.reset())); // Handle key events: ArrowDown to move to actions - this._store.add(this._input.onKeyDown(e => { + this._store.add(this.#input.onKeyDown(e => { if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { - const model = this._input.getModel(); - const position = this._input.getPosition(); + const model = this.#input.getModel(); + const position = this.#input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { e.preventDefault(); e.stopPropagation(); @@ -237,18 +244,18 @@ export class InlineChatInputWidget extends Disposable { if (firstItem?.element && dom.isAncestorOfActiveElement(firstItem.element)) { event.preventDefault(); event.stopPropagation(); - this._input.focus(); + this.#input.focus(); } } }, true)); // Track focus - hide when focus leaves - const focusTracker = this._store.add(dom.trackFocus(this._domNode)); + const focusTracker = this._store.add(dom.trackFocus(this.#domNode)); this._store.add(focusTracker.onDidBlur(() => this.hide())); } get value(): string { - return this._input.getModel().getValue().trim(); + return this.#input.getModel().getValue().trim(); } /** @@ -258,77 +265,77 @@ export class InlineChatInputWidget extends Disposable { * @param anchorAbove Whether to anchor above the position (widget grows upward) */ show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { - this._showStore.clear(); + this.#showStore.clear(); // Clear input state - this._input.updateOptions({ wordWrap: 'off', placeholder }); - this._input.getModel().setValue(''); + this.#input.updateOptions({ wordWrap: 'off', placeholder }); + this.#input.getModel().setValue(''); // Store anchor info for scroll updates - this._anchorLineNumber = lineNumber; - this._anchorLeft = left; - this._anchorAbove = anchorAbove; + this.#anchorLineNumber = lineNumber; + this.#anchorLeft = left; + this.#anchorAbove = anchorAbove; // Set initial position - this._updatePosition(); + this.#updatePosition(); // Create overlay widget via observable pattern - this._showStore.add(this._editorObs.createOverlayWidget({ - domNode: this._domNode, - position: this._position, + this.#showStore.add(this.#editorObs.createOverlayWidget({ + domNode: this.#domNode, + position: this.#position, minContentWidthInPx: constObservable(0), allowEditorOverflow: true, })); // If anchoring above, adjust position after render to account for widget height if (anchorAbove) { - this._updatePosition(); + this.#updatePosition(); } // Update position on scroll, hide if anchor line is out of view (only when input is empty) - this._showStore.add(this._editorObs.editor.onDidScrollChange(() => { - const visibleRanges = this._editorObs.editor.getVisibleRanges(); + this.#showStore.add(this.#editorObs.editor.onDidScrollChange(() => { + const visibleRanges = this.#editorObs.editor.getVisibleRanges(); const isLineVisible = visibleRanges.some(range => - this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber + this.#anchorLineNumber >= range.startLineNumber && this.#anchorLineNumber <= range.endLineNumber ); - const hasContent = !!this._input.getModel().getValue(); + const hasContent = !!this.#input.getModel().getValue(); if (!isLineVisible && !hasContent) { this.hide(); } else { - this._updatePosition(); + this.#updatePosition(); } })); // Focus the input editor - setTimeout(() => this._input.focus(), 0); + setTimeout(() => this.#input.focus(), 0); } - private _updatePosition(): void { - const editor = this._editorObs.editor; + #updatePosition(): void { + const editor = this.#editorObs.editor; const lineHeight = editor.getOption(EditorOption.lineHeight); - const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop(); + const top = editor.getTopForLineNumber(this.#anchorLineNumber) - editor.getScrollTop(); let adjustedTop = top; - if (this._anchorAbove) { - const widgetHeight = this._domNode.offsetHeight; + if (this.#anchorAbove) { + const widgetHeight = this.#domNode.offsetHeight; adjustedTop = top - widgetHeight; } else { adjustedTop = top + lineHeight; } // Clamp to viewport bounds when anchor line is out of view - const stickyScrollHeight = this._stickyScrollHeight.get(); + const stickyScrollHeight = this.#stickyScrollHeight.get(); const layoutInfo = editor.getLayoutInfo(); - const widgetHeight = this._domNode.offsetHeight; + const widgetHeight = this.#domNode.offsetHeight; const minTop = stickyScrollHeight; const maxTop = layoutInfo.height - widgetHeight; const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop)); const isClamped = clampedTop !== adjustedTop; - this._domNode.classList.toggle('clamped', isClamped); + this.#domNode.classList.toggle('clamped', isClamped); - this._position.set({ - preference: { top: clampedTop, left: this._anchorLeft }, + this.#position.set({ + preference: { top: clampedTop, left: this.#anchorLeft }, stackOrdinal: 10000, }, undefined); } @@ -338,13 +345,13 @@ export class InlineChatInputWidget extends Disposable { */ hide(): void { // Focus editor if focus is still within the editor's DOM - const editorDomNode = this._editorObs.editor.getDomNode(); + const editorDomNode = this.#editorObs.editor.getDomNode(); if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) { - this._editorObs.editor.focus(); + this.#editorObs.editor.focus(); } - this._position.set(null, undefined); - this._input.getModel().setValue(''); - this._showStore.clear(); + this.#position.set(null, undefined); + this.#input.getModel().setValue(''); + this.#showStore.clear(); } } @@ -353,52 +360,62 @@ export class InlineChatInputWidget extends Disposable { */ export class InlineChatSessionOverlayWidget extends Disposable { - private readonly _domNode: HTMLElement = document.createElement('div'); - private readonly _container: HTMLElement; - private readonly _statusNode: HTMLElement; - private readonly _icon: HTMLElement; - private readonly _message: HTMLElement; - private readonly _toolbarNode: HTMLElement; + readonly #domNode: HTMLElement = document.createElement('div'); + readonly #container: HTMLElement; + readonly #statusNode: HTMLElement; + readonly #icon: HTMLElement; + readonly #message: HTMLElement; + readonly #toolbarNode: HTMLElement; - private readonly _showStore = this._store.add(new DisposableStore()); - private readonly _position = observableValue(this, null); - private readonly _minContentWidthInPx = constObservable(0); + readonly #showStore = this._store.add(new DisposableStore()); + readonly #position = observableValue(this, null); + readonly #minContentWidthInPx = constObservable(0); - private readonly _stickyScrollHeight: IObservable; + readonly #stickyScrollHeight: IObservable; + + readonly #editorObs: ObservableCodeEditor; + readonly #instaService: IInstantiationService; + readonly #keybindingService: IKeybindingService; + readonly #logService: ILogService; constructor( - private readonly _editorObs: ObservableCodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ILogService private readonly _logService: ILogService, + editorObs: ObservableCodeEditor, + @IInstantiationService instaService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @ILogService logService: ILogService, ) { super(); - this._domNode.classList.add('inline-chat-session-overlay-widget'); + this.#editorObs = editorObs; + this.#instaService = instaService; + this.#keybindingService = keybindingService; + this.#logService = logService; - this._container = document.createElement('div'); - this._domNode.appendChild(this._container); - this._container.classList.add('inline-chat-session-overlay-container'); + this.#domNode.classList.add('inline-chat-session-overlay-widget'); + + this.#container = document.createElement('div'); + this.#domNode.appendChild(this.#container); + this.#container.classList.add('inline-chat-session-overlay-container'); // Create status node with icon and message - this._statusNode = document.createElement('div'); - this._statusNode.classList.add('status'); - this._icon = dom.append(this._statusNode, dom.$('span')); - this._message = dom.append(this._statusNode, dom.$('span.message')); - this._container.appendChild(this._statusNode); + this.#statusNode = document.createElement('div'); + this.#statusNode.classList.add('status'); + this.#icon = dom.append(this.#statusNode, dom.$('span')); + this.#message = dom.append(this.#statusNode, dom.$('span.message')); + this.#container.appendChild(this.#statusNode); // Create toolbar node - this._toolbarNode = document.createElement('div'); - this._toolbarNode.classList.add('toolbar'); + this.#toolbarNode = document.createElement('div'); + this.#toolbarNode.classList.add('toolbar'); // Initialize sticky scroll height observable - const stickyScrollController = StickyScrollController.get(this._editorObs.editor); - this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + const stickyScrollController = StickyScrollController.get(this.#editorObs.editor); + this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); } show(session: IInlineChatSession2): void { - assertType(this._editorObs.editor.hasModel()); - this._showStore.clear(); + assertType(this.#editorObs.editor.hasModel()); + this.#showStore.clear(); // Derived entry observable for this session const entry = derived(r => session.editingSession.readEntry(session.uri, r)); @@ -458,34 +475,34 @@ export class InlineChatSessionOverlayWidget extends Disposable { } }); - this._showStore.add(autorun(r => { + this.#showStore.add(autorun(r => { const value = requestMessage.read(r); if (value) { - this._message.innerText = renderAsPlaintext(value.message); - this._icon.className = ''; - this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); + this.#message.innerText = renderAsPlaintext(value.message); + this.#icon.className = ''; + this.#icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); } else { - this._message.innerText = ''; - this._icon.className = ''; + this.#message.innerText = ''; + this.#icon.className = ''; } })); // Log when pending confirmation changes - this._showStore.add(autorun(r => { + this.#showStore.add(autorun(r => { const response = session.chatModel.lastRequestObs.read(r)?.response; const pending = response?.isPendingConfirmation.read(r); if (pending) { - this._logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`); + this.#logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`); } })); // Add toolbar - this._container.appendChild(this._toolbarNode); - this._showStore.add(toDisposable(() => this._toolbarNode.remove())); + this.#container.appendChild(this.#toolbarNode); + this.#showStore.add(toDisposable(() => this.#toolbarNode.remove())); const that = this; - this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditorInlineExecute, { + this.#showStore.add(this.#instaService.createInstance(MenuWorkbenchToolBar, this.#toolbarNode, MenuId.ChatEditorInlineExecute, { telemetrySource: 'inlineChatProgress.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -501,52 +518,52 @@ export class InlineChatSessionOverlayWidget extends Disposable { return undefined; // use default action view item with label } - return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that._keybindingService, primaryActions); + return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that.#keybindingService, primaryActions); } })); // Position in top right of editor, below sticky scroll - const lineHeight = this._editorObs.getOption(EditorOption.lineHeight); + const lineHeight = this.#editorObs.getOption(EditorOption.lineHeight); // Track widget width changes const widgetWidth = observableValue(this, 0); const resizeObserver = new dom.DisposableResizeObserver(() => { - widgetWidth.set(this._domNode.offsetWidth, undefined); + widgetWidth.set(this.#domNode.offsetWidth, undefined); }); - this._showStore.add(resizeObserver); - this._showStore.add(resizeObserver.observe(this._domNode)); + this.#showStore.add(resizeObserver); + this.#showStore.add(resizeObserver.observe(this.#domNode)); - this._showStore.add(autorun(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - const stickyScrollHeight = this._stickyScrollHeight.read(r); + this.#showStore.add(autorun(r => { + const layoutInfo = this.#editorObs.layoutInfo.read(r); + const stickyScrollHeight = this.#stickyScrollHeight.read(r); const width = widgetWidth.read(r); const padding = Math.round(lineHeight.read(r) * 2 / 3); // Cap max-width to the editor viewport (content area) const maxWidth = layoutInfo.contentWidth - 2 * padding; - this._domNode.style.maxWidth = `${maxWidth}px`; + this.#domNode.style.maxWidth = `${maxWidth}px`; // Position: top right, below sticky scroll with padding, left of minimap and scrollbar const top = stickyScrollHeight + padding; const left = layoutInfo.width - width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - padding; - this._position.set({ + this.#position.set({ preference: { top, left }, stackOrdinal: 10000, }, undefined); })); // Create overlay widget - this._showStore.add(this._editorObs.createOverlayWidget({ - domNode: this._domNode, - position: this._position, - minContentWidthInPx: this._minContentWidthInPx, + this.#showStore.add(this.#editorObs.createOverlayWidget({ + domNode: this.#domNode, + position: this.#position, + minContentWidthInPx: this.#minContentWidthInPx, allowEditorOverflow: false, })); } hide(): void { - this._position.set(null, undefined); - this._showStore.clear(); + this.#position.set(null, undefined); + this.#showStore.clear(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 4a008a59b0f..4f36ec8bba0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -41,55 +41,58 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; - private readonly _store = new DisposableStore(); - private readonly _sessions = new ResourceMap(); + readonly #store = new DisposableStore(); + readonly #sessions = new ResourceMap(); - private readonly _onWillStartSession = this._store.add(new Emitter()); - readonly onWillStartSession: Event = this._onWillStartSession.event; + readonly #onWillStartSession = this.#store.add(new Emitter()); + readonly onWillStartSession: Event = this.#onWillStartSession.event; - private readonly _onDidChangeSessions = this._store.add(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + readonly #onDidChangeSessions = this.#store.add(new Emitter()); + readonly onDidChangeSessions: Event = this.#onDidChangeSessions.event; + + readonly #chatService: IChatService; constructor( - @IChatService private readonly _chatService: IChatService, + @IChatService chatService: IChatService, @IChatAgentService chatAgentService: IChatAgentService, ) { + this.#chatService = chatService; // Listen for agent changes and dispose all sessions when there is no agent const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const agent = agentObs.read(r); if (!agent) { // No agent available, dispose all sessions - dispose(this._sessions.values()); - this._sessions.clear(); + dispose(this.#sessions.values()); + this.#sessions.clear(); } })); } dispose() { - this._store.dispose(); + this.#store.dispose(); } createSession(editor: IActiveCodeEditor): IInlineChatSession2 { const uri = editor.getModel().uri; - if (this._sessions.has(uri)) { + if (this.#sessions.has(uri)) { throw new Error('Session already exists'); } - this._onWillStartSession.fire(editor); + this.#onWillStartSession.fire(editor); - const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); + const chatModelRef = this.#chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); const chatModel = chatModelRef.object; chatModel.startEditingSession(false); const store = new DisposableStore(); store.add(toDisposable(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); + this.#chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); chatModel.editingSession?.reject(); - this._sessions.delete(uri); - this._onDidChangeSessions.fire(this); + this.#sessions.delete(uri); + this.#onDidChangeSessions.fire(this); })); store.add(chatModelRef); @@ -104,7 +107,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) { const response = chatModel.getRequests().at(-1)?.response; if (response) { - this._chatService.notifyUserAction({ + this.#chatService.notifyUserAction({ sessionResource: response.session.sessionResource, requestId: response.requestId, agentId: response.agent?.id, @@ -138,16 +141,16 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { editingSession: chatModel.editingSession!, dispose: store.dispose.bind(store) }; - this._sessions.set(uri, result); - this._onDidChangeSessions.fire(this); + this.#sessions.set(uri, result); + this.#onDidChangeSessions.fire(this); return result; } getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { - let result = this._sessions.get(uri); + let result = this.#sessions.get(uri); if (!result) { // no direct session, try to find an editing session which has a file entry for the uri - for (const [_, candidate] of this._sessions) { + for (const [_, candidate] of this.#sessions) { const entry = candidate.editingSession.getEntry(uri); if (entry) { result = candidate; @@ -159,7 +162,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { - for (const session of this._sessions.values()) { + for (const session of this.#sessions.values()) { if (isEqual(session.chatModel.sessionResource, sessionResource)) { return session; } @@ -172,11 +175,11 @@ export class InlineChatEnabler { static Id = 'inlineChat.enabler'; - private readonly _ctxHasProvider2: IContextKey; - private readonly _ctxHasNotebookProvider: IContextKey; - private readonly _ctxPossible: IContextKey; + readonly #ctxHasProvider2: IContextKey; + readonly #ctxHasNotebookProvider: IContextKey; + readonly #ctxPossible: IContextKey; - private readonly _store = new DisposableStore(); + readonly #store = new DisposableStore(); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -184,41 +187,41 @@ export class InlineChatEnabler { @IEditorService editorService: IEditorService, @IConfigurationService configService: IConfigurationService, ) { - this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); - this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); - this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); + this.#ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); + this.#ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); + this.#ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService); - this._store.add(autorun(r => { + this.#store.add(autorun(r => { const agent = agentObs.read(r); if (!agent) { - this._ctxHasProvider2.reset(); + this.#ctxHasProvider2.reset(); } else { - this._ctxHasProvider2.set(true); + this.#ctxHasProvider2.set(true); } })); - this._store.add(autorun(r => { - this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); + this.#store.add(autorun(r => { + this.#ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); })); const updateEditor = () => { const ctrl = editorService.activeEditorPane?.getControl(); const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl); - this._ctxPossible.set(isCodeEditorLike); + this.#ctxPossible.set(isCodeEditorLike); }; - this._store.add(editorService.onDidActiveEditorChange(updateEditor)); + this.#store.add(editorService.onDidActiveEditorChange(updateEditor)); updateEditor(); } dispose() { - this._ctxPossible.reset(); - this._ctxHasProvider2.reset(); - this._store.dispose(); + this.#ctxPossible.reset(); + this.#ctxHasProvider2.reset(); + this.#store.dispose(); } } @@ -229,7 +232,7 @@ export class InlineChatEscapeToolContribution extends Disposable { static readonly DONT_ASK_AGAIN_KEY = 'inlineChat.dontAskMoveToPanelChat'; - private static readonly _data: IToolData = { + static readonly #data: IToolData = { id: 'inline_chat_exit', source: ToolDataSource.Internal, canBeReferencedInPrompt: false, @@ -251,7 +254,7 @@ export class InlineChatEscapeToolContribution extends Disposable { super(); - this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, { + this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution.#data, { invoke: async (invocation, _tokenCountFn, _progress, _token) => { const sessionResource = invocation.context?.sessionResource; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index a449ed2a512..9d3dc3edbcb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -95,37 +95,59 @@ export class InlineChatWidget { protected readonly _store = new DisposableStore(); - private readonly _ctxInputEditorFocused: IContextKey; - private readonly _ctxResponseFocused: IContextKey; + readonly #ctxInputEditorFocused: IContextKey; + readonly #ctxResponseFocused: IContextKey; - private readonly _chatWidget: ChatWidget; + readonly #chatWidget: ChatWidget; protected readonly _onDidChangeHeight = this._store.add(new Emitter()); - readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); + readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting); - private readonly _requestInProgress = observableValue(this, false); - readonly requestInProgress: IObservable = this._requestInProgress; + readonly #requestInProgress = observableValue(this, false); + readonly requestInProgress: IObservable = this.#requestInProgress; - private _isLayouting: boolean = false; + #isLayouting: boolean = false; readonly scopedContextKeyService: IContextKeyService; + readonly #options: IInlineChatWidgetConstructionOptions; + readonly #contextKeyService: IContextKeyService; + readonly #keybindingService: IKeybindingService; + readonly #accessibilityService: IAccessibilityService; + readonly #configurationService: IConfigurationService; + readonly #accessibleViewService: IAccessibleViewService; + readonly #chatService: IChatService; + readonly #hoverService: IHoverService; + readonly #chatEntitlementService: IChatEntitlementService; + readonly #markdownRendererService: IMarkdownRendererService; + constructor( location: IChatWidgetLocationOptions, - private readonly _options: IInlineChatWidgetConstructionOptions, + options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibleViewService accessibleViewService: IAccessibleViewService, @ITextModelService protected readonly _textModelResolverService: ITextModelService, - @IChatService private readonly _chatService: IChatService, - @IHoverService private readonly _hoverService: IHoverService, - @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, - @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, + @IChatService chatService: IChatService, + @IHoverService hoverService: IHoverService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, + @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, ) { - this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); + this.#options = options; + this.#contextKeyService = contextKeyService; + this.#keybindingService = keybindingService; + this.#accessibilityService = accessibilityService; + this.#configurationService = configurationService; + this.#accessibleViewService = accessibleViewService; + this.#chatService = chatService; + this.#hoverService = hoverService; + this.#chatEntitlementService = chatEntitlementService; + this.#markdownRendererService = markdownRendererService; + + this.scopedContextKeyService = this._store.add(contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ IContextKeyService, @@ -134,7 +156,7 @@ export class InlineChatWidget { this._store ); - this._chatWidget = scopedInstaService.createInstance( + this.#chatWidget = scopedInstaService.createInstance( ChatWidget, location, { isInlineChat: true }, @@ -154,14 +176,14 @@ export class InlineChatWidget { if (emptyResponse) { return false; } - if (item.response.value.every(item => item.kind === 'textEditGroup' && _options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { + if (item.response.value.every(item => item.kind === 'textEditGroup' && this.#options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { return false; } return true; }, dndContainer: this._elements.root, defaultMode: ChatMode.Ask, - ..._options.chatWidgetViewOptions + ...this.#options.chatWidgetViewOptions }, { listForeground: inlineChatForeground, @@ -171,11 +193,11 @@ export class InlineChatWidget { resultEditorBackground: editorBackground } ); - this._elements.root.classList.toggle('in-zone-widget', !!_options.inZoneWidget); - this._chatWidget.render(this._elements.chatWidget); + this._elements.root.classList.toggle('in-zone-widget', !!this.#options.inZoneWidget); + this.#chatWidget.render(this._elements.chatWidget); this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); - this._chatWidget.setVisible(true); - this._store.add(this._chatWidget); + this.#chatWidget.setVisible(true); + this._store.add(this.#chatWidget); const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService); const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService); @@ -184,10 +206,10 @@ export class InlineChatWidget { const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService); const viewModelStore = this._store.add(new DisposableStore()); - this._store.add(this._chatWidget.onDidChangeViewModel(() => { + this._store.add(this.#chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); - const viewModel = this._chatWidget.viewModel; + const viewModel = this.#chatWidget.viewModel; if (!viewModel) { return; } @@ -203,7 +225,7 @@ export class InlineChatWidget { viewModelStore.add(viewModel.onDidChange(() => { - this._requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); + this.#requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); const last = viewModel.getItems().at(-1); toolbar2.context = last; @@ -224,22 +246,22 @@ export class InlineChatWidget { })); // context keys - this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); + this.#ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this.#contextKeyService); const tracker = this._store.add(trackFocus(this.domNode)); - this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false))); - this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true))); + this._store.add(tracker.onDidBlur(() => this.#ctxResponseFocused.set(false))); + this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true))); - this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService); - this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true))); - this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); + this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this.#contextKeyService); + this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); + this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); - const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu; + const statusMenuId = this.#options.statusMenuId instanceof MenuId ? this.#options.statusMenuId : this.#options.statusMenuId.menu; // BUTTON bar - const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options; + const statusMenuOptions = this.#options.statusMenuId instanceof MenuId ? undefined : this.#options.statusMenuId.options; const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar1, statusMenuId, { toolbarOptions: { primaryGroup: '0_main' }, - telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource, + telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource, menuOptions: { renderShortTitle: true }, ...statusMenuOptions, }); @@ -247,8 +269,8 @@ export class InlineChatWidget { this._store.add(statusButtonBar); // secondary toolbar - const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, _options.secondaryMenuId ?? MenuId.for(''), { - telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource, + const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, this.#options.secondaryMenuId ?? MenuId.for(''), { + telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource, menuOptions: { renderShortTitle: true, shouldForwardArgs: true }, actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId) { @@ -261,60 +283,60 @@ export class InlineChatWidget { this._store.add(toolbar2); - this._store.add(this._configurationService.onDidChangeConfiguration(e => { + this._store.add(this.#configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); + this.#updateAriaLabel(); } })); this._elements.root.tabIndex = 0; this._elements.statusLabel.tabIndex = 0; - this._updateAriaLabel(); - this._setupDisclaimer(); + this.#updateAriaLabel(); + this.#setupDisclaimer(); - this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { + this._store.add(this.#hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { return this._elements.statusLabel.dataset['title']; })); - this._store.add(this._chatService.onDidPerformUserAction(e => { - if (isEqual(e.sessionResource, this._chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { + this._store.add(this.#chatService.onDidPerformUserAction(e => { + if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 }); } })); } - private _updateAriaLabel(): void { + #updateAriaLabel(): void { - this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._elements.root.ariaLabel = this.#accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); - if (this._accessibilityService.isScreenReaderOptimized()) { + if (this.#accessibilityService.isScreenReaderOptimized()) { let label = defaultAriaLabel; - if (this._configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { - const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + if (this.#configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { + const kbLabel = this.#keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); label = kbLabel ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - this._chatWidget.inputEditor.updateOptions({ ariaLabel: label }); + this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label }); } } - private _setupDisclaimer(): void { + #setupDisclaimer(): void { const disposables = this._store.add(new DisposableStore()); this._store.add(autorun(reader => { disposables.clear(); reset(this._elements.disclaimerLabel); - const sentiment = this._chatEntitlementService.sentimentObs.read(reader); - const anonymous = this._chatEntitlementService.anonymousObs.read(reader); - const requestInProgress = this._chatService.requestInProgressObs.read(reader); + const sentiment = this.#chatEntitlementService.sentimentObs.read(reader); + const anonymous = this.#chatEntitlementService.anonymousObs.read(reader); + const requestInProgress = this.#chatService.requestInProgressObs.read(reader); const showDisclaimer = !sentiment.installed && anonymous && !requestInProgress; this._elements.disclaimerLabel.classList.toggle('hidden', !showDisclaimer); if (showDisclaimer) { - const renderedMarkdown = disposables.add(this._markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); + const renderedMarkdown = disposables.add(this.#markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); this._elements.disclaimerLabel.appendChild(renderedMarkdown.element); } @@ -331,20 +353,20 @@ export class InlineChatWidget { } get chatWidget(): ChatWidget { - return this._chatWidget; + return this.#chatWidget; } saveState() { - this._chatWidget.saveState(); + this.#chatWidget.saveState(); } layout(widgetDim: Dimension) { const contentHeight = this.contentHeight; - this._isLayouting = true; + this.#isLayouting = true; try { this._doLayout(widgetDim); } finally { - this._isLayouting = false; + this.#isLayouting = false; if (this.contentHeight !== contentHeight) { this._onDidChangeHeight.fire(); @@ -361,7 +383,7 @@ export class InlineChatWidget { this._elements.root.style.height = `${dimension.height - extraHeight}px`; this._elements.root.style.width = `${dimension.width}px`; - this._chatWidget.layout( + this.#chatWidget.layout( dimension.height - statusHeight - extraHeight, dimension.width ); @@ -372,7 +394,7 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - chatWidgetContentHeight: this._chatWidget.contentHeight, + chatWidgetContentHeight: this.#chatWidget.contentHeight, statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; @@ -385,7 +407,7 @@ export class InlineChatWidget { // at least "maxWidgetHeight" high and at most the content height. let maxWidgetOutputHeight = 100; - for (const item of this._chatWidget.viewModel?.getItems() ?? []) { + for (const item of this.#chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { maxWidgetOutputHeight = 270; break; @@ -393,29 +415,29 @@ export class InlineChatWidget { } let value = this.contentHeight; - value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); + value -= this.#chatWidget.contentHeight; + value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight); return value; } protected _getExtraHeight(): number { - return this._options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); + return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); } get value(): string { - return this._chatWidget.getInput(); + return this.#chatWidget.getInput(); } set value(value: string) { - this._chatWidget.setInput(value); + this.#chatWidget.setInput(value); } selectAll() { - this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } set placeholder(value: string) { - this._chatWidget.setInputPlaceholder(value); + this.#chatWidget.setInputPlaceholder(value); } toggleStatus(show: boolean) { @@ -436,7 +458,7 @@ export class InlineChatWidget { } async getCodeBlockInfo(codeBlockIndex: number): Promise { - const { viewModel } = this._chatWidget; + const { viewModel } = this.#chatWidget; if (!viewModel) { return undefined; } @@ -483,18 +505,18 @@ export class InlineChatWidget { } get responseContent(): string | undefined { - const requests = this._chatWidget.viewModel?.model.getRequests(); + const requests = this.#chatWidget.viewModel?.model.getRequests(); return requests?.at(-1)?.response?.response.toString(); } getChatModel(): IChatModel | undefined { - return this._chatWidget.viewModel?.model; + return this.#chatWidget.viewModel?.model; } setChatModel(chatModel: IChatModel) { chatModel.inputModel.setState({ inputText: '', selections: [] }); - this._chatWidget.setModel(chatModel); + this.#chatWidget.setModel(chatModel); } updateInfo(message: string): void { @@ -533,8 +555,8 @@ export class InlineChatWidget { } reset() { - this._chatWidget.attachmentModel.clear(true); - this._chatWidget.saveState(); + this.#chatWidget.attachmentModel.clear(true); + this.#chatWidget.saveState(); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); @@ -547,7 +569,7 @@ export class InlineChatWidget { } focus() { - this._chatWidget.focusInput(); + this.#chatWidget.focusInput(); } hasFocus() { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 21113b9d0de..5f172c19672 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -28,7 +28,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; export class InlineChatZoneWidget extends ZoneWidget { - private static readonly _options: IOptions = { + static readonly #options: IOptions = { showFrame: true, frameWidth: 1, // frameColor: 'var(--vscode-inlineChat-border)', @@ -43,9 +43,12 @@ export class InlineChatZoneWidget extends ZoneWidget { readonly widget: EditorBasedInlineChatWidget; - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - private notebookEditor?: INotebookEditor; + readonly #ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + #dimension?: Dimension; + #notebookEditor?: INotebookEditor; + + readonly #instaService: IInstantiationService; + #logService: ILogService; constructor( location: IChatWidgetLocationOptions, @@ -53,20 +56,22 @@ export class InlineChatZoneWidget extends ZoneWidget { editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor }, /** @deprecated should go away with inline2 */ clearDelegate: () => Promise, - @IInstantiationService private readonly _instaService: IInstantiationService, - @ILogService private _logService: ILogService, + @IInstantiationService instaService: IInstantiationService, + @ILogService logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super(editors.editor, InlineChatZoneWidget._options); - this.notebookEditor = editors.notebookEditor; + super(editors.editor, InlineChatZoneWidget.#options); + this.#instaService = instaService; + this.#logService = logService; + this.#notebookEditor = editors.notebookEditor; - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + this.#ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); this._disposables.add(toDisposable(() => { - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); })); - this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { + this.widget = this.#instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { statusMenuId: { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { @@ -105,14 +110,14 @@ export class InlineChatZoneWidget extends ZoneWidget { let revealFn: (() => void) | undefined; this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => { if (this.position) { - revealFn = this._createZoneAndScrollRestoreFn(this.position); + revealFn = this.#createZoneAndScrollRestoreFn(this.position); } })); this._disposables.add(this.widget.onDidChangeHeight(() => { if (this.position && !this._usesResizeHeight) { // only relayout when visible - revealFn ??= this._createZoneAndScrollRestoreFn(this.position); - const height = this._computeHeight(); + revealFn ??= this.#createZoneAndScrollRestoreFn(this.position); + const height = this.#computeHeight(); this._relayout(height.linesValue); revealFn?.(); revealFn = undefined; @@ -136,13 +141,13 @@ export class InlineChatZoneWidget extends ZoneWidget { // todo@jrieken listen ONLY when showing const updateCursorIsAboveContextKey = () => { if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); + this.#ctxCursorPosition.set('above'); } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); + this.#ctxCursorPosition.set('below'); } else { - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); } }; this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); @@ -159,19 +164,19 @@ export class InlineChatZoneWidget extends ZoneWidget { protected override _doLayout(heightInPixel: number): void { - this._updatePadding(); + this.#updatePadding(); const info = this.editor.getLayoutInfo(); const width = info.contentWidth - info.verticalScrollbarWidth; // width = Math.min(850, width); - this._dimension = new Dimension(width, heightInPixel); - this.widget.layout(this._dimension); + this.#dimension = new Dimension(width, heightInPixel); + this.widget.layout(this.#dimension); } - private _computeHeight(): { linesValue: number; pixelsValue: number } { + #computeHeight(): { linesValue: number; pixelsValue: number } { const chatContentHeight = this.widget.contentHeight; - const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height; + const editorHeight = this.#notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height; const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42)); const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight); @@ -192,25 +197,25 @@ export class InlineChatZoneWidget extends ZoneWidget { } protected override _onWidth(_widthInPixel: number): void { - if (this._dimension) { - this._doLayout(this._dimension.height); + if (this.#dimension) { + this._doLayout(this.#dimension.height); } } override show(position: Position): void { assertType(this.container); - this._updatePadding(); + this.#updatePadding(); - const revealZone = this._createZoneAndScrollRestoreFn(position); - super.show(position, this._computeHeight().linesValue); + const revealZone = this.#createZoneAndScrollRestoreFn(position); + super.show(position, this.#computeHeight().linesValue); this.widget.chatWidget.setVisible(true); this.widget.focus(); revealZone(); } - private _updatePadding() { + #updatePadding() { assertType(this.container); const info = this.editor.getLayoutInfo(); @@ -226,12 +231,12 @@ export class InlineChatZoneWidget extends ZoneWidget { } override updatePositionAndHeight(position: Position): void { - const revealZone = this._createZoneAndScrollRestoreFn(position); - super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined); + const revealZone = this.#createZoneAndScrollRestoreFn(position); + super.updatePositionAndHeight(position, !this._usesResizeHeight ? this.#computeHeight().linesValue : undefined); revealZone(); } - private _createZoneAndScrollRestoreFn(position: Position): () => void { + #createZoneAndScrollRestoreFn(position: Position): () => void { const scrollState = StableEditorBottomScrollState.capture(this.editor); @@ -242,7 +247,7 @@ export class InlineChatZoneWidget extends ZoneWidget { const scrollTop = this.editor.getScrollTop(); const lineTop = this.editor.getTopForLineNumber(lineNumber); - const zoneTop = lineTop - this._computeHeight().pixelsValue; + const zoneTop = lineTop - this.#computeHeight().pixelsValue; const editorHeight = this.editor.getLayoutInfo().height; const lineBottom = this.editor.getBottomForLineNumber(lineNumber); @@ -257,7 +262,7 @@ export class InlineChatZoneWidget extends ZoneWidget { } if (newScrollTop < scrollTop || forceScrollTop) { - this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); + this.#logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } }; @@ -269,7 +274,7 @@ export class InlineChatZoneWidget extends ZoneWidget { override hide(): void { const scrollState = StableEditorBottomScrollState.capture(this.editor); - this._ctxCursorPosition.reset(); + this.#ctxCursorPosition.reset(); this.widget.chatWidget.setVisible(false); super.hide(); aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts b/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts index 2a6ea9759da..8e1573fdaed 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts @@ -20,47 +20,49 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc export class TestWorkerService extends mock() implements IDisposable { - private readonly _store = new DisposableStore(); - private readonly _worker = this._store.add(new EditorWorker()); + readonly #store = new DisposableStore(); + readonly #worker = this.#store.add(new EditorWorker()); + readonly #modelService: IModelService; - constructor(@IModelService private readonly _modelService: IModelService) { + constructor(@IModelService modelService: IModelService) { super(); + this.#modelService = modelService; } dispose(): void { - this._store.dispose(); + this.#store.dispose(); } override async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean | undefined): Promise { return undefined; } override async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise { - await new Promise(resolve => disposableTimeout(() => resolve(), 0, this._store)); - if (this._store.isDisposed) { + await new Promise(resolve => disposableTimeout(() => resolve(), 0, this.#store)); + if (this.#store.isDisposed) { return null; } - const originalModel = this._modelService.getModel(original); - const modifiedModel = this._modelService.getModel(modified); + const originalModel = this.#modelService.getModel(original); + const modifiedModel = this.#modelService.getModel(modified); assertType(originalModel); assertType(modifiedModel); - this._worker.$acceptNewModel({ + this.#worker.$acceptNewModel({ url: originalModel.uri.toString(), versionId: originalModel.getVersionId(), lines: originalModel.getLinesContent(), EOL: originalModel.getEOL(), }); - this._worker.$acceptNewModel({ + this.#worker.$acceptNewModel({ url: modifiedModel.uri.toString(), versionId: modifiedModel.getVersionId(), lines: modifiedModel.getLinesContent(), EOL: modifiedModel.getEOL(), }); - const result = await this._worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm); + const result = await this.#worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm); if (!result) { return result; } From dc94486ab96338ab4506bb0a413889e9342a605f Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:29:58 -0800 Subject: [PATCH 292/448] Browser: context menus (#299013) * Browser: context menus * feedback * feedback * auxiliary fix --- .../browserView/common/browserView.ts | 29 +++ .../browserView/electron-main/browserView.ts | 38 ++- .../electron-main/browserViewMainService.ts | 242 ++++++++++++++---- .../electron-browser/browserFindWidget.ts | 7 +- .../electron-browser/browserViewActions.ts | 40 +-- .../browserViewWorkbenchService.ts | 32 ++- 6 files changed, 306 insertions(+), 82 deletions(-) diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index ec51dec6b8f..fe4f22d727d 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -7,6 +7,29 @@ import { Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { URI } from '../../../base/common/uri.js'; +const commandPrefix = 'workbench.action.browser'; +export enum BrowserViewCommandId { + Open = `${commandPrefix}.open`, + NewTab = `${commandPrefix}.newTab`, + GoBack = `${commandPrefix}.goBack`, + GoForward = `${commandPrefix}.goForward`, + Reload = `${commandPrefix}.reload`, + HardReload = `${commandPrefix}.hardReload`, + FocusUrlInput = `${commandPrefix}.focusUrlInput`, + AddElementToChat = `${commandPrefix}.addElementToChat`, + AddConsoleLogsToChat = `${commandPrefix}.addConsoleLogsToChat`, + ToggleDevTools = `${commandPrefix}.toggleDevTools`, + OpenExternal = `${commandPrefix}.openExternal`, + ClearGlobalStorage = `${commandPrefix}.clearGlobalStorage`, + ClearWorkspaceStorage = `${commandPrefix}.clearWorkspaceStorage`, + ClearEphemeralStorage = `${commandPrefix}.clearEphemeralStorage`, + OpenSettings = `${commandPrefix}.openSettings`, + ShowFind = `${commandPrefix}.showFind`, + HideFind = `${commandPrefix}.hideFind`, + FindNext = `${commandPrefix}.findNext`, + FindPrevious = `${commandPrefix}.findPrevious`, +} + export interface IBrowserViewBounds { windowId: number; x: number; @@ -287,4 +310,10 @@ export interface IBrowserViewService { * @param id The browser view identifier */ clearStorage(id: string): Promise; + + /** + * Update the keybinding accelerators used in browser view context menus. + * @param keybindings A map of command ID to accelerator label + */ + updateKeybindings(keybindings: { [commandId: string]: string }): Promise; } diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 10fb6b62afa..bcac609f8e6 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -11,15 +11,16 @@ import { VSBuffer } from '../../../base/common/buffer.js'; import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; -import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; +import { ICodeWindow } from '../../window/electron-main/window.js'; import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js'; -import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { isMacintosh } from '../../../base/common/platform.js'; import { BrowserViewUri } from '../common/browserViewUri.js'; import { BrowserViewDebugger } from './browserViewDebugger.js'; import { ILogService } from '../../log/common/log.js'; import { ICDPTarget, ICDPConnection, CDPTargetInfo } from '../common/cdp/types.js'; import { BrowserSession } from './browserSession.js'; +import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; +import { hasKey } from '../../../base/common/types.js'; /** Key combinations that are used in system-level shortcuts. */ const nativeShortcuts = new Set([ @@ -47,7 +48,7 @@ export class BrowserView extends Disposable implements ICDPTarget { private _lastUserGestureTimestamp: number = -Infinity; private _debugger: BrowserViewDebugger; - private _window: IBaseWindow | undefined; + private _window: ICodeWindow | IAuxiliaryWindow | undefined; private _isSendingKeyEvent = false; private _isDisposed = false; @@ -88,6 +89,7 @@ export class BrowserView extends Disposable implements ICDPTarget { public readonly id: string, public readonly session: BrowserSession, createChildView: (options?: Electron.WebContentsViewConstructorOptions) => BrowserView, + openContextMenu: (view: BrowserView, params: Electron.ContextMenuParams) => void, options: Electron.WebContentsViewConstructorOptions | undefined, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService, @@ -150,6 +152,10 @@ export class BrowserView extends Disposable implements ICDPTarget { }; }); + this._view.webContents.on('context-menu', (_event, params) => { + openContextMenu(this, params); + }); + this._view.webContents.on('destroyed', () => { this.dispose(); }); @@ -376,7 +382,7 @@ export class BrowserView extends Disposable implements ICDPTarget { */ layout(bounds: IBrowserViewBounds): void { if (this._window?.win?.id !== bounds.windowId) { - const newWindow = this.windowById(bounds.windowId); + const newWindow = this._windowById(bounds.windowId); if (newWindow) { this._window?.win?.contentView.removeChildView(this._view); this._window = newWindow; @@ -575,6 +581,22 @@ export class BrowserView extends Disposable implements ICDPTarget { return this._view; } + /** + * Get the hosting Electron window for this view, if any. + * This can be an auxiliary window, depending on where the view is currently hosted. + */ + getElectronWindow(): Electron.BrowserWindow | undefined { + return this._window?.win ?? undefined; + } + + /** + * Get the main code window hosting this browser view, if any. This is used for routing commands from the browser view to the correct window. + * If the browser view is hosted in an auxiliary window, this will return the parent code window of that auxiliary window. + */ + getTopCodeWindow(): ICodeWindow | undefined { + return this._window && hasKey(this._window, { parentId: true }) ? this._codeWindowById(this._window.parentId) : undefined; + } + // ============ ICDPTarget implementation ============ /** @@ -662,11 +684,11 @@ export class BrowserView extends Disposable implements ICDPTarget { return true; } - private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { - return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId); + private _windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { + return this._codeWindowById(windowId) ?? this._auxiliaryWindowById(windowId); } - private codeWindowById(windowId: number | undefined): ICodeWindow | undefined { + private _codeWindowById(windowId: number | undefined): ICodeWindow | undefined { if (typeof windowId !== 'number') { return undefined; } @@ -674,7 +696,7 @@ export class BrowserView extends Disposable implements ICDPTarget { return this.windowsMainService.getWindowById(windowId); } - private auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined { + private _auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined { if (typeof windowId !== 'number') { return undefined; } diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index c2a4e3aeefe..313b3f416de 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -6,7 +6,8 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IBrowserViewBounds, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewService, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions } from '../common/browserView.js'; +import { IBrowserViewBounds, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewService, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, BrowserViewCommandId } from '../common/browserView.js'; +import { clipboard, Menu, MenuItem } from 'electron'; import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget } from '../common/cdp/types.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; @@ -17,9 +18,13 @@ import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { BrowserSession } from './browserSession.js'; import { IProductService } from '../../product/common/productService.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; -import { logBrowserOpen } from '../common/browserViewTelemetry.js'; +import { IntegratedBrowserOpenSource, logBrowserOpen } from '../common/browserViewTelemetry.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import { localize } from '../../../nls.js'; +import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; +import { ITextEditorOptions } from '../../editor/common/editor.js'; +import { htmlAttributeEncodeValue } from '../../../base/common/strings.js'; export const IBrowserViewMainService = createDecorator('browserViewMainService'); @@ -41,6 +46,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa } private readonly browserViews = this._register(new DisposableMap()); + private _keybindings: { [commandId: string]: string } = Object.create(null); // ICDPBrowserTarget events private readonly _onTargetCreated = this._register(new Emitter()); @@ -54,38 +60,12 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa @IInstantiationService private readonly instantiationService: IInstantiationService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IProductService private readonly productService: IProductService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService ) { super(); } - /** - * Create a browser view backed by the given {@link BrowserSession}. - */ - private createBrowserView(id: string, browserSession: BrowserSession, options?: Electron.WebContentsViewConstructorOptions): BrowserView { - if (this.browserViews.has(id)) { - throw new Error(`Browser view with id ${id} already exists`); - } - - const view = this.instantiationService.createInstance( - BrowserView, - id, - browserSession, - // Recursive factory for nested windows (child views share the same session) - (childOptions) => this.createBrowserView(generateUuid(), browserSession, childOptions), - options - ); - this.browserViews.set(id, view); - - this._onTargetCreated.fire(view); - Event.once(view.onDidClose)(() => { - this._onTargetDestroyed.fire(view); - this.browserViews.deleteAndDispose(id); - }); - - return view; - } - async getOrCreateBrowserView(id: string, scope: BrowserViewStorageScope, workspaceId?: string): Promise { if (this.browserViews.has(id)) { // Note: scope will be ignored if the view already exists. @@ -160,26 +140,14 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa } async createTarget(url: string, browserContextId?: string, windowId?: number): Promise { - const targetId = generateUuid(); - const browserSession = browserContextId && BrowserSession.get(browserContextId) || BrowserSession.getOrCreateEphemeral(targetId); + const browserSession = browserContextId ? BrowserSession.get(browserContextId) : undefined; - // Create the browser view (fires onTargetCreated) - const view = this.createBrowserView(targetId, browserSession); - - logBrowserOpen(this.telemetryService, 'cdpCreated'); - - const window = windowId !== undefined ? this.windowsMainService.getWindowById(windowId) : this.windowsMainService.getFocusedWindow(); - if (!window) { - throw new Error(`Window ${windowId} not found`); - } - - // Request the workbench to open the editor - window.sendWhenReady('vscode:runAction', CancellationToken.None, { - id: '_workbench.open', - args: [BrowserViewUri.forUrl(url, targetId), [undefined, { preserveFocus: true }], undefined] + return this.openNew(url, { + session: browserSession, + windowId, + editorOptions: { preserveFocus: true }, + source: 'cdpCreated' }); - - return view; } async activateTarget(target: ICDPTarget): Promise { @@ -372,4 +340,182 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa ); await browserSession.electronSession.clearData(); } + + async updateKeybindings(keybindings: { [commandId: string]: string }): Promise { + this._keybindings = keybindings; + } + + /** + * Create a browser view backed by the given {@link BrowserSession}. + */ + private createBrowserView(id: string, browserSession: BrowserSession, options?: Electron.WebContentsViewConstructorOptions): BrowserView { + if (this.browserViews.has(id)) { + throw new Error(`Browser view with id ${id} already exists`); + } + + const view = this.instantiationService.createInstance( + BrowserView, + id, + browserSession, + // Recursive factory for nested windows (child views share the same session) + (childOptions) => this.createBrowserView(generateUuid(), browserSession, childOptions), + (v, params) => this.showContextMenu(v, params), + options + ); + this.browserViews.set(id, view); + + this._onTargetCreated.fire(view); + Event.once(view.onDidClose)(() => { + this._onTargetDestroyed.fire(view); + this.browserViews.deleteAndDispose(id); + }); + + return view; + } + + private async openNew( + url: string, + { + session, + windowId, + editorOptions, + source + }: { + session: BrowserSession | undefined; + windowId: number | undefined; + editorOptions: ITextEditorOptions; + source: IntegratedBrowserOpenSource; + } + ): Promise { + const targetId = generateUuid(); + const view = this.createBrowserView(targetId, session || BrowserSession.getOrCreateEphemeral(targetId)); + + const window = windowId !== undefined ? this.windowsMainService.getWindowById(windowId) : this.windowsMainService.getFocusedWindow(); + if (!window) { + throw new Error(`Window ${windowId} not found`); + } + + + logBrowserOpen(this.telemetryService, source); + + // Request the workbench to open the editor + window.sendWhenReady('vscode:runAction', CancellationToken.None, { + id: '_workbench.open', + args: [BrowserViewUri.forUrl(url, targetId), [undefined, editorOptions], undefined] + }); + + return view; + } + + private showContextMenu(view: BrowserView, params: Electron.ContextMenuParams): void { + const win = view.getElectronWindow(); + if (!win) { + return; + } + const webContents = view.webContents; + if (webContents.isDestroyed()) { + return; + } + const menu = new Menu(); + + if (params.linkURL) { + menu.append(new MenuItem({ + label: localize('browser.contextMenu.openLinkInNewTab', 'Open Link in New Tab'), + click: () => { + void this.openNew(params.linkURL, { + session: view.session, + windowId: view.getTopCodeWindow()?.id, + editorOptions: { preserveFocus: true, inactive: true }, + source: 'browserLinkBackground' + }); + } + })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.openLinkInExternalBrowser', 'Open Link in External Browser'), + click: () => { void this.nativeHostMainService.openExternal(undefined, params.linkURL); } + })); + menu.append(new MenuItem({ type: 'separator' })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.copyLink', 'Copy Link'), + click: () => { + clipboard.write({ + text: params.linkURL, + html: `${htmlAttributeEncodeValue(params.linkText || params.linkURL)}` + }); + } + })); + } + + if (params.hasImageContents && params.srcURL) { + if (menu.items.length > 0) { + menu.append(new MenuItem({ type: 'separator' })); + } + menu.append(new MenuItem({ + label: localize('browser.contextMenu.openImageInNewTab', 'Open Image in New Tab'), + click: () => { + void this.openNew(params.srcURL!, { + session: view.session, + windowId: view.getTopCodeWindow()?.id, + editorOptions: { preserveFocus: true, inactive: true }, + source: 'browserLinkBackground' + }); + } + })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.copyImage', 'Copy Image'), + click: () => { view.webContents.copyImageAt(params.x, params.y); } + })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.copyImageUrl', 'Copy Image URL'), + click: () => { clipboard.writeText(params.srcURL!); } + })); + } + + if (params.isEditable) { + menu.append(new MenuItem({ role: 'cut', enabled: params.editFlags.canCut })); + menu.append(new MenuItem({ role: 'copy', enabled: params.editFlags.canCopy })); + menu.append(new MenuItem({ role: 'paste', enabled: params.editFlags.canPaste })); + menu.append(new MenuItem({ role: 'pasteAndMatchStyle', enabled: params.editFlags.canPaste })); + menu.append(new MenuItem({ role: 'selectAll', enabled: params.editFlags.canSelectAll })); + } else if (params.selectionText) { + menu.append(new MenuItem({ role: 'copy' })); + } + + // Add navigation items as defaults + if (menu.items.length === 0) { + if (webContents.navigationHistory.canGoBack()) { + menu.append(new MenuItem({ + label: localize('browser.contextMenu.back', 'Back'), + accelerator: this._keybindings[BrowserViewCommandId.GoBack], + click: () => webContents.navigationHistory.goBack() + })); + } + if (webContents.navigationHistory.canGoForward()) { + menu.append(new MenuItem({ + label: localize('browser.contextMenu.forward', 'Forward'), + accelerator: this._keybindings[BrowserViewCommandId.GoForward], + click: () => webContents.navigationHistory.goForward() + })); + } + menu.append(new MenuItem({ + label: localize('browser.contextMenu.reload', 'Reload'), + accelerator: this._keybindings[BrowserViewCommandId.Reload], + click: () => webContents.reload() + })); + } + + menu.append(new MenuItem({ type: 'separator' })); + menu.append(new MenuItem({ + label: localize('browser.contextMenu.inspect', 'Inspect'), + click: () => webContents.inspectElement(params.x, params.y) + })); + + const viewBounds = view.getWebContentsView().getBounds(); + menu.popup({ + window: win, + x: viewBounds.x + params.x, + y: viewBounds.y + params.y, + sourceType: params.menuSourceType + }); + } } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts index aec2add429b..a1ad809a6f3 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserFindWidget.ts @@ -11,6 +11,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IBrowserViewModel } from '../common/browserView.js'; +import { BrowserViewCommandId } from '../../../../platform/browserView/common/browserView.js'; import { localize } from '../../../../nls.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -45,9 +46,9 @@ export class BrowserFindWidget extends SimpleFindWidget { showResultCount: true, enableSash: true, initialWidth: 350, - previousMatchActionId: 'workbench.action.browser.findPrevious', - nextMatchActionId: 'workbench.action.browser.findNext', - closeWidgetActionId: 'workbench.action.browser.hideFind' + previousMatchActionId: BrowserViewCommandId.FindPrevious, + nextMatchActionId: BrowserViewCommandId.FindNext, + closeWidgetActionId: BrowserViewCommandId.HideFind }, contextViewService, contextKeyService, hoverService, keybindingService, configurationService, accessibilityService); this._findWidgetVisible = CONTEXT_BROWSER_FIND_WIDGET_VISIBLE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index b32e1c70ea2..5fd326273c4 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -14,7 +14,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_ERROR, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js'; import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js'; import { IBrowserViewWorkbenchService } from '../common/browserView.js'; -import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; +import { BrowserViewCommandId, BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; @@ -37,7 +37,7 @@ interface IOpenBrowserOptions { class OpenIntegratedBrowserAction extends Action2 { constructor() { super({ - id: 'workbench.action.browser.open', + id: BrowserViewCommandId.Open, title: localize2('browser.openAction', "Open Integrated Browser"), category: BrowserCategory, f1: true @@ -67,7 +67,7 @@ class OpenIntegratedBrowserAction extends Action2 { class NewTabAction extends Action2 { constructor() { super({ - id: 'workbench.action.browser.newTab', + id: BrowserViewCommandId.NewTab, title: localize2('browser.newTabAction', "New Tab"), category: BrowserCategory, f1: true, @@ -97,7 +97,7 @@ class NewTabAction extends Action2 { } class GoBackAction extends Action2 { - static readonly ID = 'workbench.action.browser.goBack'; + static readonly ID = BrowserViewCommandId.GoBack; constructor() { super({ @@ -129,7 +129,7 @@ class GoBackAction extends Action2 { } class GoForwardAction extends Action2 { - static readonly ID = 'workbench.action.browser.goForward'; + static readonly ID = BrowserViewCommandId.GoForward; constructor() { super({ @@ -161,7 +161,7 @@ class GoForwardAction extends Action2 { } class ReloadAction extends Action2 { - static readonly ID = 'workbench.action.browser.reload'; + static readonly ID = BrowserViewCommandId.Reload; constructor() { super({ @@ -199,7 +199,7 @@ class ReloadAction extends Action2 { } class HardReloadAction extends Action2 { - static readonly ID = 'workbench.action.browser.hardReload'; + static readonly ID = BrowserViewCommandId.HardReload; constructor() { super({ @@ -227,7 +227,7 @@ class HardReloadAction extends Action2 { } class FocusUrlInputAction extends Action2 { - static readonly ID = 'workbench.action.browser.focusUrlInput'; + static readonly ID = BrowserViewCommandId.FocusUrlInput; constructor() { super({ @@ -251,7 +251,7 @@ class FocusUrlInputAction extends Action2 { } class AddElementToChatAction extends Action2 { - static readonly ID = 'workbench.action.browser.addElementToChat'; + static readonly ID = BrowserViewCommandId.AddElementToChat; constructor() { const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true)); @@ -288,7 +288,7 @@ class AddElementToChatAction extends Action2 { } class AddConsoleLogsToChatAction extends Action2 { - static readonly ID = 'workbench.action.browser.addConsoleLogsToChat'; + static readonly ID = BrowserViewCommandId.AddConsoleLogsToChat; constructor() { const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true)); @@ -316,7 +316,7 @@ class AddConsoleLogsToChatAction extends Action2 { } class ToggleDevToolsAction extends Action2 { - static readonly ID = 'workbench.action.browser.toggleDevTools'; + static readonly ID = BrowserViewCommandId.ToggleDevTools; constructor() { super({ @@ -347,7 +347,7 @@ class ToggleDevToolsAction extends Action2 { } class OpenInExternalBrowserAction extends Action2 { - static readonly ID = 'workbench.action.browser.openExternal'; + static readonly ID = BrowserViewCommandId.OpenExternal; constructor() { super({ @@ -383,7 +383,7 @@ class OpenInExternalBrowserAction extends Action2 { } class ClearGlobalBrowserStorageAction extends Action2 { - static readonly ID = 'workbench.action.browser.clearGlobalStorage'; + static readonly ID = BrowserViewCommandId.ClearGlobalStorage; constructor() { super({ @@ -408,7 +408,7 @@ class ClearGlobalBrowserStorageAction extends Action2 { } class ClearWorkspaceBrowserStorageAction extends Action2 { - static readonly ID = 'workbench.action.browser.clearWorkspaceStorage'; + static readonly ID = BrowserViewCommandId.ClearWorkspaceStorage; constructor() { super({ @@ -433,7 +433,7 @@ class ClearWorkspaceBrowserStorageAction extends Action2 { } class ClearEphemeralBrowserStorageAction extends Action2 { - static readonly ID = 'workbench.action.browser.clearEphemeralStorage'; + static readonly ID = BrowserViewCommandId.ClearEphemeralStorage; constructor() { super({ @@ -460,7 +460,7 @@ class ClearEphemeralBrowserStorageAction extends Action2 { } class OpenBrowserSettingsAction extends Action2 { - static readonly ID = 'workbench.action.browser.openSettings'; + static readonly ID = BrowserViewCommandId.OpenSettings; constructor() { super({ @@ -486,7 +486,7 @@ class OpenBrowserSettingsAction extends Action2 { // Find actions class ShowBrowserFindAction extends Action2 { - static readonly ID = 'workbench.action.browser.showFind'; + static readonly ID = BrowserViewCommandId.ShowFind; constructor() { super({ @@ -515,7 +515,7 @@ class ShowBrowserFindAction extends Action2 { } class HideBrowserFindAction extends Action2 { - static readonly ID = 'workbench.action.browser.hideFind'; + static readonly ID = BrowserViewCommandId.HideFind; constructor() { super({ @@ -540,7 +540,7 @@ class HideBrowserFindAction extends Action2 { } class BrowserFindNextAction extends Action2 { - static readonly ID = 'workbench.action.browser.findNext'; + static readonly ID = BrowserViewCommandId.FindNext; constructor() { super({ @@ -571,7 +571,7 @@ class BrowserFindNextAction extends Action2 { } class BrowserFindPreviousAction extends Action2 { - static readonly ID = 'workbench.action.browser.findPrevious'; + static readonly ID = BrowserViewCommandId.FindPrevious; constructor() { super({ diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts index c60a295cd5a..d189013b3ad 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts @@ -3,15 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBrowserViewService, ipcBrowserViewChannelName } from '../../../../platform/browserView/common/browserView.js'; +import { BrowserViewCommandId, IBrowserViewService, ipcBrowserViewChannelName } from '../../../../platform/browserView/common/browserView.js'; import { IBrowserViewWorkbenchService, IBrowserViewModel, BrowserViewModel } from '../common/browserView.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { Event } from '../../../../base/common/event.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; -export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService { +/** Command IDs whose accelerators are shown in browser view context menus. */ +const browserViewContextMenuCommands = [ + BrowserViewCommandId.GoBack, + BrowserViewCommandId.GoForward, + BrowserViewCommandId.Reload, +]; + +export class BrowserViewWorkbenchService extends Disposable implements IBrowserViewWorkbenchService { declare readonly _serviceBrand: undefined; private readonly _browserViewService: IBrowserViewService; @@ -20,10 +29,15 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService constructor( @IMainProcessService mainProcessService: IMainProcessService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IKeybindingService private readonly keybindingService: IKeybindingService ) { + super(); const channel = mainProcessService.getChannel(ipcBrowserViewChannelName); this._browserViewService = ProxyChannel.toService(channel); + + this.sendKeybindings(); + this._register(this.keybindingService.onDidUpdateKeybindings(() => this.sendKeybindings())); } async getOrCreateBrowserViewModel(id: string): Promise { @@ -67,4 +81,16 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService return model; } + + private sendKeybindings(): void { + const keybindings: { [commandId: string]: string } = Object.create(null); + for (const commandId of browserViewContextMenuCommands) { + const binding = this.keybindingService.lookupKeybinding(commandId); + const accelerator = binding?.getElectronAccelerator(); + if (accelerator) { + keybindings[commandId] = accelerator; + } + } + void this._browserViewService.updateKeybindings(keybindings); + } } From 4b1876f38c477d1f8fe56c5ad8e1ad5d54ce773c Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Fri, 6 Mar 2026 16:52:29 +0100 Subject: [PATCH 293/448] fix tests --- src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 47c38236ad3..50815341dbb 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -844,7 +844,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { const action = toAction({ id: optionGroup.id, label: optionGroup.name, run: () => { } }); const widget = this.instantiationService.createInstance( optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, - action, initialState, itemDelegate + action, initialState, itemDelegate, undefined ); this._toolbarPickerDisposables.add(widget); From 2dbd83bd9fcd0b08538d85d0a673ead3593e2e05 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Mar 2026 08:26:53 -0800 Subject: [PATCH 294/448] chat: register file system provider via workbench contribution (#299528) * chat: register file system provider via workbench contribution - Moves the registerProvider call from ChatResponseResourceFileSystemProvider constructor to a new ChatResponseResourceWorkbenchContribution class that gets instantiated by the workbench. This ensures the vscode-chat-response-resource:// file system provider is registered even though the service has no other dependencies that would trigger eager instantiation. - Changes the singleton registration from Eager to Delayed since the workbench contribution now depends on it, triggering instantiation. - Fixes file system provider not being found errors (ENOPRO) when MCPs or extensions try to access chat response resources. Fixes #299504 (Commit message generated by Copilot) * bump --- .../contrib/chat/browser/chat.contribution.ts | 5 +++-- .../chatResponseResourceFileSystemProvider.ts | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d9bcf54ea39..4fbdb207fa0 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -37,7 +37,7 @@ import '../common/widget/chatColors.js'; import { IChatEditingService } from '../common/editing/chatEditingService.js'; import { IChatLayoutService } from '../common/widget/chatLayoutService.js'; import { ChatModeService, IChatMode, IChatModeService } from '../common/chatModes.js'; -import { ChatResponseResourceFileSystemProvider, IChatResponseResourceFileSystemProvider } from '../common/widget/chatResponseResourceFileSystemProvider.js'; +import { ChatResponseResourceFileSystemProvider, ChatResponseResourceWorkbenchContribution, IChatResponseResourceFileSystemProvider } from '../common/widget/chatResponseResourceFileSystemProvider.js'; import { IChatService } from '../common/chatService/chatService.js'; import { ChatService } from '../common/chatService/chatServiceImpl.js'; import { IChatSessionsService } from '../common/chatSessionsService.js'; @@ -1747,9 +1747,9 @@ registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, Wo registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored); -registerSingleton(IChatResponseResourceFileSystemProvider, ChatResponseResourceFileSystemProvider, InstantiationType.Eager); registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(ChatResponseResourceWorkbenchContribution.ID, ChatResponseResourceWorkbenchContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContributions, WorkbenchPhase.Eventually); registerWorkbenchContribution2(PromptLanguageFeaturesProvider.ID, PromptLanguageFeaturesProvider, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatWindowNotifier.ID, ChatWindowNotifier, WorkbenchPhase.AfterRestored); @@ -1785,6 +1785,7 @@ registerEditorFeature(ChatPasteProvidersFeature); agentPluginDiscoveryRegistry.register(new SyncDescriptor(ConfiguredAgentPluginDiscovery)); agentPluginDiscoveryRegistry.register(new SyncDescriptor(MarketplaceAgentPluginDiscovery)); +registerSingleton(IChatResponseResourceFileSystemProvider, ChatResponseResourceFileSystemProvider, InstantiationType.Delayed); registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts b/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts index 48596accfdf..6cab24df2d6 100644 --- a/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts @@ -11,14 +11,15 @@ import { newWriteableStream, ReadableStreamEvents } from '../../../../../base/co import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileService, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat } from '../../../../../platform/files/common/files.js'; +import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileService, IFileSystemProvider, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat } from '../../../../../platform/files/common/files.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ChatResponseResource } from '../model/chatModel.js'; import { IChatService, IChatToolInvocation, IChatToolInvocationSerialized } from '../chatService/chatService.js'; import { isToolResultInputOutputDetails } from '../tools/languageModelToolsService.js'; export const IChatResponseResourceFileSystemProvider = createDecorator('chatResponseResourceFileSystemProvider'); -export interface IChatResponseResourceFileSystemProvider { +export interface IChatResponseResourceFileSystemProvider extends IFileSystemProvider { readonly _serviceBrand: undefined; /** @@ -59,7 +60,6 @@ export class ChatResponseResourceFileSystemProvider extends Disposable implement @IFileService private readonly _fileService: IFileService ) { super(); - this._register(this._fileService.registerProvider(ChatResponseResource.scheme, this)); this._register(this.chatService.onDidDisposeSession(e => { for (const sessionResource of e.sessionResource) { const uris = this._sessionAssociations.get(sessionResource); @@ -187,3 +187,16 @@ export class ChatResponseResourceFileSystemProvider extends Disposable implement return part.isText ? new TextEncoder().encode(part.value) : decodeBase64(part.value).buffer; } } + +export class ChatResponseResourceWorkbenchContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chatResponseResourceWorkbenchContribution'; + + constructor( + @IChatResponseResourceFileSystemProvider chatResponseResourceFsProvider: IChatResponseResourceFileSystemProvider, + @IFileService fileService: IFileService, + ) { + super(); + this._register(fileService.registerProvider(ChatResponseResource.scheme, chatResponseResourceFsProvider)); + } +} From 8e0baf5de1712bc8407c7a153344bd73155ae068 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 6 Mar 2026 16:59:42 +0000 Subject: [PATCH 295/448] Refactor workspace trust editor styles for improved layout (#299798) * refactor workspace trust editor styles for improved layout and responsiveness Co-authored-by: Copilot * set max-width for workspace trust editor to improve layout --------- Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../browser/media/workspaceTrustEditor.css | 109 +++++++++++++----- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css index 3bd850fb25b..7ac6aaa7c37 100644 --- a/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ .workspace-trust-editor { - max-width: 1000px; - padding-top: 11px; - margin: auto; - height: calc(100% - 11px); + max-width: 1600px; + padding-top: 12px; + margin: 0; + height: calc(100% - 12px); } .workspace-trust-editor:focus { @@ -15,12 +15,16 @@ } .workspace-trust-editor > .workspace-trust-header { - padding: 14px; + padding: 16px 24px; display: flex; flex-direction: column; align-items: center; } +.workspace-trust-editor > .workspace-trust-header:focus:not(:focus-visible) { + outline: none; +} + .workspace-trust-editor .workspace-trust-header .workspace-trust-title { font-size: 24px; font-weight: 600; @@ -35,7 +39,7 @@ } .workspace-trust-editor .workspace-trust-header .workspace-trust-title .workspace-trust-title-icon { - color: var(--workspace-trust-selected-color) !important; + color: var(--workspace-trust-selected-color); } .workspace-trust-editor .workspace-trust-header .workspace-trust-description { @@ -43,7 +47,8 @@ user-select: text; max-width: 600px; text-align: center; - padding: 14px 0; + padding: 8px 0; + line-height: 20px; } .workspace-trust-editor .workspace-trust-section-title { @@ -64,59 +69,67 @@ /** Features List */ .workspace-trust-editor .workspace-trust-features { - padding: 14px; + padding: 24px; cursor: default; user-select: text; display: flex; flex-direction: row; - flex-flow: wrap; - justify-content: space-evenly; + flex-wrap: wrap; + justify-content: center; + gap: 16px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { - min-height: 315px; border: 1px solid var(--workspace-trust-unselected-color); - margin: 4px 4px; + border-radius: var(--vscode-cornerRadius-medium); display: flex; flex-direction: column; - padding: 10px 40px; + padding: 8px 36px; } .workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted, .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted { - border-width: 2px; - border-color: var(--workspace-trust-selected-color) !important; - padding: 9px 39px; - outline-offset: 2px; + outline: 2px solid var(--workspace-trust-selected-color); + outline-offset: -2px; +} + +.workspace-trust-editor .workspace-trust-features .workspace-trust-limitations:focus:not(:focus-visible) { + outline: none; +} + +.workspace-trust-editor .workspace-trust-features .workspace-trust-limitations:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations ul { list-style: none; padding-inline-start: 0px; + margin: 16px 0; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations li { display: flex; - padding-bottom: 10px; + padding-bottom: 4px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations .list-item-icon { - padding-right: 5px; line-height: 24px; + padding: 0 6px 0 0; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations.trusted .list-item-icon { - color: var(--workspace-trust-check-color) !important; - font-size: 18px; + color: var(--workspace-trust-check-color); + font-size: 16px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations.untrusted .list-item-icon { - color: var(--workspace-trust-x-color) !important; - font-size: 20px; + color: var(--workspace-trust-x-color); + font-size: 16px; } .workspace-trust-editor .workspace-trust-features .workspace-trust-limitations .list-item-text { - font-size: 16px; + font-size: 14px; line-height: 24px; } @@ -130,7 +143,7 @@ font-size: 16px; font-weight: 600; line-height: 24px; - padding: 10px 0px; + padding: 16px 0px; display: flex; } @@ -143,12 +156,13 @@ .workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon, .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted .workspace-trust-limitations-header .workspace-trust-limitations-title .workspace-trust-title-icon { display: unset; - color: var(--workspace-trust-selected-color) !important; + color: var(--workspace-trust-selected-color); } .workspace-trust-editor .workspace-trust-features .workspace-trust-untrusted-description { font-style: italic; - padding-bottom: 10px; + color: var(--vscode-descriptionForeground); + padding-bottom: 8px; } /** Buttons Container */ @@ -170,7 +184,7 @@ } .workspace-trust-editor .workspace-trust-settings .trusted-uris-button-bar { - margin-top: 5px; + margin-top: 8px; } .workspace-trust-editor .workspace-trust-settings .trusted-uris-button-bar .monaco-button { @@ -183,7 +197,7 @@ .workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons > .monaco-button, .workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons > .monaco-button-dropdown, .workspace-trust-editor .workspace-trust-settings .trusted-uris-button-bar .monaco-button { - margin: 4px 5px; /* allows button focus outline to be visible */ + margin: 8px 4px; /* allows button focus outline to be visible */ } .workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown .monaco-dropdown-button { @@ -192,7 +206,7 @@ .workspace-trust-limitations { width: 50%; - max-width: 350px; + max-width: 400px; min-width: 250px; flex: 1; } @@ -208,7 +222,7 @@ } .workspace-trust-intro-dialog .workspace-trust-dialog-image-row.badge-row img { - max-height: 40px; + max-height: 36px; padding-right: 10px; } @@ -218,7 +232,9 @@ } .workspace-trust-editor .workspace-trust-settings { - padding: 20px 14px; + padding: 24px 36px; + border-top: 1px solid var(--vscode-editorWidget-border); + margin-top: 8px; } .workspace-trust-editor .workspace-trust-settings .workspace-trusted-folders-title { @@ -229,6 +245,10 @@ display: none; } +.workspace-trust-editor .trusted-uris-table { + margin-top: 16px; +} + .workspace-trust-editor .monaco-table-tr .monaco-table-td .path { width: 100%; } @@ -292,3 +312,28 @@ .workspace-trust-editor .workspace-trust-settings .monaco-list-row:hover .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { display: flex; } + +/** Responsive: single-column layout for narrow widths */ +@container (max-width: 600px) { + .workspace-trust-editor .workspace-trust-features { + flex-direction: column; + align-items: center; + } + + .workspace-trust-limitations { + width: 100%; + max-width: 400px; + } +} + +@media (max-width: 600px) { + .workspace-trust-editor .workspace-trust-features { + flex-direction: column; + align-items: center; + } + + .workspace-trust-limitations { + width: 100%; + max-width: 400px; + } +} From 3623d50299a82428c437ee7702af4fc427f0f287 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Mar 2026 12:04:21 -0500 Subject: [PATCH 296/448] Render command title vs ID for chat tip hover (#299811) fixes #299579 --- .../contrib/chat/browser/chatTipCatalog.ts | 18 +++++++++--------- .../chat/test/browser/chatTipService.test.ts | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index f112d72e2cd..404ab0b82b7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -127,7 +127,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.switchToAuto', - "Using GPT-4.1? Try switching to [Auto](command:workbench.action.chat.openModelPicker) in the model picker for better coding performance." + "Using GPT-4.1? Try switching to [Auto](command:workbench.action.chat.openModelPicker \"Open Model Picker\") in the model picker for better coding performance." ) ); }, @@ -142,7 +142,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.init', - "Use [{0}](command:{1}){2} to generate or update a workspace instructions file for AI coding agents.", + "Use [{0}](command:{1} \"Run /init\"){2} to generate or update a workspace instructions file for AI coding agents.", '/init', GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, kb @@ -163,7 +163,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.createPrompt', - "Use [{0}](command:{1}){2} to generate a reusable prompt file with the agent.", + "Use [{0}](command:{1} \"Run /create-prompt\"){2} to generate a reusable prompt file with the agent.", '/create-prompt', GENERATE_PROMPT_COMMAND_ID, kb @@ -185,7 +185,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.createAgent', - "Use [{0}](command:{1}){2} to scaffold a custom agent for your workflow.", + "Use [{0}](command:{1} \"Run /create-agent\"){2} to scaffold a custom agent for your workflow.", '/create-agent', GENERATE_AGENT_COMMAND_ID, kb @@ -207,7 +207,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.createSkill', - "Use [{0}](command:{1}){2} to create a skill the agent can load when relevant.", + "Use [{0}](command:{1} \"Run /create-skill\"){2} to create a skill the agent can load when relevant.", '/create-skill', GENERATE_SKILL_COMMAND_ID, kb @@ -229,7 +229,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.planMode', - "Try the [{0}](command:workbench.action.chat.openPlan){1} to research and plan before implementing changes.", + "Try the [{0}](command:workbench.action.chat.openPlan \"Start Plan Mode\"){1} to research and plan before implementing changes.", 'Plan agent', kb ) @@ -301,7 +301,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.forkConversation', - "Use [{0}](command:{1}){2} to branch the conversation. Explore a different approach without losing the original context.", + "Use [{0}](command:{1} \"Run /fork\"){2} to branch the conversation. Explore a different approach without losing the original context.", '/fork', INSERT_FORK_CONVERSATION_COMMAND_ID, kb @@ -321,7 +321,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.agenticBrowser', - "Enable [{0}](command:workbench.action.openSettings?%5B%22workbench.browser.enableChatTools%22%5D) to let the agent open and interact with pages in the Integrated Browser.", + "Enable [{0}](command:workbench.action.openSettings?%5B%22workbench.browser.enableChatTools%22%5D \"Open Settings\") to let the agent open and interact with pages in the Integrated Browser.", 'agentic browser integration' ) ); @@ -362,7 +362,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ return new MarkdownString( localize( 'tip.thinkingPhrases', - "Customize the loading messages shown while the agent works with [{0}](command:workbench.action.openSettings?%5B%22{1}%22%5D).", + "Customize the loading messages shown while the agent works with [{0}](command:workbench.action.openSettings?%5B%22{1}%22%5D \"Open Settings\").", 'thinking phrases', ChatConfiguration.ThinkingPhrases ) diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 66009e7549b..dc13e3e902e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -138,6 +138,22 @@ suite('ChatTipService', () => { assert.ok(tip.content.value.length > 0, 'Tip should have content'); }); + test('uses descriptive titles for tip command links', () => { + for (const tip of TIP_CATALOG) { + const markdown = tip.buildMessage({ + keybindingService: { + lookupKeybinding: () => undefined, + } as Partial as IKeybindingService, + }).value; + + const commandLinkRegex = /\[[^\]]+\]\((command:[^)]+)\)/g; + let match: RegExpExecArray | null; + while ((match = commandLinkRegex.exec(markdown)) !== null) { + assert.ok(/\s"[^"]+"$/.test(match[1]), `Expected command link in ${tip.id} to include a descriptive title: ${match[0]}`); + } + } + }); + test('records # file reference usage for attach files tip eligibility', () => { const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>()); instantiationService.stub(IChatService, { From 2b32258b0e9c13dc1c61124145f7457861666247 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 6 Mar 2026 18:09:01 +0100 Subject: [PATCH 297/448] merge to main (#299794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sessions - fix chat input shrinking at narrow widths (#299498) style - set width to 100% for `interactive-input-part` * modal - force focus into first modal editor always * fix: update precondition for FixDiagnosticsAction and hide input widget on command execution (#299499) fixes https://github.com/microsoft/vscode/issues/299251 * refactor: remove workspace context service dependency from FolderPicker * Add logging for agent feedback actions * modal - some fixes to actions and layout * modal - surface some editor actions in a new toolbar (#299582) * modal - surface some editor actions in a new toolbar * ccr * keybindings - remove "Edit as JSON" as its now available from the title menu * settings - remove "Edit as JSON" as its now available from the title menu * update hover fixes * terminal fixes * terminal improvements * Sessions: fix auth scopes of gh FSP * sessions customizations: make it easier to scan mcp/plugin marketplac… (#299636) sessions customizations: make it easier to scan mcp/plugin marketplace list * sessions: add built-in prompt files with override support (#299629) * sessions: add built-in prompt files with override support Ship bundled .prompt.md files with the Sessions app that appear as slash commands out of the box. Built-in prompts use a BUILTIN_STORAGE constant (cast as PromptsStorage) defined in the aiCustomization layer, avoiding changes to the core PromptsStorage enum and prompt service types. - AgenticPromptsService discovers prompts from vs/sessions/prompts/ at runtime via FileAccess and injects them into the listing pipeline - Override logic: user/workspace prompts with matching names take precedence over built-in ones - Built-in prompts open as read-only in the management editor - Sessions tree view, workspace service, and counts handle BUILTIN_STORAGE - Add /create-pr as the first built-in prompt - Bundle prompt files via gulpfile resource includes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * sessions: use AICustomizationPromptsStorage type for builtin storage Adopt the new AICustomizationPromptsStorage union type in the sessions tree view method signature. Use string-keyed Records and targeted casts at the PromptsStorage boundary to stay type-safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * sessions: remove PromptsStorage casts, widen IStorageSourceFilter Use AICustomizationPromptsStorage in sessions-local interfaces (IAICustomizationGroupItem, IAICustomizationFileItem) and widen IStorageSourceFilter.sources to readonly string[] so BUILTIN_STORAGE flows through without casts. The only remaining cast is at the IPromptPath creation boundary in AgenticPromptsService. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * sessions: move BUILTIN_STORAGE to sessions common layer Move AICustomizationPromptsStorage type and BUILTIN_STORAGE constant from the workbench browser UI module to sessions/contrib/chat/common so that AgenticPromptsService (a service) does not depend on UI code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * sessions: fix ESLint dangerous type assertion in builtin prompts (#299663) Replace the `as IPromptPath` cast in discoverBuiltinPrompts with a createBuiltinPromptPath factory function that contains the type narrowing in one place, satisfying the code-no-dangerous-type-assertions ESLint rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enhance Agent Sessions Control and Renderer with observable active session resource * fix terminal * Enable model management in NewChatWidget * review feedback * different competion settings for copilot markdown and plaintext --------- Co-authored-by: Benjamin Pasero Co-authored-by: Benjamin Pasero Co-authored-by: Johannes Rieken Co-authored-by: BeniBenj Co-authored-by: Osvaldo Ortega Co-authored-by: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- build/gulpfile.vscode.ts | 1 + extensions/github/src/commands.ts | 26 - .../markdown-language-features/package.json | 8 + src/vs/platform/actions/common/actions.ts | 1 + src/vs/sessions/AI_CUSTOMIZATIONS.md | 12 + src/vs/sessions/browser/media/style.css | 1 + src/vs/sessions/browser/workbench.ts | 2 + .../browser/media/updateHoverWidget.css | 1 + .../test/browser/updateHoverWidget.fixture.ts | 2 - .../browser/agentFeedbackEditorActions.ts | 65 +- .../browser/agentFeedbackEditorOverlay.ts | 29 +- .../agentFeedbackEditorWidgetContribution.ts | 289 +++++--- .../browser/agentFeedbackService.ts | 71 +- .../media/agentFeedbackEditorWidget.css | 92 ++- .../browser/sessionEditorComments.ts | 119 ++++ ...gentFeedbackEditorOverlayWidget.fixture.ts | 136 ++++ .../agentFeedbackEditorWidget.fixture.ts | 346 +++++++++ .../browser/sessionEditorComments.test.ts | 98 +++ .../browser/aiCustomizationTreeViewViews.ts | 32 +- .../changesView/browser/changesView.ts | 73 +- .../changesView/browser/media/changesView.css | 13 + .../aiCustomizationWorkspaceService.ts | 5 +- .../contrib/chat/browser/folderPicker.ts | 6 +- .../contrib/chat/browser/newChatViewPane.ts | 2 +- .../contrib/chat/browser/promptsService.ts | 79 ++- .../chat/common/builtinPromptsStorage.ts | 28 + .../browser/codeReview.contributions.ts | 149 ++++ .../codeReview/browser/codeReviewService.ts | 353 ++++++++++ .../test/browser/codeReviewService.test.ts | 661 ++++++++++++++++++ .../browser/configuration.contribution.ts | 5 + .../browser/githubFileSystemProvider.ts | 8 +- .../sessions/browser/customizationCounts.ts | 6 +- .../test/browser/customizationCounts.test.ts | 20 +- .../browser/sessionsTerminalContribution.ts | 279 ++++---- .../sessionsTerminalContribution.test.ts | 194 ++--- src/vs/sessions/prompts/create-pr.prompt.md | 11 + src/vs/sessions/sessions.desktop.main.ts | 1 + .../parts/editor/editor.contribution.ts | 7 +- .../parts/editor/media/modalEditorPart.css | 8 + .../browser/parts/editor/modalEditorPart.ts | 43 +- .../agentSessions/agentSessionsControl.ts | 6 +- .../agentSessions/agentSessionsViewer.ts | 6 +- .../aiCustomization/aiCustomizationIcons.ts | 5 + .../aiCustomizationListWidget.ts | 7 +- .../aiCustomizationManagement.ts | 12 + .../aiCustomizationManagementEditor.ts | 3 +- .../browser/aiCustomization/mcpListWidget.ts | 22 +- .../media/aiCustomizationManagement.css | 13 + .../aiCustomization/pluginListWidget.ts | 22 +- .../chatSetup/chatSetupContributions.ts | 9 +- .../common/aiCustomizationWorkspaceService.ts | 6 +- .../preferences/browser/keybindingsEditor.ts | 16 +- .../browser/media/keybindingsEditor.css | 31 +- .../browser/media/settingsEditor2.css | 7 - .../browser/preferences.contribution.ts | 47 +- .../preferences/browser/settingsEditor2.ts | 14 +- .../actions/common/menusExtensionPoint.ts | 5 + .../services/editor/browser/editorService.ts | 20 + .../test/browser/modalEditorGroup.test.ts | 21 +- .../agentSessionsViewer.fixture.ts | 2 +- 60 files changed, 3000 insertions(+), 556 deletions(-) create mode 100644 src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts create mode 100644 src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlayWidget.fixture.ts create mode 100644 src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts create mode 100644 src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts create mode 100644 src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts create mode 100644 src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts create mode 100644 src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts create mode 100644 src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts create mode 100644 src/vs/sessions/prompts/create-pr.prompt.md diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index ac4ee9cec7d..e343569490d 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -100,6 +100,7 @@ const vscodeResourceIncludes = [ // Sessions 'out-build/vs/sessions/contrib/chat/browser/media/*.svg', + 'out-build/vs/sessions/prompts/*.prompt.md', // Extensions 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 33acf5a406b..78dd3271588 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -90,28 +90,6 @@ function resolveSessionRepo(gitAPI: GitAPI, sessionMetadata: { worktreePath?: st return { repository, remoteInfo, gitRemote: { name: gitRemote.name, fetchUrl: gitRemote.fetchUrl! }, head: head as ResolvedSessionRepo['head'] }; } -async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { - const resolved = resolveSessionRepo(gitAPI, sessionMetadata, false); - if (!resolved) { - vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false); - return; - } - - try { - const octokit = await getOctokit(); - const { data: openPRs } = await octokit.pulls.list({ - owner: resolved.remoteInfo.owner, - repo: resolved.remoteInfo.repo, - head: `${resolved.remoteInfo.owner}:${resolved.head.name}`, - state: 'all', - }); - - vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', openPRs.length > 0); - } catch { - vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false); - } -} - async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { if (!sessionResource) { return; @@ -263,9 +241,5 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return openPullRequest(gitAPI, sessionResource, sessionMetadata); })); - disposables.add(vscode.commands.registerCommand('github.checkOpenPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => { - return checkOpenPullRequest(gitAPI, sessionResource, sessionMetadata); - })); - return disposables; } diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 745ba66b56c..2993574a7fa 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -230,6 +230,14 @@ "group": "1_markdown" } ], + "modalEditor/editorTitle": [ + { + "command": "markdown.showPreviewToSide", + "when": "editorLangId =~ /^(markdown|prompt|instructions|chatagent|skill)$/ && !notebookEditorFocused && !hasCustomMarkdownPreview", + "alt": "markdown.showPreview", + "group": "navigation" + } + ], "explorer/context": [ { "command": "markdown.showPreview", diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index e563293ea2a..497ec1a43ab 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -89,6 +89,7 @@ export class MenuId { static readonly EditorContextShare = new MenuId('EditorContextShare'); static readonly EditorTitle = new MenuId('EditorTitle'); static readonly ModalEditorTitle = new MenuId('ModalEditorTitle'); + static readonly ModalEditorEditorTitle = new MenuId('ModalEditorEditorTitle'); static readonly CompactWindowEditorTitle = new MenuId('CompactWindowEditorTitle'); static readonly EditorTitleRun = new MenuId('EditorTitleRun'); static readonly EditorTitleContext = new MenuId('EditorTitleContext'); diff --git a/src/vs/sessions/AI_CUSTOMIZATIONS.md b/src/vs/sessions/AI_CUSTOMIZATIONS.md index a3309067c7f..e3c8ec0b8d1 100644 --- a/src/vs/sessions/AI_CUSTOMIZATIONS.md +++ b/src/vs/sessions/AI_CUSTOMIZATIONS.md @@ -90,9 +90,21 @@ The shared `applyStorageSourceFilter()` helper applies this filter to any `{uri, Sessions overrides `PromptsService` via `AgenticPromptsService` (in `promptsService.ts`): - **Discovery**: `AgenticPromptFilesLocator` scopes workspace folders to the active session's worktree +- **Built-in prompts**: Discovers bundled `.prompt.md` files from `vs/sessions/prompts/` and surfaces them with `PromptsStorage.builtin` storage type +- **User override**: Built-in prompts are omitted when a user or workspace prompt with the same name exists - **Creation targets**: `getSourceFolders()` override replaces VS Code profile user roots with `~/.copilot/{subfolder}` for CLI compatibility - **Hook folders**: Falls back to `.github/hooks` in the active worktree +### Built-in Prompts + +Prompt files bundled with the Sessions app live in `src/vs/sessions/prompts/`. They are: + +- Discovered at runtime via `FileAccess.asFileUri('vs/sessions/prompts')` +- Tagged with `PromptsStorage.builtin` storage type +- Shown in a "Built-in" group in the AI Customization tree view and management editor +- Filtered out when a user/workspace prompt shares the same clean name (override behavior) +- Included in storage filters for prompts and CLI-user types + ### Count Consistency `customizationCounts.ts` uses the **same data sources** as the list widget's `loadItems()`: diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 299ce8545d8..4454c30fb8c 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -68,6 +68,7 @@ } .agent-sessions-workbench .interactive-session .interactive-input-part { + width: 100%; max-width: 950px; margin: 0 auto !important; display: inherit !important; diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index b54a7e6a0d9..1b34d6c3aec 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -591,6 +591,8 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { this.getPart(Parts.EDITOR_PART).create(editorPartContainer, { restorePreviousState: false }); mark('code/didCreatePart/workbench.parts.editor'); + this.getPart(Parts.EDITOR_PART).layout(0, 0, 0, 0); // needed to make some view methods work + this.mainContainer.appendChild(editorPartContainer); } diff --git a/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css b/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css index 752f4e7faca..6291d8e2922 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css +++ b/src/vs/sessions/contrib/accountMenu/browser/media/updateHoverWidget.css @@ -8,6 +8,7 @@ flex-direction: column; gap: 8px; min-width: 200px; + padding: 12px 16px; } .sessions-update-hover-header { diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts index 8092585ab6d..6ca47e2ee1b 100644 --- a/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts +++ b/src/vs/sessions/contrib/accountMenu/test/browser/updateHoverWidget.fixture.ts @@ -32,8 +32,6 @@ function createMockUpdateService(state: State): IUpdateService { } function renderHoverWidget(ctx: ComponentFixtureContext, state: State): void { - ctx.container.style.padding = '16px'; - ctx.container.style.width = '320px'; ctx.container.style.backgroundColor = 'var(--vscode-editorHoverWidget-background)'; const instantiationService = createEditorServices(ctx.disposableStore, { diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts index 620cf99ae3d..2fd5134bb04 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts @@ -6,8 +6,9 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { URI } from '../../../../base/common/uri.js'; import { isEqual } from '../../../../base/common/resources.js'; import { EditorsOrder, IEditorIdentifier } from '../../../../workbench/common/editor.js'; @@ -16,14 +17,20 @@ import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/c import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { IAgentFeedbackService } from './agentFeedbackService.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'; +import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; +import { getSessionEditorComments } from './sessionEditorComments.js'; export const submitFeedbackActionId = 'agentFeedbackEditor.action.submit'; export const navigatePreviousFeedbackActionId = 'agentFeedbackEditor.action.navigatePrevious'; export const navigateNextFeedbackActionId = 'agentFeedbackEditor.action.navigateNext'; export const clearAllFeedbackActionId = 'agentFeedbackEditor.action.clearAll'; export const navigationBearingFakeActionId = 'agentFeedbackEditor.navigation.bearings'; +export const hasSessionEditorComments = new RawContextKey('agentFeedbackEditor.hasSessionComments', false); +export const hasSessionAgentFeedback = new RawContextKey('agentFeedbackEditor.hasAgentFeedback', false); abstract class AgentFeedbackEditorAction extends Action2 { @@ -37,16 +44,27 @@ abstract class AgentFeedbackEditorAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const agentFeedbackService = accessor.get(IAgentFeedbackService); + const chatEditingService = accessor.get(IChatEditingService); + const agentSessionsService = accessor.get(IAgentSessionsService); + const codeReviewService = accessor.get(ICodeReviewService); const candidates = getActiveResourceCandidates(editorService.activeEditorPane?.input); - const sessionResource = candidates - .map(candidate => agentFeedbackService.getMostRecentSessionForResource(candidate)) - .find((value): value is URI => !!value); - if (!sessionResource) { - return; - } + for (const candidate of candidates) { + const sessionResource = getSessionForResource(candidate, chatEditingService, agentSessionsService) + ?? agentFeedbackService.getMostRecentSessionForResource(candidate); + if (!sessionResource) { + continue; + } - return this.runWithSession(accessor, sessionResource); + const comments = getSessionEditorComments( + sessionResource, + agentFeedbackService.getFeedback(sessionResource), + codeReviewService.getReviewState(sessionResource).get(), + ); + if (comments.length > 0) { + return this.runWithSession(accessor, sessionResource); + } + } } abstract runWithSession(accessor: ServicesAccessor, sessionResource: URI): Promise | void; @@ -65,7 +83,7 @@ class SubmitFeedbackAction extends AgentFeedbackEditorAction { id: Menus.AgentFeedbackEditorContent, group: 'a_submit', order: 0, - when: ChatContextKeys.enabled, + when: ContextKeyExpr.and(ChatContextKeys.enabled, hasSessionAgentFeedback), }, }); } @@ -74,9 +92,11 @@ class SubmitFeedbackAction extends AgentFeedbackEditorAction { const chatWidgetService = accessor.get(IChatWidgetService); const agentFeedbackService = accessor.get(IAgentFeedbackService); const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); if (!widget) { + logService.error('[AgentFeedback] Cannot submit feedback: no chat widget found for session', sessionResource.toString()); return; } @@ -114,27 +134,36 @@ class NavigateFeedbackAction extends AgentFeedbackEditorAction { id: Menus.AgentFeedbackEditorContent, group: 'navigate', order: _next ? 2 : 1, - when: ChatContextKeys.enabled, + when: ContextKeyExpr.and(ChatContextKeys.enabled, hasSessionEditorComments), }, }); } - override runWithSession(accessor: ServicesAccessor, sessionResource: URI): void { + override async runWithSession(accessor: ServicesAccessor, sessionResource: URI): Promise { const agentFeedbackService = accessor.get(IAgentFeedbackService); + const codeReviewService = accessor.get(ICodeReviewService); const editorService = accessor.get(IEditorService); + const comments = getSessionEditorComments( + sessionResource, + agentFeedbackService.getFeedback(sessionResource), + codeReviewService.getReviewState(sessionResource).get(), + ); - const feedback = agentFeedbackService.getNextFeedback(sessionResource, this._next); - if (!feedback) { + const comment = agentFeedbackService.getNextNavigableItem(sessionResource, comments, this._next); + if (!comment) { return; } - editorService.openEditor({ - resource: feedback.resourceUri, + await editorService.openEditor({ + resource: comment.resourceUri, options: { preserveFocus: false, revealIfVisible: true, + selection: { startLineNumber: comment.range.startLineNumber, startColumn: comment.range.startColumn }, // place the cursor but not selection } }); + + agentFeedbackService.setNavigationAnchor(sessionResource, comment.id); } } @@ -152,7 +181,7 @@ class ClearAllFeedbackAction extends AgentFeedbackEditorAction { id: Menus.AgentFeedbackEditorContent, group: 'a_submit', order: 1, - when: ChatContextKeys.enabled, + when: ContextKeyExpr.and(ChatContextKeys.enabled, hasSessionAgentFeedback), }, }); } @@ -177,6 +206,6 @@ export function registerAgentFeedbackEditorActions(): void { }, group: 'navigate', order: -1, - when: ChatContextKeys.enabled, + when: ContextKeyExpr.and(ChatContextKeys.enabled, hasSessionEditorComments), }); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts index 56cb43ad934..60990b03926 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts @@ -18,13 +18,15 @@ import { IWorkbenchContribution } from '../../../../workbench/common/contributio import { EditorGroupView } from '../../../../workbench/browser/parts/editor/editorGroupView.js'; import { IEditorGroup, IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; import { IAgentFeedbackService } from './agentFeedbackService.js'; -import { navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from './agentFeedbackEditorActions.js'; +import { hasSessionAgentFeedback, hasSessionEditorComments, navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from './agentFeedbackEditorActions.js'; import { assertType } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.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'; +import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; +import { getSessionEditorComments, hasAgentFeedbackComments } from './sessionEditorComments.js'; class AgentFeedbackActionViewItem extends ActionViewItem { @@ -54,7 +56,7 @@ class AgentFeedbackActionViewItem extends ActionViewItem { } } -class AgentFeedbackOverlayWidget extends Disposable { +export class AgentFeedbackOverlayWidget extends Disposable { private readonly _domNode: HTMLElement; private readonly _toolbarNode: HTMLElement; @@ -145,6 +147,8 @@ class AgentFeedbackOverlayController { @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IInstantiationService instaService: IInstantiationService, @IChatEditingService chatEditingService: IChatEditingService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICodeReviewService codeReviewService: ICodeReviewService, ) { this._domNode.classList.add('agent-feedback-editor-overlay'); this._domNode.style.position = 'absolute'; @@ -155,6 +159,8 @@ class AgentFeedbackOverlayController { const widget = this._store.add(instaService.createInstance(AgentFeedbackOverlayWidget)); this._domNode.appendChild(widget.getDomNode()); this._store.add(toDisposable(() => this._domNode.remove())); + const hasCommentsContext = hasSessionEditorComments.bindTo(contextKeyService); + const hasAgentFeedbackContext = hasSessionAgentFeedback.bindTo(contextKeyService); const show = () => { if (!container.contains(this._domNode)) { @@ -181,19 +187,34 @@ class AgentFeedbackOverlayController { const candidates = getActiveResourceCandidates(group.activeEditorPane?.input); let navigationBearings = undefined; + let hasAgentFeedback = false; for (const candidate of candidates) { const sessionResource = getSessionForResource(candidate, chatEditingService, agentSessionsService); - if (sessionResource && agentFeedbackService.getFeedback(sessionResource).length > 0) { - navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource); + if (!sessionResource) { + continue; + } + + const comments = getSessionEditorComments( + sessionResource, + agentFeedbackService.getFeedback(sessionResource), + codeReviewService.getReviewState(sessionResource).read(r), + ); + if (comments.length > 0) { + navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource, comments); + hasAgentFeedback = hasAgentFeedbackComments(comments); break; } } if (!navigationBearings) { + hasCommentsContext.set(false); + hasAgentFeedbackContext.set(false); hide(); return; } + hasCommentsContext.set(true); + hasAgentFeedbackContext.set(hasAgentFeedback); widget.show(navigationBearings); show(); })); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index 42e5e404bed..987c39acfef 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -5,9 +5,11 @@ import './media/agentFeedbackEditorWidget.css'; +import { Action } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; +import { autorun, observableSignalFromEvent } from '../../../../base/common/observable.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; @@ -19,46 +21,16 @@ import { Range } from '../../../../editor/common/core/range.js'; import { overviewRulerRangeHighlight } from '../../../../editor/common/core/editorColorRegistry.js'; import { OverviewRulerLane } from '../../../../editor/common/model.js'; import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import * as nls from '../../../../nls.js'; -import { IAgentFeedback, IAgentFeedbackService } from './agentFeedbackService.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'; - -/** - * Groups nearby feedback items within a threshold number of lines. - */ -function groupNearbyFeedback(items: readonly IAgentFeedback[], lineThreshold: number = 5): IAgentFeedback[][] { - if (items.length === 0) { - return []; - } - - // Sort by start line number - const sorted = [...items].sort((a, b) => a.range.startLineNumber - b.range.startLineNumber); - - const groups: IAgentFeedback[][] = []; - let currentGroup: IAgentFeedback[] = [sorted[0]]; - - for (let i = 1; i < sorted.length; i++) { - const firstItem = currentGroup[0]; - const currentItem = sorted[i]; - - const verticalSpan = currentItem.range.startLineNumber - firstItem.range.startLineNumber; - - if (verticalSpan <= lineThreshold) { - currentGroup.push(currentItem); - } else { - groups.push(currentGroup); - currentGroup = [currentItem]; - } - } - - if (currentGroup.length > 0) { - groups.push(currentGroup); - } - - return groups; -} +import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; +import { getResourceEditorComments, getSessionEditorComments, groupNearbySessionEditorComments, ISessionEditorComment, SessionEditorCommentSource, toSessionEditorCommentId } from './sessionEditorComments.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { isEqual } from '../../../../base/common/resources.js'; /** * Widget that displays agent feedback comments for a group of nearby feedback items. @@ -87,9 +59,10 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid constructor( private readonly _editor: ICodeEditor, - private readonly _feedbackItems: readonly IAgentFeedback[], - private readonly _agentFeedbackService: IAgentFeedbackService, + private readonly _commentItems: readonly ISessionEditorComment[], private readonly _sessionResource: URI, + @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, + @ICodeReviewService private readonly _codeReviewService: ICodeReviewService, ) { super(); @@ -171,26 +144,13 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid } private _dismiss(): void { - // Remove all feedback items in this widget from the service - for (const feedback of this._feedbackItems) { - this._agentFeedbackService.removeFeedback(this._sessionResource, feedback.id); + for (const comment of this._commentItems) { + this._removeComment(comment); } - - this._domNode.classList.add('fadeOut'); - - const dispose = () => { - this.dispose(); - }; - - const handle = setTimeout(dispose, 150); - this._domNode.addEventListener('animationend', () => { - clearTimeout(handle); - dispose(); - }, { once: true }); } private _updateTitle(): void { - const count = this._feedbackItems.length; + const count = this._commentItems.length; if (count === 1) { this._titleNode.textContent = nls.localize('oneComment', "1 comment"); } else { @@ -213,37 +173,141 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid clearNode(this._bodyNode); this._itemElements.clear(); - for (const feedback of this._feedbackItems) { + for (const comment of this._commentItems) { const item = $('div.agent-feedback-widget-item'); - this._itemElements.set(feedback.id, item); - - // Line indicator - const lineInfo = $('span.agent-feedback-widget-line-info'); - if (feedback.range.startLineNumber === feedback.range.endLineNumber) { - lineInfo.textContent = nls.localize('lineNumber', "Line {0}", feedback.range.startLineNumber); - } else { - lineInfo.textContent = nls.localize('lineRange', "Lines {0}-{1}", feedback.range.startLineNumber, feedback.range.endLineNumber); + item.classList.add(`agent-feedback-widget-item-${comment.source}`); + if (comment.suggestion) { + item.classList.add('agent-feedback-widget-item-suggestion'); } - item.appendChild(lineInfo); + this._itemElements.set(comment.id, item); + + const itemHeader = $('div.agent-feedback-widget-item-header'); + const itemMeta = $('div.agent-feedback-widget-item-meta'); + + const lineInfo = $('span.agent-feedback-widget-line-info'); + if (comment.range.startLineNumber === comment.range.endLineNumber) { + lineInfo.textContent = nls.localize('lineNumber', "Line {0}", comment.range.startLineNumber); + } else { + lineInfo.textContent = nls.localize('lineRange', "Lines {0}-{1}", comment.range.startLineNumber, comment.range.endLineNumber); + } + itemMeta.appendChild(lineInfo); + + if (comment.source !== SessionEditorCommentSource.AgentFeedback) { + const typeBadge = $('span.agent-feedback-widget-item-type'); + typeBadge.textContent = this._getTypeLabel(comment); + itemMeta.appendChild(typeBadge); + } + + itemHeader.appendChild(itemMeta); + + const actionBarContainer = $('div.agent-feedback-widget-item-actions'); + const actionBar = this._eventStore.add(new ActionBar(actionBarContainer)); + if (comment.canConvertToAgentFeedback) { + actionBar.push(new Action( + 'agentFeedback.widget.convert', + nls.localize('convertComment', "Convert to Agent Feedback"), + ThemeIcon.asClassName(Codicon.comment), + true, + () => this._convertToAgentFeedback(comment), + ), { icon: true, label: false }); + } + actionBar.push(new Action( + 'agentFeedback.widget.remove', + nls.localize('removeComment', "Remove"), + ThemeIcon.asClassName(Codicon.close), + true, + () => this._removeComment(comment), + ), { icon: true, label: false }); + itemHeader.appendChild(actionBarContainer); + item.appendChild(itemHeader); - // Feedback text const text = $('span.agent-feedback-widget-text'); - text.textContent = feedback.text; + text.textContent = comment.text; item.appendChild(text); - // Hover handlers for range highlighting + if (comment.suggestion?.edits.length) { + item.appendChild(this._renderSuggestion(comment)); + } + this._eventStore.add(addDisposableListener(item, 'mouseenter', () => { - this._highlightRange(feedback); + this._highlightRange(comment); })); this._eventStore.add(addDisposableListener(item, 'mouseleave', () => { this._rangeHighlightDecoration.clear(); })); + this._eventStore.add(addDisposableListener(item, 'click', e => { + if ((e.target as HTMLElement | null)?.closest('.action-bar')) { + return; + } + this.focusFeedback(comment.id); + this._agentFeedbackService.setNavigationAnchor(this._sessionResource, comment.id); + this._revealComment(comment); + })); + this._bodyNode.appendChild(item); } } + private _getTypeLabel(comment: ISessionEditorComment): string { + if (comment.source === SessionEditorCommentSource.CodeReview) { + return comment.suggestion + ? nls.localize('reviewSuggestion', "Review Suggestion") + : nls.localize('reviewComment', "Review"); + } + + return comment.suggestion + ? nls.localize('feedbackSuggestion', "Feedback Suggestion") + : nls.localize('feedbackComment', "Feedback"); + } + + private _renderSuggestion(comment: ISessionEditorComment): HTMLElement { + const suggestionNode = $('div.agent-feedback-widget-suggestion'); + const title = $('div.agent-feedback-widget-suggestion-title'); + title.textContent = nls.localize('suggestedChange', "Suggested Change"); + suggestionNode.appendChild(title); + + for (const edit of comment.suggestion?.edits ?? []) { + const editNode = $('div.agent-feedback-widget-suggestion-edit'); + const rangeLabel = $('div.agent-feedback-widget-suggestion-range'); + if (edit.range.startLineNumber === edit.range.endLineNumber) { + rangeLabel.textContent = nls.localize('suggestionLineNumber', "Line {0}", edit.range.startLineNumber); + } else { + rangeLabel.textContent = nls.localize('suggestionLineRange', "Lines {0}-{1}", edit.range.startLineNumber, edit.range.endLineNumber); + } + editNode.appendChild(rangeLabel); + + const newText = $('pre.agent-feedback-widget-suggestion-text'); + newText.textContent = edit.newText; + editNode.appendChild(newText); + suggestionNode.appendChild(editNode); + } + + return suggestionNode; + } + + private _removeComment(comment: ISessionEditorComment): void { + if (comment.source === SessionEditorCommentSource.CodeReview) { + this._codeReviewService.removeComment(this._sessionResource, comment.sourceId); + return; + } + + this._agentFeedbackService.removeFeedback(this._sessionResource, comment.sourceId); + } + + private _convertToAgentFeedback(comment: ISessionEditorComment): void { + if (!comment.canConvertToAgentFeedback) { + return; + } + + const feedback = this._agentFeedbackService.addFeedback(this._sessionResource, comment.resourceUri, comment.range, comment.text, comment.suggestion); + this._agentFeedbackService.setNavigationAnchor(this._sessionResource, toSessionEditorCommentId(SessionEditorCommentSource.AgentFeedback, feedback.id)); + if (comment.source === SessionEditorCommentSource.CodeReview) { + this._codeReviewService.removeComment(this._sessionResource, comment.sourceId); + } + } + /** * Expand the widget body. */ @@ -277,7 +341,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid el.classList.remove('focused'); } - const feedback = this._feedbackItems.find(f => f.id === feedbackId); + const feedback = this._commentItems.find(f => f.id === feedbackId); if (!feedback) { return; } @@ -300,7 +364,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid this._rangeHighlightDecoration.clear(); } - private _highlightRange(feedback: IAgentFeedback): void { + private _highlightRange(feedback: ISessionEditorComment): void { const endLineNumber = feedback.range.endLineNumber; const range = new Range( feedback.range.startLineNumber, 1, @@ -333,7 +397,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid * Returns true if this widget contains the given feedback item (by id). */ containsFeedback(feedbackId: string): boolean { - return this._feedbackItems.some(f => f.id === feedbackId); + return this._commentItems.some(f => f.id === feedbackId); } /** @@ -374,8 +438,8 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid */ toggle(show: boolean): void { this._domNode.classList.toggle('visible', show); - if (show && this._feedbackItems.length > 0) { - this.layout(this._feedbackItems[0].range.startLineNumber); + if (show && this._commentItems.length > 0) { + this.layout(this._commentItems[0].range.startLineNumber); } } @@ -411,6 +475,16 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid this._editor.removeOverlayWidget(this); super.dispose(); } + + private _revealComment(comment: ISessionEditorComment): void { + const range = new Range( + comment.range.startLineNumber, + 1, + comment.range.endLineNumber, + this._editor.getModel()?.getLineMaxColumn(comment.range.endLineNumber) ?? 1, + ); + this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); + } } /** @@ -430,25 +504,20 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + @ICodeReviewService private readonly _codeReviewService: ICodeReviewService, ) { super(); - this._store.add(this._agentFeedbackService.onDidChangeFeedback(e => { - if (this._sessionResource && e.sessionResource.toString() === this._sessionResource.toString()) { - this._rebuildWidgets(); - } - })); - this._store.add(this._agentFeedbackService.onDidChangeNavigation(sessionResource => { if (this._sessionResource && sessionResource.toString() === this._sessionResource.toString()) { this._handleNavigation(); } })); - this._store.add(this._editor.onDidChangeModel(() => { - this._resolveSession(); - this._rebuildWidgets(); - })); + const rebuildSignal = observableSignalFromEvent(this, Event.any( + this._agentFeedbackService.onDidChangeFeedback, + this._editor.onDidChangeModel, + )); this._store.add(Event.any(this._editor.onDidScrollChange, this._editor.onDidLayoutChange)(() => { for (const widget of this._widgets) { @@ -456,8 +525,17 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito } })); - this._resolveSession(); - this._rebuildWidgets(); + this._store.add(autorun(reader => { + rebuildSignal.read(reader); + this._resolveSession(); + if (!this._sessionResource) { + this._clearWidgets(); + return; + } + + this._rebuildWidgets(this._codeReviewService.getReviewState(this._sessionResource).read(reader)); + this._handleNavigation(); + })); } private _resolveSession(): void { @@ -469,10 +547,10 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); } - private _rebuildWidgets(): void { + private _rebuildWidgets(reviewState = this._sessionResource ? this._codeReviewService.getReviewState(this._sessionResource).get() : undefined): void { this._clearWidgets(); - if (!this._sessionResource) { + if (!this._sessionResource || !reviewState) { return; } @@ -481,17 +559,20 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito return; } - const allFeedback = this._agentFeedbackService.getFeedback(this._sessionResource); - // Filter to feedback items belonging to this editor's file - const fileFeedback = allFeedback.filter(f => f.resourceUri.toString() === model.uri.toString()); - if (fileFeedback.length === 0) { + const comments = getSessionEditorComments( + this._sessionResource, + this._agentFeedbackService.getFeedback(this._sessionResource), + reviewState, + ); + const fileComments = getResourceEditorComments(model.uri, comments); + if (fileComments.length === 0) { return; } - const groups = groupNearbyFeedback(fileFeedback, 5); + const groups = groupNearbySessionEditorComments(fileComments, 5); for (const group of groups) { - const widget = new AgentFeedbackEditorWidget(this._editor, group, this._agentFeedbackService, this._sessionResource); + const widget = new AgentFeedbackEditorWidget(this._editor, group, this._sessionResource, this._agentFeedbackService, this._codeReviewService); this._widgets.push(widget); widget.layout(group[0].range.startLineNumber); @@ -503,17 +584,33 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito return; } - const bearing = this._agentFeedbackService.getNavigationBearing(this._sessionResource); + const model = this._editor.getModel(); + if (!model) { + return; + } + + const comments = getSessionEditorComments( + this._sessionResource, + this._agentFeedbackService.getFeedback(this._sessionResource), + this._codeReviewService.getReviewState(this._sessionResource).get(), + ); + const bearing = this._agentFeedbackService.getNavigationBearing(this._sessionResource, comments); if (bearing.activeIdx < 0) { return; } - const allFeedback = this._agentFeedbackService.getFeedback(this._sessionResource); - const activeFeedback = allFeedback[bearing.activeIdx]; + const activeFeedback = comments[bearing.activeIdx]; if (!activeFeedback) { return; } + if (!isEqual(activeFeedback.resourceUri, model.uri)) { + for (const widget of this._widgets) { + widget.collapse(); + } + return; + } + // Expand the widget containing the active feedback, collapse all others for (const widget of this._widgets) { if (widget.containsFeedback(activeFeedback.id)) { diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 550c1d961b1..9d99b64cada 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -16,6 +16,8 @@ import { agentSessionContainsResource, editingEntriesContainResource } from '../ import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js'; // --- Types -------------------------------------------------------------------- @@ -25,6 +27,11 @@ export interface IAgentFeedback { readonly resourceUri: URI; readonly range: IRange; readonly sessionResource: URI; + readonly suggestion?: ICodeReviewSuggestion; +} + +export interface INavigableSessionComment { + readonly id: string; } export interface IAgentFeedbackChangeEvent { @@ -50,7 +57,7 @@ export interface IAgentFeedbackService { /** * Add a feedback item for the given session. */ - addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string): IAgentFeedback; + addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): IAgentFeedback; /** * Remove a single feedback item. @@ -76,11 +83,13 @@ export interface IAgentFeedbackService { * Navigate to next/previous feedback item in a session. */ getNextFeedback(sessionResource: URI, next: boolean): IAgentFeedback | undefined; + getNextNavigableItem(sessionResource: URI, items: readonly T[], next: boolean): T | undefined; + setNavigationAnchor(sessionResource: URI, itemId: string | undefined): void; /** * Get the current navigation bearings for a session. */ - getNavigationBearing(sessionResource: URI): IAgentFeedbackNavigationBearing; + getNavigationBearing(sessionResource: URI, items?: readonly INavigableSessionComment[]): IAgentFeedbackNavigationBearing; /** * Clear all feedback items for a session (e.g., after sending). @@ -91,7 +100,7 @@ export interface IAgentFeedbackService { * Add a feedback item and then submit the feedback. Waits for the * attachment to be updated in the chat widget before submitting. */ - addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string): Promise; + addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): Promise; } // --- Implementation ----------------------------------------------------------- @@ -117,11 +126,12 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe @IEditorService private readonly _editorService: IEditorService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @ICommandService private readonly _commandService: ICommandService, + @ILogService private readonly _logService: ILogService, ) { super(); } - addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string): IAgentFeedback { + addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): IAgentFeedback { const key = sessionResource.toString(); let feedbackItems = this._feedbackBySession.get(key); if (!feedbackItems) { @@ -135,6 +145,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe resourceUri, range, sessionResource, + suggestion, }; // Insert at the correct sorted position. @@ -268,42 +279,52 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe } }); setTimeout(() => { - this._navigationAnchorBySession.set(key, feedbackId); - this._onDidChangeNavigation.fire(sessionResource); + this.setNavigationAnchor(sessionResource, feedbackId); }, 50); // delay to ensure editor has revealed the correct position before firing navigation event } getNextFeedback(sessionResource: URI, next: boolean): IAgentFeedback | undefined { + return this.getNextNavigableItem(sessionResource, this.getFeedback(sessionResource), next); + } + + getNextNavigableItem(sessionResource: URI, items: readonly T[], next: boolean): T | undefined { const key = sessionResource.toString(); - const feedbackItems = this._feedbackBySession.get(key); - if (!feedbackItems?.length) { + if (!items.length) { this._navigationAnchorBySession.delete(key); return undefined; } const anchorId = this._navigationAnchorBySession.get(key); - let anchorIndex = anchorId ? feedbackItems.findIndex(item => item.id === anchorId) : -1; + let anchorIndex = anchorId ? items.findIndex(item => item.id === anchorId) : -1; if (anchorIndex < 0 && !next) { anchorIndex = 0; } const nextIndex = next - ? (anchorIndex + 1) % feedbackItems.length - : (anchorIndex - 1 + feedbackItems.length) % feedbackItems.length; + ? (anchorIndex + 1) % items.length + : (anchorIndex - 1 + items.length) % items.length; - const feedback = feedbackItems[nextIndex]; - this._navigationAnchorBySession.set(key, feedback.id); - this._onDidChangeNavigation.fire(sessionResource); - return feedback; + const item = items[nextIndex]; + this.setNavigationAnchor(sessionResource, item.id); + return item; } - getNavigationBearing(sessionResource: URI): IAgentFeedbackNavigationBearing { + setNavigationAnchor(sessionResource: URI, itemId: string | undefined): void { + const key = sessionResource.toString(); + if (itemId) { + this._navigationAnchorBySession.set(key, itemId); + } else { + this._navigationAnchorBySession.delete(key); + } + this._onDidChangeNavigation.fire(sessionResource); + } + + getNavigationBearing(sessionResource: URI, items: readonly INavigableSessionComment[] = this._feedbackBySession.get(sessionResource.toString()) ?? []): IAgentFeedbackNavigationBearing { const key = sessionResource.toString(); - const feedbackItems = this._feedbackBySession.get(key) ?? []; const anchorId = this._navigationAnchorBySession.get(key); - const activeIdx = anchorId ? feedbackItems.findIndex(item => item.id === anchorId) : -1; - return { activeIdx, totalCount: feedbackItems.length }; + const activeIdx = anchorId ? items.findIndex(item => item.id === anchorId) : -1; + return { activeIdx, totalCount: items.length }; } clearFeedback(sessionResource: URI): void { @@ -315,8 +336,8 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._onDidChangeFeedback.fire({ sessionResource, feedbackItems: [] }); } - async addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string): Promise { - this.addFeedback(sessionResource, resourceUri, range, text); + async addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): Promise { + this.addFeedback(sessionResource, resourceUri, range, text, suggestion); // Wait for the attachment contribution to update the chat widget's attachment model const widget = this._chatWidgetService.getWidgetBySessionResource(sessionResource); @@ -330,10 +351,14 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe ); } } else { - // This should not normally happen, but if the widget isn't found, wait a bit to give it a chance to initialize before submitting. + this._logService.error('[AgentFeedback] addFeedbackAndSubmit: no chat widget found for session, feedback may not be submitted correctly', sessionResource.toString()); await new Promise(resolve => setTimeout(resolve, 100)); } - await this._commandService.executeCommand('agentFeedbackEditor.action.submit'); + try { + await this._commandService.executeCommand('agentFeedbackEditor.action.submit'); + } catch (err) { + this._logService.error('[AgentFeedback] Failed to execute submit feedback command', err); + } } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css index 34725117ebe..8eed23bc26c 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -152,6 +152,7 @@ border-bottom: 1px solid var(--vscode-editorWidget-border, var(--vscode-widget-border)); cursor: pointer; position: relative; + gap: 6px; } .agent-feedback-widget-item:last-child { @@ -167,12 +168,61 @@ color: var(--vscode-list-activeSelectionForeground); } +.agent-feedback-widget-item-codeReview { + box-shadow: inset 2px 0 0 var(--vscode-editorWarning-foreground); +} + +.agent-feedback-widget-item-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; +} + +.agent-feedback-widget-item-meta { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + flex-wrap: wrap; +} + +.agent-feedback-widget-item-actions { + margin-left: auto; + flex: 0 0 auto; + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.agent-feedback-widget-item:hover .agent-feedback-widget-item-actions { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.agent-feedback-widget-item-type { + display: inline-flex; + align-items: center; + padding: 1px 6px; + border-radius: 999px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.2px; + background: color-mix(in srgb, var(--vscode-editorWidget-border, var(--vscode-widget-border)) 25%, transparent); + color: var(--vscode-descriptionForeground); +} + +.agent-feedback-widget-item-codeReview .agent-feedback-widget-item-type { + background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 22%, transparent); + color: var(--vscode-editorWarning-foreground); +} + /* Line info */ .agent-feedback-widget-line-info { font-size: 10px; font-weight: 600; color: var(--vscode-descriptionForeground); - margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; } @@ -183,6 +233,46 @@ word-wrap: break-word; } +.agent-feedback-widget-suggestion { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px; + border-radius: 6px; + background: color-mix(in srgb, var(--vscode-editorWidget-border, var(--vscode-widget-border)) 12%, transparent); +} + +.agent-feedback-widget-item-codeReview .agent-feedback-widget-suggestion { + background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 10%, transparent); +} + +.agent-feedback-widget-suggestion-title, +.agent-feedback-widget-suggestion-range { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--vscode-descriptionForeground); +} + +.agent-feedback-widget-suggestion-edit { + display: flex; + flex-direction: column; + gap: 4px; +} + +.agent-feedback-widget-suggestion-text { + margin: 0; + padding: 6px 8px; + border-radius: 4px; + overflow-x: auto; + white-space: pre-wrap; + font-family: monospace; + font-size: 11px; + line-height: 1.45; + background: color-mix(in srgb, var(--vscode-editor-background) 65%, transparent); +} + /* Gutter decoration for range indicator on hover */ .agent-feedback-widget-range-glyph { margin-left: 8px; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts b/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts new file mode 100644 index 00000000000..f5e740cbd98 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange, Range } from '../../../../editor/common/core/range.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IAgentFeedback } from './agentFeedbackService.js'; +import { CodeReviewStateKind, ICodeReviewComment, ICodeReviewState, ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js'; + +export const enum SessionEditorCommentSource { + AgentFeedback = 'agentFeedback', + CodeReview = 'codeReview', +} + +export interface ISessionEditorComment { + readonly id: string; + readonly sourceId: string; + readonly source: SessionEditorCommentSource; + readonly sessionResource: URI; + readonly resourceUri: URI; + readonly range: IRange; + readonly text: string; + readonly suggestion?: ICodeReviewSuggestion; + readonly severity?: string; + readonly canConvertToAgentFeedback: boolean; +} + +export function getCodeReviewComments(reviewState: ICodeReviewState): readonly ICodeReviewComment[] { + return reviewState.kind === CodeReviewStateKind.Result ? reviewState.comments : []; +} + +export function getSessionEditorComments( + sessionResource: URI, + agentFeedbackItems: readonly IAgentFeedback[], + reviewState: ICodeReviewState, +): readonly ISessionEditorComment[] { + const comments: ISessionEditorComment[] = []; + + for (const item of agentFeedbackItems) { + comments.push({ + id: toSessionEditorCommentId(SessionEditorCommentSource.AgentFeedback, item.id), + sourceId: item.id, + source: SessionEditorCommentSource.AgentFeedback, + sessionResource, + resourceUri: item.resourceUri, + range: item.range, + text: item.text, + suggestion: item.suggestion, + canConvertToAgentFeedback: false, + }); + } + + for (const item of getCodeReviewComments(reviewState)) { + comments.push({ + id: toSessionEditorCommentId(SessionEditorCommentSource.CodeReview, item.id), + sourceId: item.id, + source: SessionEditorCommentSource.CodeReview, + sessionResource, + resourceUri: item.uri, + range: item.range, + text: item.body, + suggestion: item.suggestion, + severity: item.severity, + canConvertToAgentFeedback: true, + }); + } + + comments.sort(compareSessionEditorComments); + return comments; +} + +export function compareSessionEditorComments(a: ISessionEditorComment, b: ISessionEditorComment): number { + return a.resourceUri.toString().localeCompare(b.resourceUri.toString()) + || Range.compareRangesUsingStarts(Range.lift(a.range), Range.lift(b.range)) + || a.source.localeCompare(b.source) + || a.sourceId.localeCompare(b.sourceId); +} + +export function groupNearbySessionEditorComments(items: readonly ISessionEditorComment[], lineThreshold: number = 5): ISessionEditorComment[][] { + if (items.length === 0) { + return []; + } + + const sorted = [...items].sort(compareSessionEditorComments); + const groups: ISessionEditorComment[][] = []; + let currentGroup: ISessionEditorComment[] = [sorted[0]]; + + for (let i = 1; i < sorted.length; i++) { + const firstItem = currentGroup[0]; + const currentItem = sorted[i]; + + const sameResource = currentItem.resourceUri.toString() === firstItem.resourceUri.toString(); + const verticalSpan = currentItem.range.startLineNumber - firstItem.range.startLineNumber; + + if (sameResource && verticalSpan <= lineThreshold) { + currentGroup.push(currentItem); + } else { + groups.push(currentGroup); + currentGroup = [currentItem]; + } + } + + groups.push(currentGroup); + return groups; +} + +export function getResourceEditorComments(resourceUri: URI, comments: readonly ISessionEditorComment[]): readonly ISessionEditorComment[] { + const resource = resourceUri.toString(); + return comments.filter(comment => comment.resourceUri.toString() === resource); +} + +export function toSessionEditorCommentId(source: SessionEditorCommentSource, sourceId: string): string { + return `${source}:${sourceId}`; +} + +export function hasAgentFeedbackComments(comments: readonly ISessionEditorComment[]): boolean { + return comments.some(comment => comment.source === SessionEditorCommentSource.AgentFeedback); +} diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlayWidget.fixture.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlayWidget.fixture.ts new file mode 100644 index 00000000000..2314f52bc38 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorOverlayWidget.fixture.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { toAction } from '../../../../../base/common/actions.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { AgentFeedbackOverlayWidget } from '../../browser/agentFeedbackEditorOverlay.js'; +import { clearAllFeedbackActionId, navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from '../../browser/agentFeedbackEditorActions.js'; + +interface INavigationBearings { + readonly activeIdx: number; + readonly totalCount: number; +} + +interface IFixtureOptions { + readonly navigationBearings: INavigationBearings; + readonly hasAgentFeedbackActions?: boolean; +} + +class FixtureMenuService implements IMenuService { + constructor(private readonly _hasAgentFeedbackActions: boolean) { + } + + declare readonly _serviceBrand: undefined; + + createMenu(_id: MenuId): IMenu { + const navigateActions = [ + toAction({ id: navigationBearingFakeActionId, label: 'Navigation Status', run: () => { } }), + toAction({ id: navigatePreviousFeedbackActionId, label: 'Previous', class: 'codicon codicon-arrow-up', run: () => { } }), + toAction({ id: navigateNextFeedbackActionId, label: 'Next', class: 'codicon codicon-arrow-down', run: () => { } }), + ] as unknown as (MenuItemAction | SubmenuItemAction)[]; + + const submitActions = this._hasAgentFeedbackActions + ? [ + toAction({ id: submitFeedbackActionId, label: 'Submit', class: 'codicon codicon-send', run: () => { } }), + toAction({ id: clearAllFeedbackActionId, label: 'Clear', class: 'codicon codicon-clear-all', run: () => { } }), + ] as unknown as (MenuItemAction | SubmenuItemAction)[] + : []; + + return { + onDidChange: Event.None, + dispose: () => { }, + getActions: () => submitActions.length > 0 + ? [ + ['navigate', navigateActions], + ['a_submit', submitActions], + ] + : [ + ['navigate', navigateActions], + ], + }; + } + + getMenuActions(_id: MenuId, _contextKeyService: unknown, _options?: IMenuActionOptions) { return []; } + getMenuContexts() { return new Set(); } + resetHiddenStates() { } +} + +function renderWidget(context: ComponentFixtureContext, options: IFixtureOptions): void { + const scopedDisposables = context.disposableStore.add(new DisposableStore()); + context.container.classList.add('monaco-workbench'); + context.container.style.width = '420px'; + context.container.style.height = '64px'; + context.container.style.padding = '12px'; + context.container.style.background = 'var(--vscode-editor-background)'; + + const instantiationService = createEditorServices(scopedDisposables, { + colorTheme: context.theme, + additionalServices: reg => { + reg.defineInstance(IMenuService, new FixtureMenuService(options.hasAgentFeedbackActions ?? true)); + registerWorkbenchServices(reg); + }, + }); + + const widget = scopedDisposables.add(instantiationService.createInstance(AgentFeedbackOverlayWidget)); + widget.show(options.navigationBearings); + context.container.appendChild(widget.getDomNode()); +} + +export default defineThemedFixtureGroup({ path: 'sessions/agentFeedback/' }, { + ZeroOfZero: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: -1, totalCount: 0 }, + hasAgentFeedbackActions: false, + }), + }), + + SingleFeedback: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: 0, totalCount: 1 }, + }), + }), + + FirstOfThree: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: -1, totalCount: 3 }, + }), + }), + + ReviewOnlyTwoComments: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: 0, totalCount: 2 }, + hasAgentFeedbackActions: false, + }), + }), + + MiddleOfThree: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: 1, totalCount: 3 }, + }), + }), + + MixedFourComments: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: 2, totalCount: 4 }, + hasAgentFeedbackActions: true, + }), + }), + + LastOfThree: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + navigationBearings: { activeIdx: 2, totalCount: 3 }, + }), + }), +}); diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts new file mode 100644 index 00000000000..818c87c9a36 --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts @@ -0,0 +1,346 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../../base/common/event.js'; +import { Color } from '../../../../../base/common/color.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IRange } from '../../../../../editor/common/core/range.js'; +import { TokenizationRegistry } from '../../../../../editor/common/languages.js'; +import { IAgentFeedback, IAgentFeedbackService } from '../../browser/agentFeedbackService.js'; +import { AgentFeedbackEditorWidget } from '../../browser/agentFeedbackEditorWidgetContribution.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { CodeReviewStateKind, ICodeReviewService, ICodeReviewState, ICodeReviewSuggestion } from '../../../codeReview/browser/codeReviewService.js'; +import { ISessionEditorComment, SessionEditorCommentSource } from '../../browser/sessionEditorComments.js'; + +const sessionResource = URI.parse('vscode-agent-session://fixture/session-1'); +const fileResource = URI.parse('inmemory://model/agent-feedback-widget.ts'); + +const sampleCode = [ + 'function alpha() {', + '\tconst first = 1;', + '\treturn first;', + '}', + '', + 'function beta() {', + '\tconst second = 2;', + '\tconst third = second + 1;', + '\treturn third;', + '}', + '', + 'function gamma() {', + '\tconst done = true;', + '\treturn done;', + '}', +].join('\n'); + +interface IFixtureOptions { + readonly expanded?: boolean; + readonly focusedCommentId?: string; + readonly hidden?: boolean; + readonly commentItems: readonly ISessionEditorComment[]; +} + +function createRange(startLineNumber: number, endLineNumber: number = startLineNumber): IRange { + return { + startLineNumber, + startColumn: 1, + endLineNumber, + endColumn: 1, + }; +} + +function createFeedbackComment(id: string, text: string, startLineNumber: number, endLineNumber: number = startLineNumber, suggestion?: ICodeReviewSuggestion): ISessionEditorComment { + return { + id: `agentFeedback:${id}`, + sourceId: id, + source: SessionEditorCommentSource.AgentFeedback, + sessionResource, + resourceUri: fileResource, + range: createRange(startLineNumber, endLineNumber), + text, + suggestion, + canConvertToAgentFeedback: false, + }; +} + +function createReviewComment(id: string, text: string, startLineNumber: number, endLineNumber: number = startLineNumber, suggestion?: ICodeReviewSuggestion): ISessionEditorComment { + const range: IRange = { + startLineNumber, + startColumn: 1, + endLineNumber, + endColumn: 1, + }; + + return { + id: `codeReview:${id}`, + sourceId: id, + source: SessionEditorCommentSource.CodeReview, + text, + resourceUri: fileResource, + range, + sessionResource, + suggestion, + severity: 'warning', + canConvertToAgentFeedback: true, + }; +} + +function createMockAgentFeedbackService(): IAgentFeedbackService { + return new class extends mock() { + override readonly onDidChangeFeedback = Event.None; + override readonly onDidChangeNavigation = Event.None; + + override addFeedback(): IAgentFeedback { + throw new Error('Not implemented for fixture'); + } + + override removeFeedback(): void { } + + override getFeedback(): readonly IAgentFeedback[] { + return []; + } + + override getMostRecentSessionForResource(): URI | undefined { + return undefined; + } + + override async revealFeedback(): Promise { } + + override getNextFeedback(): IAgentFeedback | undefined { + return undefined; + } + + override getNavigationBearing() { + return { activeIdx: -1, totalCount: 0 }; + } + + override getNextNavigableItem() { + return undefined; + } + + override setNavigationAnchor(): void { } + + override clearFeedback(): void { } + + override async addFeedbackAndSubmit(): Promise { } + }(); +} + +function createMockCodeReviewService(): ICodeReviewService { + return new class extends mock() { + private readonly _state = observableValue('fixture.reviewState', { kind: CodeReviewStateKind.Idle }); + + override getReviewState() { + return this._state; + } + + override hasReview(): boolean { + return false; + } + + override requestReview(): void { } + + override removeComment(): void { } + + override dismissReview(): void { } + }(); +} + +function ensureTokenColorMap(): void { + if (TokenizationRegistry.getColorMap()?.length) { + return; + } + + const colorMap = [ + Color.fromHex('#000000'), + Color.fromHex('#d4d4d4'), + Color.fromHex('#9cdcfe'), + Color.fromHex('#ce9178'), + Color.fromHex('#b5cea8'), + Color.fromHex('#4fc1ff'), + Color.fromHex('#c586c0'), + Color.fromHex('#569cd6'), + Color.fromHex('#dcdcaa'), + Color.fromHex('#f44747'), + ]; + + TokenizationRegistry.setColorMap(colorMap); +} + +function renderWidget(context: ComponentFixtureContext, options: IFixtureOptions): void { + const scopedDisposables = context.disposableStore.add(new DisposableStore()); + context.container.style.width = '760px'; + context.container.style.height = '420px'; + context.container.style.border = '1px solid var(--vscode-editorWidget-border)'; + context.container.style.background = 'var(--vscode-editor-background)'; + + ensureTokenColorMap(); + + const instantiationService = createEditorServices(scopedDisposables, { colorTheme: context.theme }); + const model = scopedDisposables.add(createTextModel(instantiationService, sampleCode, fileResource, 'typescript')); + + const editorOptions: ICodeEditorWidgetOptions = { + contributions: [], + }; + + const editor = scopedDisposables.add(instantiationService.createInstance( + CodeEditorWidget, + context.container, + { + automaticLayout: true, + lineNumbers: 'on', + minimap: { enabled: false }, + scrollBeyondLastLine: false, + fontSize: 13, + lineHeight: 20, + }, + editorOptions + )); + + editor.setModel(model); + + const widget = scopedDisposables.add(new AgentFeedbackEditorWidget( + editor, + options.commentItems, + sessionResource, + createMockAgentFeedbackService(), + createMockCodeReviewService(), + )); + + widget.layout(options.commentItems[0].range.startLineNumber); + + if (options.expanded) { + widget.expand(); + } + + if (options.focusedCommentId) { + widget.focusFeedback(options.focusedCommentId); + } + + if (options.hidden) { + const domNode = widget.getDomNode(); + domNode.style.transition = 'none'; + domNode.style.animation = 'none'; + widget.toggle(false); + } +} + +const singleFeedback = [ + createFeedbackComment('f-1', 'Prefer a clearer variable name on this line.', 2), +]; + +const groupedFeedback = [ + createFeedbackComment('f-1', 'Prefer a clearer variable name on this line.', 2), + createFeedbackComment('f-2', 'This return statement can be simplified.', 3), + createFeedbackComment('f-3', 'Consider documenting why this branch is needed.', 6, 8), +]; + +const reviewOnly = [ + createReviewComment('r-1', 'Handle the null case before returning here.', 7), + createReviewComment('r-2', 'This branch needs a stronger explanation.', 8), +]; + +const mixedComments = [ + createFeedbackComment('f-1', 'Prefer a clearer variable name on this line.', 2), + createReviewComment('r-1', 'This should be extracted into a helper.', 3), + createFeedbackComment('f-2', 'Consider renaming this for readability.', 4), +]; + +const reviewSuggestion: ICodeReviewSuggestion = { + edits: [ + { range: createRange(8), oldText: '\tconst third = second + 1;', newText: '\tconst third = second + computeOffset();' }, + ], +}; + +const suggestionMix = [ + createReviewComment('r-3', 'Prefer using the helper so the intent is explicit.', 8, 8, reviewSuggestion), + createFeedbackComment('f-3', 'Keep the helper name aligned with the domain concept.', 9), +]; + +export default defineThemedFixtureGroup({ path: 'sessions/agentFeedback/' }, { + CollapsedSingleComment: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: singleFeedback, + }), + }), + + ExpandedSingleComment: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: singleFeedback, + expanded: true, + }), + }), + + CollapsedMultiComment: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: groupedFeedback, + }), + }), + + ExpandedMultiComment: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: groupedFeedback, + expanded: true, + }), + }), + + ExpandedFocusedFeedback: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: groupedFeedback, + expanded: true, + focusedCommentId: 'agentFeedback:f-2', + }), + }), + + ExpandedReviewOnly: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: reviewOnly, + expanded: true, + }), + }), + + ExpandedMixedComments: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: mixedComments, + expanded: true, + }), + }), + + ExpandedFocusedReviewComment: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: mixedComments, + expanded: true, + focusedCommentId: 'codeReview:r-1', + }), + }), + + ExpandedReviewSuggestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: suggestionMix, + expanded: true, + }), + }), + + HiddenWidget: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: mixedComments, + hidden: true, + }), + }), +}); diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts new file mode 100644 index 00000000000..dec6e0ce3aa --- /dev/null +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { CodeReviewStateKind, ICodeReviewState } from '../../../codeReview/browser/codeReviewService.js'; +import { getResourceEditorComments, getSessionEditorComments, groupNearbySessionEditorComments, hasAgentFeedbackComments, SessionEditorCommentSource } from '../../browser/sessionEditorComments.js'; + +type ICodeReviewResultState = Extract; + +suite('SessionEditorComments', () => { + const session = URI.parse('test://session/1'); + const fileA = URI.parse('file:///a.ts'); + const fileB = URI.parse('file:///b.ts'); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function reviewState(comments: ICodeReviewResultState['comments']): ICodeReviewState { + return { + kind: CodeReviewStateKind.Result, + version: 'v1', + comments, + }; + } + + test('merges and sorts feedback and review comments by resource and range', () => { + const comments = getSessionEditorComments(session, [ + { id: 'feedback-b', text: 'feedback b', resourceUri: fileB, range: new Range(8, 1, 8, 1), sessionResource: session }, + { id: 'feedback-a', text: 'feedback a', resourceUri: fileA, range: new Range(12, 1, 12, 1), sessionResource: session }, + ], reviewState([ + { id: 'review-a', uri: fileA, range: new Range(3, 1, 3, 1), body: 'review a', kind: 'issue', severity: 'warning' }, + { id: 'review-b', uri: fileB, range: new Range(2, 1, 2, 1), body: 'review b', kind: 'issue', severity: 'warning' }, + ])); + + assert.deepStrictEqual(comments.map(comment => `${comment.resourceUri.path}:${comment.range.startLineNumber}:${comment.source}`), [ + '/a.ts:3:codeReview', + '/a.ts:12:agentFeedback', + '/b.ts:2:codeReview', + '/b.ts:8:agentFeedback', + ]); + }); + + test('groups nearby comments only within the same resource', () => { + const comments = getSessionEditorComments(session, [ + { id: 'feedback-a', text: 'feedback a', resourceUri: fileA, range: new Range(10, 1, 10, 1), sessionResource: session }, + ], reviewState([ + { id: 'review-a', uri: fileA, range: new Range(13, 1, 13, 1), body: 'review a', kind: 'issue', severity: 'warning' }, + { id: 'review-b', uri: fileB, range: new Range(11, 1, 11, 1), body: 'review b', kind: 'issue', severity: 'warning' }, + ])); + + const groups = groupNearbySessionEditorComments(comments, 5); + assert.strictEqual(groups.length, 2); + assert.deepStrictEqual(groups[0].map(comment => `${comment.resourceUri.path}:${comment.range.startLineNumber}:${comment.source}`), [ + '/a.ts:10:agentFeedback', + '/a.ts:13:codeReview', + ]); + assert.deepStrictEqual(groups[1].map(comment => `${comment.resourceUri.path}:${comment.range.startLineNumber}:${comment.source}`), [ + '/b.ts:11:codeReview', + ]); + }); + + test('preserves review suggestion metadata and capability flags', () => { + const comments = getSessionEditorComments(session, [], reviewState([ + { + id: 'review-suggestion', + uri: fileA, + range: new Range(7, 1, 7, 1), + body: 'prefer a constant', + kind: 'suggestion', + severity: 'info', + suggestion: { + edits: [{ range: new Range(7, 1, 7, 10), oldText: 'let value', newText: 'const value' }], + }, + }, + ])); + + assert.strictEqual(comments.length, 1); + assert.strictEqual(comments[0].source, SessionEditorCommentSource.CodeReview); + assert.strictEqual(comments[0].canConvertToAgentFeedback, true); + assert.strictEqual(comments[0].suggestion?.edits[0].newText, 'const value'); + }); + + test('filters resource comments and detects authored feedback presence', () => { + const comments = getSessionEditorComments(session, [ + { id: 'feedback-a', text: 'feedback a', resourceUri: fileA, range: new Range(1, 1, 1, 1), sessionResource: session }, + ], reviewState([ + { id: 'review-b', uri: fileB, range: new Range(2, 1, 2, 1), body: 'review b', kind: 'issue', severity: 'warning' }, + ])); + + assert.strictEqual(hasAgentFeedbackComments(comments), true); + assert.deepStrictEqual(getResourceEditorComments(fileA, comments).map(comment => comment.source), [SessionEditorCommentSource.AgentFeedback]); + assert.deepStrictEqual(getResourceEditorComments(fileB, comments).map(comment => comment.source), [SessionEditorCommentSource.CodeReview]); + }); +}); diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts index 7e85b4dd447..e68286ee17c 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts @@ -27,9 +27,10 @@ import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/ import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IPromptsService, PromptsStorage, IAgentSkill, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { agentIcon, extensionIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon, userIcon, workspaceIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, extensionIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon, userIcon, workspaceIcon, builtinIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { AICustomizationItemMenuId } from './aiCustomizationTreeView.js'; import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { AICustomizationPromptsStorage, BUILTIN_STORAGE } from '../../chat/common/builtinPromptsStorage.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js'; @@ -80,7 +81,7 @@ interface IAICustomizationGroupItem { readonly type: 'group'; readonly id: string; readonly label: string; - readonly storage: PromptsStorage; + readonly storage: AICustomizationPromptsStorage; readonly promptType: PromptsType; readonly icon: ThemeIcon; } @@ -94,7 +95,7 @@ interface IAICustomizationFileItem { readonly uri: URI; readonly name: string; readonly description?: string; - readonly storage: PromptsStorage; + readonly storage: AICustomizationPromptsStorage; readonly promptType: PromptsType; } @@ -234,7 +235,7 @@ class AICustomizationFileRenderer implements ITreeRenderer; + files?: Map; } /** @@ -375,11 +376,13 @@ class UnifiedAICustomizationDataSource implements IAsyncDataSource item.storage === PromptsStorage.local); const userItems = allItems.filter(item => item.storage === PromptsStorage.user); const extensionItems = allItems.filter(item => item.storage === PromptsStorage.extension); + const builtinItems = allItems.filter(item => item.storage === BUILTIN_STORAGE); - cached.files = new Map([ + cached.files = new Map([ [PromptsStorage.local, workspaceItems], [PromptsStorage.user, userItems], [PromptsStorage.extension, extensionItems], + [BUILTIN_STORAGE, builtinItems], ]); const itemCount = allItems.length; @@ -390,6 +393,7 @@ class UnifiedAICustomizationDataSource implements IAsyncDataSource 0) { groups.push(this.createGroupItem(promptType, PromptsStorage.local, workspaceItems.length)); @@ -400,6 +404,9 @@ class UnifiedAICustomizationDataSource implements IAsyncDataSource 0) { groups.push(this.createGroupItem(promptType, PromptsStorage.extension, extensionItems.length)); } + if (builtinItems.length > 0) { + groups.push(this.createGroupItem(promptType, BUILTIN_STORAGE, builtinItems.length)); + } return groups; } @@ -407,26 +414,29 @@ class UnifiedAICustomizationDataSource implements IAsyncDataSource = { + private createGroupItem(promptType: PromptsType, storage: AICustomizationPromptsStorage, count: number): IAICustomizationGroupItem { + const storageLabels: Record = { [PromptsStorage.local]: localize('workspaceWithCount', "Workspace ({0})", count), [PromptsStorage.user]: localize('userWithCount', "User ({0})", count), [PromptsStorage.extension]: localize('extensionsWithCount', "Extensions ({0})", count), [PromptsStorage.plugin]: localize('pluginsWithCount', "Plugins ({0})", count), + [BUILTIN_STORAGE]: localize('builtinWithCount', "Built-in ({0})", count), }; - const storageIcons: Record = { + const storageIcons: Record = { [PromptsStorage.local]: workspaceIcon, [PromptsStorage.user]: userIcon, [PromptsStorage.extension]: extensionIcon, [PromptsStorage.plugin]: pluginIcon, + [BUILTIN_STORAGE]: builtinIcon, }; - const storageSuffixes: Record = { + const storageSuffixes: Record = { [PromptsStorage.local]: 'workspace', [PromptsStorage.user]: 'user', [PromptsStorage.extension]: 'extensions', [PromptsStorage.plugin]: 'plugins', + [BUILTIN_STORAGE]: 'builtin', }; return { @@ -443,7 +453,7 @@ class UnifiedAICustomizationDataSource implements IAsyncDataSource { + private async getFilesForStorageAndType(storage: AICustomizationPromptsStorage, promptType: PromptsType): Promise { const cached = this.cache.get(promptType); // For skills, use the cached skills data @@ -602,7 +612,7 @@ export class AICustomizationViewPane extends ViewPane { this.treeDisposables.add(this.tree.onDidOpen(async e => { if (e.element && e.element.type === 'file') { this.editorService.openEditor({ - resource: e.element.uri + resource: e.element.uri, }); } else if (e.element && e.element.type === 'link') { const input = AICustomizationManagementEditorInput.getOrCreate(); diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 1ef5586817f..da1e460a297 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -47,7 +47,7 @@ import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { chatEditingWidgetFileStateContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; import { createFileIconThemableTreeContainerScope } from '../../../../workbench/contrib/files/browser/views/explorerView.js'; @@ -57,6 +57,7 @@ import { IExtensionService } from '../../../../workbench/services/extensions/com import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; +import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; const $ = dom.$; @@ -64,6 +65,7 @@ const $ = dom.$; export const CHANGES_VIEW_CONTAINER_ID = 'workbench.view.agentSessions.changesContainer'; export const CHANGES_VIEW_ID = 'workbench.view.agentSessions.changes'; +const RUN_SESSION_CODE_REVIEW_ACTION_ID = 'sessions.codeReview.run'; // --- View Mode @@ -87,6 +89,7 @@ interface IChangesFileItem { readonly changeType: ChangeType; readonly linesAdded: number; readonly linesRemoved: number; + readonly reviewCommentCount: number; } interface IChangesFolderItem { @@ -253,6 +256,7 @@ export class ChangesViewPane extends ViewPane { @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, @ILabelService private readonly labelService: ILabelService, @IStorageService private readonly storageService: IStorageService, + @ICodeReviewService private readonly codeReviewService: ICodeReviewService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -449,6 +453,7 @@ export class ChangesViewPane extends ViewPane { changeType: isDeletion ? 'deleted' : 'modified', linesAdded, linesRemoved, + reviewCommentCount: 0, }); } @@ -474,25 +479,54 @@ export class ChangesViewPane extends ViewPane { return model?.changes instanceof Array ? model.changes : Iterable.empty(); }); + const reviewCommentCountByFileObs = derived(reader => { + const sessionResource = activeSessionResource.read(reader); + const sessionChanges = [...sessionFileChangesObs.read(reader)]; + + if (!sessionResource || sessionChanges.length === 0) { + return new Map(); + } + + const reviewFiles = getCodeReviewFilesFromSessionChanges(sessionChanges as readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]); + const reviewVersion = getCodeReviewVersion(reviewFiles); + const reviewState = this.codeReviewService.getReviewState(sessionResource).read(reader); + + if (reviewState.kind !== CodeReviewStateKind.Result || reviewState.version !== reviewVersion) { + return new Map(); + } + + const result = new Map(); + for (const comment of reviewState.comments) { + const uriKey = comment.uri.toString(); + result.set(uriKey, (result.get(uriKey) ?? 0) + 1); + } + + return result; + }); + // Convert session file changes to list items (cloud/background sessions) - const sessionFilesObs = derived(reader => - [...sessionFileChangesObs.read(reader)].map((entry): IChangesFileItem => { + const sessionFilesObs = derived(reader => { + const reviewCommentCountByFile = reviewCommentCountByFileObs.read(reader); + + return [...sessionFileChangesObs.read(reader)].map((entry): IChangesFileItem => { const isDeletion = entry.modifiedUri === undefined; const isAddition = entry.originalUri === undefined; + const uri = isIChatSessionFileChange2(entry) + ? entry.modifiedUri ?? entry.uri + : entry.modifiedUri; return { type: 'file', - uri: isIChatSessionFileChange2(entry) - ? entry.modifiedUri ?? entry.uri - : entry.modifiedUri, + uri, originalUri: entry.originalUri, state: ModifiedFileEntryState.Accepted, isDeletion, changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', linesAdded: entry.insertions, linesRemoved: entry.deletions, + reviewCommentCount: reviewCommentCountByFile.get(uri.toString()) ?? 0, }; - }) - ); + }); + }); // Combine both entry sources for display const combinedEntriesObs = derived(reader => { @@ -596,6 +630,9 @@ export class ChangesViewPane extends ViewPane { ); return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } + if (action.id === RUN_SESSION_CODE_REVIEW_ACTION_ID) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; } @@ -859,6 +896,7 @@ interface IChangesTreeTemplate { readonly templateDisposables: DisposableStore; readonly toolbar: MenuWorkbenchToolBar | undefined; readonly contextKeyService: IContextKeyService | undefined; + readonly reviewCommentsBadge: HTMLElement; readonly decorationBadge: HTMLElement; readonly addedSpan: HTMLElement; readonly removedSpan: HTMLElement; @@ -881,6 +919,9 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { @@ -933,6 +974,7 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer 0) { + templateData.reviewCommentsBadge.style.display = ''; + templateData.reviewCommentsBadge.className = 'changes-review-comments-badge'; + templateData.reviewCommentsBadge.replaceChildren( + dom.$('.codicon.codicon-comment-unresolved'), + dom.$('span', undefined, `${data.reviewCommentCount}`) + ); + } else { + templateData.reviewCommentsBadge.style.display = 'none'; + templateData.reviewCommentsBadge.replaceChildren(); + } + // Update decoration badge (A/M/D) const badge = templateData.decorationBadge; badge.className = 'changes-decoration-badge'; @@ -996,6 +1050,7 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer i.kind === ActionListItemKind.Action).length > FILTER_THRESHOLD; @@ -285,7 +283,7 @@ export class FolderPicker extends Disposable { } dom.clearNode(trigger); - const folderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + const folderUri = this._selectedFolderUri; const label = folderUri ? basename(folderUri) : localize('pickFolder', "Pick Folder"); dom.append(trigger, renderIcon(Codicon.folder)); diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 50815341dbb..b033d523652 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -669,7 +669,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._focusEditor(); }, getModels: () => this._getAvailableModels(), - canManageModels: () => false, + canManageModels: () => true, }; const pickerOptions: IChatInputPickerOptions = { diff --git a/src/vs/sessions/contrib/chat/browser/promptsService.ts b/src/vs/sessions/contrib/chat/browser/promptsService.ts index f226fd20410..45db75ebadd 100644 --- a/src/vs/sessions/contrib/chat/browser/promptsService.ts +++ b/src/vs/sessions/contrib/chat/browser/promptsService.ts @@ -8,21 +8,28 @@ import { PromptFilesLocator } from '../../../../workbench/contrib/chat/common/pr import { Event } from '../../../../base/common/event.js'; import { basename, isEqualOrParent, joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { FileAccess } from '../../../../base/common/network.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; -import { HOOKS_SOURCE_FOLDER } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js'; +import { HOOKS_SOURCE_FOLDER, getCleanPromptName } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { IPromptPath, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { BUILTIN_STORAGE, IBuiltinPromptPath } from '../../chat/common/builtinPromptsStorage.js'; import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { ISearchService } from '../../../../workbench/services/search/common/search.js'; import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +/** URI root for built-in prompts bundled with the Sessions app. */ +export const BUILTIN_PROMPTS_URI = FileAccess.asFileUri('vs/sessions/prompts'); + export class AgenticPromptsService extends PromptsService { private _copilotRoot: URI | undefined; + private _builtinPromptsCache: Map> | undefined; protected override createPromptFilesLocator(): PromptFilesLocator { return this.instantiationService.createInstance(AgenticPromptFilesLocator); @@ -36,6 +43,76 @@ export class AgenticPromptsService extends PromptsService { return this._copilotRoot; } + /** + * Returns built-in prompt files bundled with the Sessions app. + */ + private async getBuiltinPromptFiles(type: PromptsType): Promise { + if (type !== PromptsType.prompt) { + return []; + } + + if (!this._builtinPromptsCache) { + this._builtinPromptsCache = new Map(); + } + + let cached = this._builtinPromptsCache.get(type); + if (!cached) { + cached = this.discoverBuiltinPrompts(type); + this._builtinPromptsCache.set(type, cached); + } + return cached; + } + + private async discoverBuiltinPrompts(type: PromptsType): Promise { + const fileService = this.instantiationService.invokeFunction(accessor => accessor.get(IFileService)); + const promptsDir = FileAccess.asFileUri('vs/sessions/prompts'); + try { + const stat = await fileService.resolve(promptsDir); + if (!stat.children) { + return []; + } + return stat.children + .filter(child => !child.isDirectory && child.name.endsWith('.prompt.md')) + .map(child => ({ uri: child.resource, storage: BUILTIN_STORAGE, type })); + } catch { + return []; + } + } + + /** + * Override to include built-in prompts and filter out those overridden + * by user or workspace prompts with the same name. + */ + public override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise { + const baseResults = await super.listPromptFiles(type, token); + const builtinPrompts = await this.getBuiltinPromptFiles(type); + if (builtinPrompts.length === 0) { + return baseResults; + } + + // Collect names of user/workspace prompts to detect overrides + const overriddenNames = new Set(); + for (const p of baseResults) { + if (p.storage === PromptsStorage.local || p.storage === PromptsStorage.user) { + overriddenNames.add(getCleanPromptName(p.uri)); + } + } + + const nonOverridden = builtinPrompts.filter( + p => !overriddenNames.has(getCleanPromptName(p.uri)) + ); + // Built-in items use BUILTIN_STORAGE ('builtin') which is not in the + // core IPromptPath union but is handled by the sessions UI layer. + return [...baseResults, ...nonOverridden] as readonly IPromptPath[]; + } + + public override async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { + if (storage === BUILTIN_STORAGE) { + return this.getBuiltinPromptFiles(type) as Promise; + } + return super.listPromptFilesForStorage(type, storage, token); + } + /** * Override to use ~/.copilot as the user-level source folder for creation, * instead of the VS Code profile's promptsHome. diff --git a/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts b/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts new file mode 100644 index 00000000000..fb3efadd52f --- /dev/null +++ b/src/vs/sessions/contrib/chat/common/builtinPromptsStorage.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; + +/** + * Extended storage type for AI Customization that includes built-in prompts + * shipped with the application, alongside the core `PromptsStorage` values. + */ +export type AICustomizationPromptsStorage = PromptsStorage | 'builtin'; + +/** + * Storage type discriminator for built-in prompts shipped with the application. + */ +export const BUILTIN_STORAGE: AICustomizationPromptsStorage = 'builtin'; + +/** + * Prompt path for built-in prompts bundled with the Sessions app. + */ +export interface IBuiltinPromptPath { + readonly uri: URI; + readonly storage: AICustomizationPromptsStorage; + readonly type: PromptsType; +} diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts new file mode 100644 index 00000000000..805ad59cca0 --- /dev/null +++ b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from './codeReviewService.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; + +registerSingleton(ICodeReviewService, CodeReviewService, InstantiationType.Delayed); + +const canRunSessionCodeReviewContextKey = new RawContextKey('sessions.canRunCodeReview', true, { + type: 'boolean', + description: localize('sessions.canRunCodeReview', "True when a new code review can be started for the active session version."), +}); + +function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disposable { + class RunSessionCodeReviewAction extends Action2 { + static readonly ID = 'sessions.codeReview.run'; + + constructor() { + super({ + id: RunSessionCodeReviewAction.ID, + title: localize('sessions.runCodeReview', "Run Code Review"), + tooltip, + category: CHAT_CATEGORY, + icon, + precondition: canRunSessionCodeReviewContextKey, + menu: [ + { + id: MenuId.ChatEditingSessionChangesToolbar, + group: 'navigation', + order: 7, + when: ContextKeyExpr.and(IsSessionsWindowContext, ChatContextKeys.hasAgentSessionChanges), + }, + ], + }); + } + + override async run(accessor: ServicesAccessor, sessionResource?: URI): Promise { + const sessionManagementService = accessor.get(ISessionsManagementService); + const agentSessionsService = accessor.get(IAgentSessionsService); + const codeReviewService = accessor.get(ICodeReviewService); + + const resource = URI.isUri(sessionResource) + ? sessionResource + : sessionManagementService.getActiveSession()?.resource; + + if (!resource) { + return; + } + + const session = agentSessionsService.getSession(resource); + if (!(session?.changes instanceof Array) || session.changes.length === 0) { + return; + } + + const files = getCodeReviewFilesFromSessionChanges(session.changes); + const version = getCodeReviewVersion(files); + + codeReviewService.requestReview(resource, version, files); + } + } + + return registerAction2(RunSessionCodeReviewAction) as Disposable; +} + +class CodeReviewToolbarContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'sessions.contrib.codeReviewToolbar'; + + private readonly _actionRegistration = this._register(new MutableDisposable()); + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + @ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService, + @ICodeReviewService private readonly _codeReviewService: ICodeReviewService, + ) { + super(); + + const canRunCodeReviewContext = canRunSessionCodeReviewContextKey.bindTo(contextKeyService); + const sessionsChangedSignal = observableFromEvent(this, this._agentSessionsService.model.onDidChangeSessions, () => undefined); + + this._register(autorun(reader => { + const activeSession = this._sessionManagementService.activeSession.read(reader); + sessionsChangedSignal.read(reader); + this._actionRegistration.clear(); + + const sessionResource = activeSession?.resource; + if (!sessionResource) { + canRunCodeReviewContext.set(false); + this._actionRegistration.value = registerSessionCodeReviewAction(localize('sessions.runCodeReview.noSession', "No active session available for code review."), Codicon.codeReview); + return; + } + + const session = this._agentSessionsService.getSession(sessionResource); + if (!(session?.changes instanceof Array) || session.changes.length === 0) { + canRunCodeReviewContext.set(false); + this._actionRegistration.value = registerSessionCodeReviewAction(localize('sessions.runCodeReview.noChanges', "No changes available for code review."), Codicon.codeReview); + return; + } + + const files = getCodeReviewFilesFromSessionChanges(session.changes); + const version = getCodeReviewVersion(files); + const reviewState = this._codeReviewService.getReviewState(sessionResource).read(reader); + + let canRunCodeReview = true; + let tooltip = localize('sessions.runCodeReview.tooltip.default', "Run Code Review"); + let icon = Codicon.codeReview; + + if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === version) { + canRunCodeReview = false; + tooltip = localize('sessions.runCodeReview.tooltip.loading', "Creating code review..."); + icon = Codicon.commentDraft; + } else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version) { + canRunCodeReview = false; + if (reviewState.comments.length === 0) { + tooltip = localize('sessions.runCodeReview.tooltip.allResolved', "All review comments have been addressed."); + icon = Codicon.comment; + } else { + icon = Codicon.commentUnresolved; + tooltip = reviewState.comments.length === 1 + ? localize('sessions.runCodeReview.tooltip.oneUnresolved', "1 review comment unresolved.") + : localize('sessions.runCodeReview.tooltip.manyUnresolved', "{0} review comments unresolved.", reviewState.comments.length); + } + } + + canRunCodeReviewContext.set(canRunCodeReview); + this._actionRegistration.value = registerSessionCodeReviewAction(tooltip, icon); + })); + } +} + +registerWorkbenchContribution2(CodeReviewToolbarContribution.ID, CodeReviewToolbarContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts new file mode 100644 index 00000000000..acd401cf4e3 --- /dev/null +++ b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts @@ -0,0 +1,353 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { IRange, Range } from '../../../../editor/common/core/range.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { hash } from '../../../../base/common/hash.js'; +import { hasKey } from '../../../../base/common/types.js'; +import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; + +// --- Types ------------------------------------------------------------------- + +export interface ICodeReviewComment { + readonly id: string; + readonly uri: URI; + readonly range: IRange; + readonly body: string; + readonly kind: string; + readonly severity: string; + readonly suggestion?: ICodeReviewSuggestion; +} + +export interface ICodeReviewSuggestion { + readonly edits: readonly ICodeReviewSuggestionChange[]; +} + +export interface ICodeReviewSuggestionChange { + readonly range: IRange; + readonly newText: string; + readonly oldText: string; +} + +export interface ICodeReviewFile { + readonly currentUri: URI; + readonly baseUri?: URI; +} + +export function getCodeReviewFilesFromSessionChanges(changes: readonly (IChatSessionFileChange | IChatSessionFileChange2)[]): readonly ICodeReviewFile[] { + return changes.map(change => { + if (isIChatSessionFileChange2(change)) { + return { + currentUri: change.modifiedUri ?? change.uri, + baseUri: change.originalUri, + }; + } + + return { + currentUri: change.modifiedUri, + baseUri: change.originalUri, + }; + }); +} + +export function getCodeReviewVersion(files: readonly ICodeReviewFile[]): string { + const stableFileList = files + .map(file => `${file.currentUri.toString()}|${file.baseUri?.toString() ?? ''}`) + .sort(); + + return `v1:${stableFileList.length}:${hash(stableFileList)}`; +} + +export const enum CodeReviewStateKind { + Idle = 'idle', + Loading = 'loading', + Result = 'result', + Error = 'error', +} + +export type ICodeReviewState = + | { readonly kind: CodeReviewStateKind.Idle } + | { readonly kind: CodeReviewStateKind.Loading; readonly version: string } + | { readonly kind: CodeReviewStateKind.Result; readonly version: string; readonly comments: readonly ICodeReviewComment[] } + | { readonly kind: CodeReviewStateKind.Error; readonly version: string; readonly reason: string }; + +/** Shape of a single comment as returned by the code review command. */ +interface IRawCodeReviewComment { + readonly uri: IRawCodeReviewUri; + readonly range: IRawCodeReviewRange; + readonly body?: string; + readonly kind?: string; + readonly severity?: string; + readonly suggestion?: IRawCodeReviewSuggestion; +} + +type IRawCodeReviewUri = URI | UriComponents | string; + +interface IRawCodeReviewPosition { + readonly line?: number; + readonly character?: number; +} + +interface IRawCodeReviewRangeWithPositions { + readonly start?: IRawCodeReviewPosition; + readonly end?: IRawCodeReviewPosition; +} + +interface IRawCodeReviewRangeWithLines { + readonly startLine?: number; + readonly startColumn?: number; + readonly endLine?: number; + readonly endColumn?: number; +} + +type IRawCodeReviewRangeTuple = readonly [IRawCodeReviewPosition, IRawCodeReviewPosition]; + +type IRawCodeReviewRange = IRange | IRawCodeReviewRangeWithPositions | IRawCodeReviewRangeWithLines | IRawCodeReviewRangeTuple; + +interface IRawCodeReviewSuggestion { + readonly edits: readonly IRawCodeReviewSuggestionChange[]; +} + +interface IRawCodeReviewSuggestionChange { + readonly range: IRawCodeReviewRange; + readonly newText: string; + readonly oldText: string; +} + +// --- Service Interface ------------------------------------------------------- + +export const ICodeReviewService = createDecorator('codeReviewService'); + +export interface ICodeReviewService { + readonly _serviceBrand: undefined; + + /** + * Get the observable review state for a session. + */ + getReviewState(sessionResource: URI): IObservable; + + /** + * Synchronously check if a completed review exists for the given session+version. + */ + hasReview(sessionResource: URI, version: string): boolean; + + /** + * Request a code review for the given session. The review is associated with + * a version string (fingerprint of changed files). If a review is already in + * progress or completed for this version, this is a no-op. + */ + requestReview(sessionResource: URI, version: string, files: readonly { readonly currentUri: URI; readonly baseUri?: URI }[]): void; + + /** + * Remove a single comment from the review results. + */ + removeComment(sessionResource: URI, commentId: string): void; + + /** + * Dismiss/clear the review for a session entirely. + */ + dismissReview(sessionResource: URI): void; +} + +// --- Implementation ---------------------------------------------------------- + +interface ISessionReviewData { + readonly state: ReturnType>; +} + +function isRawCodeReviewRangeWithPositions(range: IRawCodeReviewRange): range is IRawCodeReviewRangeWithPositions { + return typeof range === 'object' && range !== null && hasKey(range, { start: true, end: true }); +} + +function isRawCodeReviewRangeTuple(range: IRawCodeReviewRange): range is IRawCodeReviewRangeTuple { + return Array.isArray(range) && range.length >= 2; +} + +function normalizeCodeReviewUri(uri: IRawCodeReviewUri): URI { + return typeof uri === 'string' ? URI.parse(uri) : URI.revive(uri); +} + +function normalizeCodeReviewRange(range: IRawCodeReviewRange): IRange { + if (Range.isIRange(range)) { + return Range.lift(range); + } + + if (isRawCodeReviewRangeTuple(range)) { + const [start, end] = range; + return new Range( + (start.line ?? 0) + 1, + (start.character ?? 0) + 1, + (end.line ?? start.line ?? 0) + 1, + (end.character ?? start.character ?? 0) + 1, + ); + } + + if (isRawCodeReviewRangeWithPositions(range) && range.start && range.end) { + return new Range( + (range.start.line ?? 0) + 1, + (range.start.character ?? 0) + 1, + (range.end.line ?? range.start.line ?? 0) + 1, + (range.end.character ?? range.start.character ?? 0) + 1, + ); + } + + const lineRange = range as IRawCodeReviewRangeWithLines; + return new Range( + (lineRange.startLine ?? 0) + 1, + (lineRange.startColumn ?? 0) + 1, + (lineRange.endLine ?? lineRange.startLine ?? 0) + 1, + (lineRange.endColumn ?? lineRange.startColumn ?? 0) + 1, + ); +} + +function normalizeCodeReviewSuggestion(suggestion: IRawCodeReviewSuggestion | undefined): ICodeReviewSuggestion | undefined { + if (!suggestion) { + return undefined; + } + + return { + edits: suggestion.edits.map(edit => ({ + range: normalizeCodeReviewRange(edit.range), + newText: edit.newText, + oldText: edit.oldText, + })), + }; +} + +export class CodeReviewService extends Disposable implements ICodeReviewService { + + declare readonly _serviceBrand: undefined; + + private readonly _reviewsBySession = new Map(); + + constructor( + @ICommandService private readonly _commandService: ICommandService, + ) { + super(); + } + + getReviewState(sessionResource: URI): IObservable { + return this._getOrCreateData(sessionResource).state; + } + + hasReview(sessionResource: URI, version: string): boolean { + const data = this._reviewsBySession.get(sessionResource.toString()); + if (!data) { + return false; + } + const state = data.state.get(); + return state.kind === CodeReviewStateKind.Result && state.version === version; + } + + requestReview(sessionResource: URI, version: string, files: readonly { readonly currentUri: URI; readonly baseUri?: URI }[]): void { + const data = this._getOrCreateData(sessionResource); + const currentState = data.state.get(); + + // Don't re-request if already loading or completed for this version + if (currentState.kind === CodeReviewStateKind.Loading && currentState.version === version) { + return; + } + if (currentState.kind === CodeReviewStateKind.Result && currentState.version === version) { + return; + } + + data.state.set({ kind: CodeReviewStateKind.Loading, version }, undefined); + + this._executeReview(sessionResource, version, files, data); + } + + removeComment(sessionResource: URI, commentId: string): void { + const data = this._reviewsBySession.get(sessionResource.toString()); + if (!data) { + return; + } + + const state = data.state.get(); + if (state.kind !== CodeReviewStateKind.Result) { + return; + } + + const filtered = state.comments.filter(c => c.id !== commentId); + data.state.set({ kind: CodeReviewStateKind.Result, version: state.version, comments: filtered }, undefined); + } + + dismissReview(sessionResource: URI): void { + const data = this._reviewsBySession.get(sessionResource.toString()); + if (data) { + data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + } + } + + private _getOrCreateData(sessionResource: URI): ISessionReviewData { + const key = sessionResource.toString(); + let data = this._reviewsBySession.get(key); + if (!data) { + data = { + state: observableValue(`codeReview.state.${key}`, { kind: CodeReviewStateKind.Idle }), + }; + this._reviewsBySession.set(key, data); + } + return data; + } + + private async _executeReview( + sessionResource: URI, + version: string, + files: readonly { readonly currentUri: URI; readonly baseUri?: URI }[], + data: ISessionReviewData, + ): Promise { + try { + const result: { type: string; comments?: IRawCodeReviewComment[]; reason?: string } | undefined = + await this._commandService.executeCommand('chat.internal.codeReview.run', { + files: files.map(f => ({ + currentUri: f.currentUri, + baseUri: f.baseUri, + })), + }); + + // Check if version is still current (hasn't been dismissed or replaced) + const currentState = data.state.get(); + if (currentState.kind !== CodeReviewStateKind.Loading || currentState.version !== version) { + return; + } + + if (!result || result.type === 'cancelled') { + data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + return; + } + + if (result.type === 'error') { + data.state.set({ kind: CodeReviewStateKind.Error, version, reason: result.reason ?? 'Unknown error' }, undefined); + return; + } + + if (result.type === 'success') { + const comments: ICodeReviewComment[] = (result.comments ?? []).map((raw) => ({ + id: generateUuid(), + uri: normalizeCodeReviewUri(raw.uri), + range: normalizeCodeReviewRange(raw.range), + body: raw.body ?? '', + kind: raw.kind ?? '', + severity: raw.severity ?? '', + suggestion: normalizeCodeReviewSuggestion(raw.suggestion), + })); + + transaction(tx => { + data.state.set({ kind: CodeReviewStateKind.Result, version, comments }, tx); + }); + } + } catch (err) { + const currentState = data.state.get(); + if (currentState.kind === CodeReviewStateKind.Loading && currentState.version === version) { + data.state.set({ kind: CodeReviewStateKind.Error, version, reason: String(err) }, undefined); + } + } + } +} diff --git a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts new file mode 100644 index 00000000000..76dcc8c1385 --- /dev/null +++ b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts @@ -0,0 +1,661 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { Event } from '../../../../../base/common/event.js'; +import { CodeReviewService, CodeReviewStateKind, ICodeReviewService } from '../../browser/codeReviewService.js'; + +suite('CodeReviewService', () => { + + const store = new DisposableStore(); + let service: ICodeReviewService; + let commandService: MockCommandService; + + let session: URI; + let fileA: URI; + let fileB: URI; + + class MockCommandService implements ICommandService { + declare readonly _serviceBrand: undefined; + readonly onWillExecuteCommand = Event.None; + readonly onDidExecuteCommand = Event.None; + + result: unknown = undefined; + lastCommandId: string | undefined; + lastArgs: unknown[] | undefined; + executeDeferred: { resolve: (v: unknown) => void; reject: (e: unknown) => void } | undefined; + + async executeCommand(commandId: string, ...args: unknown[]): Promise { + this.lastCommandId = commandId; + this.lastArgs = args; + + if (this.executeDeferred) { + return await new Promise((resolve, reject) => { + this.executeDeferred = { resolve: resolve as (v: unknown) => void, reject }; + }); + } + + return this.result as T; + } + + /** + * Configure the mock to defer execution until manually resolved/rejected. + */ + deferNextExecution(): void { + this.executeDeferred = undefined; + const self = this; + const originalResult = this.result; + + // Override executeCommand for next call to capture the deferred promise + const origExecute = this.executeCommand.bind(this); + this.executeCommand = async function (commandId: string, ...args: unknown[]): Promise { + self.lastCommandId = commandId; + self.lastArgs = args; + + return new Promise((resolve, reject) => { + self.executeDeferred = { resolve: resolve as (v: unknown) => void, reject }; + }); + } as typeof origExecute; + + // Restore after use + this._restoreExecute = () => { + this.executeCommand = origExecute; + this.result = originalResult; + }; + } + + private _restoreExecute: (() => void) | undefined; + + resolveExecution(value: unknown): void { + this.executeDeferred?.resolve(value); + this.executeDeferred = undefined; + this._restoreExecute?.(); + } + + rejectExecution(error: unknown): void { + this.executeDeferred?.reject(error); + this.executeDeferred = undefined; + this._restoreExecute?.(); + } + } + + setup(() => { + const instantiationService = store.add(new TestInstantiationService()); + + commandService = new MockCommandService(); + instantiationService.stub(ICommandService, commandService); + + service = store.add(instantiationService.createInstance(CodeReviewService)); + session = URI.parse('test://session/1'); + fileA = URI.parse('file:///a.ts'); + fileB = URI.parse('file:///b.ts'); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + // --- getReviewState --- + + test('initial state is idle', () => { + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Idle); + }); + + test('getReviewState returns the same observable for the same session', () => { + const obs1 = service.getReviewState(session); + const obs2 = service.getReviewState(session); + assert.strictEqual(obs1, obs2); + }); + + test('getReviewState returns different observables for different sessions', () => { + const session2 = URI.parse('test://session/2'); + const obs1 = service.getReviewState(session); + const obs2 = service.getReviewState(session2); + assert.notStrictEqual(obs1, obs2); + }); + + // --- hasReview --- + + test('hasReview returns false when no review exists', () => { + assert.strictEqual(service.hasReview(session, 'v1'), false); + }); + + test('hasReview returns false when review is for a different version', async () => { + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + // Wait for async command to complete + await tick(); + + assert.strictEqual(service.hasReview(session, 'v1'), true); + assert.strictEqual(service.hasReview(session, 'v2'), false); + }); + + test('hasReview returns true after successful review', async () => { + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + await tick(); + + assert.strictEqual(service.hasReview(session, 'v1'), true); + }); + + // --- requestReview --- + + test('requestReview transitions to loading state', () => { + commandService.deferNextExecution(); + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Loading); + if (state.kind === CodeReviewStateKind.Loading) { + assert.strictEqual(state.version, 'v1'); + } + + // Resolve to avoid leaking + commandService.resolveExecution({ type: 'success', comments: [] }); + }); + + test('requestReview calls command with correct arguments', async () => { + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [ + { currentUri: fileA, baseUri: fileB }, + { currentUri: fileB }, + ]); + + await tick(); + + assert.strictEqual(commandService.lastCommandId, 'chat.internal.codeReview.run'); + const args = commandService.lastArgs?.[0] as { files: { currentUri: URI; baseUri?: URI }[] }; + assert.strictEqual(args.files.length, 2); + assert.strictEqual(args.files[0].currentUri.toString(), fileA.toString()); + assert.strictEqual(args.files[0].baseUri?.toString(), fileB.toString()); + assert.strictEqual(args.files[1].currentUri.toString(), fileB.toString()); + assert.strictEqual(args.files[1].baseUri, undefined); + }); + + test('requestReview with success populates comments', async () => { + commandService.result = { + type: 'success', + comments: [ + { + uri: fileA, + range: new Range(1, 1, 5, 1), + body: 'Bug found', + kind: 'bug', + severity: 'high', + }, + { + uri: fileB, + range: new Range(10, 1, 15, 1), + body: 'Style issue', + kind: 'style', + severity: 'low', + }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }, { currentUri: fileB }]); + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.version, 'v1'); + assert.strictEqual(state.comments.length, 2); + assert.strictEqual(state.comments[0].body, 'Bug found'); + assert.strictEqual(state.comments[0].kind, 'bug'); + assert.strictEqual(state.comments[0].severity, 'high'); + assert.strictEqual(state.comments[0].uri.toString(), fileA.toString()); + assert.strictEqual(state.comments[1].body, 'Style issue'); + } + }); + + test('requestReview with error transitions to error state', async () => { + commandService.result = { type: 'error', reason: 'Auth failed' }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Error); + if (state.kind === CodeReviewStateKind.Error) { + assert.strictEqual(state.version, 'v1'); + assert.strictEqual(state.reason, 'Auth failed'); + } + }); + + test('requestReview with cancelled result transitions to idle', async () => { + commandService.result = { type: 'cancelled' }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Idle); + }); + + test('requestReview with undefined result transitions to idle', async () => { + commandService.result = undefined; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Idle); + }); + + test('requestReview with thrown error transitions to error state', async () => { + commandService.deferNextExecution(); + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + commandService.rejectExecution(new Error('Network error')); + + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Error); + if (state.kind === CodeReviewStateKind.Error) { + assert.ok(state.reason.includes('Network error')); + } + }); + + test('requestReview is a no-op when loading for the same version', () => { + commandService.deferNextExecution(); + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + // Attempt to request again for the same version + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + // Should still be loading (not re-triggered) + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Loading); + + commandService.resolveExecution({ type: 'success', comments: [] }); + }); + + test('requestReview is a no-op when result exists for the same version', async () => { + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + // Attempt to request again + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + // Should still have the result + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + }); + + test('requestReview for a new version replaces loading state', async () => { + // Start v1 review — it will complete immediately with empty result + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + assert.strictEqual(service.hasReview(session, 'v1'), true); + + // Request v2 — since v1 is a different version, it should proceed + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'v2 comment' }] }; + service.requestReview(session, 'v2', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.version, 'v2'); + assert.strictEqual(state.comments.length, 1); + assert.strictEqual(state.comments[0].body, 'v2 comment'); + } + + // v1 is no longer valid + assert.strictEqual(service.hasReview(session, 'v1'), false); + }); + + // --- removeComment --- + + test('removeComment removes a specific comment', async () => { + commandService.result = { + type: 'success', + comments: [ + { uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment1' }, + { uri: fileA, range: new Range(5, 1, 5, 1), body: 'comment2' }, + { uri: fileB, range: new Range(10, 1, 10, 1), body: 'comment3' }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }, { currentUri: fileB }]); + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind !== CodeReviewStateKind.Result) { return; } + + const commentToRemove = state.comments[1]; + service.removeComment(session, commentToRemove.id); + + const newState = service.getReviewState(session).get(); + assert.strictEqual(newState.kind, CodeReviewStateKind.Result); + if (newState.kind === CodeReviewStateKind.Result) { + assert.strictEqual(newState.comments.length, 2); + assert.strictEqual(newState.comments[0].body, 'comment1'); + assert.strictEqual(newState.comments[1].body, 'comment3'); + } + }); + + test('removeComment is a no-op for unknown comment id', async () => { + commandService.result = { + type: 'success', + comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment1' }], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + service.removeComment(session, 'nonexistent-id'); + + const state = service.getReviewState(session).get(); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.comments.length, 1); + } + }); + + test('removeComment is a no-op when no review exists', () => { + // Should not throw + service.removeComment(session, 'some-id'); + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Idle); + }); + + test('removeComment is a no-op when state is not result', () => { + commandService.deferNextExecution(); + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + // State is loading — removeComment should be ignored + service.removeComment(session, 'some-id'); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Loading); + + commandService.resolveExecution({ type: 'success', comments: [] }); + }); + + test('removeComment preserves version in result', async () => { + commandService.result = { + type: 'success', + comments: [ + { uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment1' }, + { uri: fileA, range: new Range(5, 1, 5, 1), body: 'comment2' }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + if (state.kind !== CodeReviewStateKind.Result) { return; } + + service.removeComment(session, state.comments[0].id); + + const newState = service.getReviewState(session).get(); + if (newState.kind === CodeReviewStateKind.Result) { + assert.strictEqual(newState.version, 'v1'); + } + }); + + // --- dismissReview --- + + test('dismissReview resets to idle', async () => { + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Result); + + service.dismissReview(session); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + }); + + test('dismissReview while loading resets to idle', () => { + commandService.deferNextExecution(); + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Loading); + + service.dismissReview(session); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + + // Resolve the pending command — should be ignored since dismissed + commandService.resolveExecution({ type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'late' }] }); + }); + + test('dismissReview is a no-op when no data exists', () => { + // Should not throw + service.dismissReview(session); + }); + + test('hasReview returns false after dismissReview', async () => { + commandService.result = { type: 'success', comments: [] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + assert.strictEqual(service.hasReview(session, 'v1'), true); + + service.dismissReview(session); + + assert.strictEqual(service.hasReview(session, 'v1'), false); + }); + + // --- Isolation between sessions --- + + test('different sessions are independent', async () => { + const session2 = URI.parse('test://session/2'); + + commandService.result = { + type: 'success', + comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'session1 comment' }], + }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + commandService.result = { + type: 'success', + comments: [{ uri: fileB, range: new Range(2, 1, 2, 1), body: 'session2 comment' }], + }; + service.requestReview(session2, 'v2', [{ currentUri: fileB }]); + await tick(); + + const state1 = service.getReviewState(session).get(); + const state2 = service.getReviewState(session2).get(); + + assert.strictEqual(state1.kind, CodeReviewStateKind.Result); + assert.strictEqual(state2.kind, CodeReviewStateKind.Result); + + if (state1.kind === CodeReviewStateKind.Result && state2.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state1.comments[0].body, 'session1 comment'); + assert.strictEqual(state2.comments[0].body, 'session2 comment'); + } + + // Dismissing session1 doesn't affect session2 + service.dismissReview(session); + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + assert.strictEqual(service.getReviewState(session2).get().kind, CodeReviewStateKind.Result); + }); + + // --- Comment parsing --- + + test('comments with string URIs are parsed correctly', async () => { + commandService.result = { + type: 'success', + comments: [ + { + uri: 'file:///parsed.ts', + range: new Range(1, 1, 1, 1), + body: 'parsed comment', + }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.comments[0].uri.toString(), 'file:///parsed.ts'); + } + }); + + test('comments with missing optional fields get defaults', async () => { + commandService.result = { + type: 'success', + comments: [ + { + uri: fileA, + range: new Range(1, 1, 1, 1), + // body, kind, severity omitted + }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.comments[0].body, ''); + assert.strictEqual(state.comments[0].kind, ''); + assert.strictEqual(state.comments[0].severity, ''); + assert.strictEqual(state.comments[0].suggestion, undefined); + } + }); + + test('comments normalize VS Code API style ranges', async () => { + commandService.result = { + type: 'success', + comments: [ + { + uri: fileA, + range: { + start: { line: 4, character: 2 }, + end: { line: 6, character: 5 }, + }, + body: 'normalized comment', + suggestion: { + edits: [ + { + range: { + start: { line: 8, character: 1 }, + end: { line: 8, character: 9 }, + }, + oldText: 'let value', + newText: 'const value', + }, + ], + }, + }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.deepStrictEqual(state.comments[0].range, new Range(5, 3, 7, 6)); + assert.deepStrictEqual(state.comments[0].suggestion?.edits[0].range, new Range(9, 2, 9, 10)); + } + }); + + test('comments normalize serialized URIs and tuple ranges from API payloads', async () => { + const serializedUri = JSON.parse(JSON.stringify(URI.parse('git:/c%3A/Code/vscode.worktrees/copilot-worktree-2026-03-04T14-44-38/src/vs/sessions/contrib/changesView/test/browser/codeReviewService.test.ts?%7B%22path%22%3A%22c%3A%5C%5CCode%5C%5Cvscode.worktrees%5C%5Ccopilot-worktree-2026-03-04T14-44-38%5C%5Csrc%5C%5Cvs%5C%5Csessions%5C%5Ccontrib%5C%5CchangesView%5C%5Ctest%5C%5Cbrowser%5C%5CcodeReviewService.test.ts%22%2C%22ref%22%3A%22copilot-worktree-2026-03-04T14-44-38%22%7D'))); + + commandService.result = { + type: 'success', + comments: [ + { + uri: serializedUri, + range: [ + { line: 72, character: 2 }, + { line: 72, character: 3 }, + ], + body: 'tuple range comment', + kind: 'bug', + severity: 'medium', + }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.comments[0].uri.toString(), URI.revive(serializedUri).toString()); + assert.deepStrictEqual(state.comments[0].range, new Range(73, 3, 73, 4)); + } + }); + + test('each comment gets a unique id', async () => { + commandService.result = { + type: 'success', + comments: [ + { uri: fileA, range: new Range(1, 1, 1, 1), body: 'a' }, + { uri: fileA, range: new Range(1, 1, 1, 1), body: 'b' }, + ], + }; + + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + if (state.kind === CodeReviewStateKind.Result) { + assert.notStrictEqual(state.comments[0].id, state.comments[1].id); + } + }); + + // --- Observable reactivity --- + + test('observable fires on state transitions', async () => { + const states: string[] = []; + const obs = service.getReviewState(session); + + // Collect initial state + states.push(obs.get().kind); + + commandService.deferNextExecution(); + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + states.push(obs.get().kind); + + commandService.resolveExecution({ type: 'success', comments: [] }); + await tick(); + states.push(obs.get().kind); + + service.dismissReview(session); + states.push(obs.get().kind); + + assert.deepStrictEqual(states, [ + CodeReviewStateKind.Idle, + CodeReviewStateKind.Loading, + CodeReviewStateKind.Result, + CodeReviewStateKind.Idle, + ]); + }); +}); + +function tick(): Promise { + return new Promise(resolve => setTimeout(resolve, 0)); +} diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 44fb59656b3..7bb224f596d 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -30,6 +30,11 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'git.detectWorktrees': false, 'git.showProgress': false, + 'github.copilot.enable': { + 'markdown': true, + 'plaintext': true, + }, + 'github.copilot.chat.claudeCode.enabled': true, 'github.copilot.chat.cli.branchSupport.enabled': true, 'github.copilot.chat.languageContext.typescript.enabled': true, diff --git a/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts b/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts index ea04197d42d..5f6d33c46a3 100644 --- a/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts +++ b/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts @@ -138,7 +138,13 @@ export class GitHubFileSystemProvider extends Disposable implements IFileSystemP // --- GitHub API private async getAuthToken(): Promise { - const sessions = await this.authenticationService.getSessions('github', ['repo'], { silent: true }) ?? await this.authenticationService.getSessions('github', ['repo'], { createIfNone: true }); + let sessions = await this.authenticationService.getSessions('github', [], { silent: true }); + if (!sessions || sessions.length === 0) { + sessions = await this.authenticationService.getSessions('github', [], { createIfNone: true }); + } + if (!sessions || sessions.length === 0) { + throw createFileSystemProviderError('No GitHub authentication sessions available', FileSystemProviderErrorCode.Unavailable); + } return sessions[0].accessToken ?? ''; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index ad30f5c04ad..4f6c1951b51 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -10,6 +10,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IFileService } from '../../../../platform/files/common/files.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { BUILTIN_STORAGE } from '../../chat/common/builtinPromptsStorage.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { parseHooksFromFile } from '../../../../workbench/contrib/chat/common/promptSyntax/hookCompatibility.js'; @@ -20,12 +21,14 @@ export interface ISourceCounts { readonly workspace: number; readonly user: number; readonly extension: number; + readonly builtin: number; } -const storageToCountKey: Partial> = { +const storageToCountKey: Partial> = { [PromptsStorage.local]: 'workspace', [PromptsStorage.user]: 'user', [PromptsStorage.extension]: 'extension', + [BUILTIN_STORAGE]: 'builtin', }; export function getSourceCountsTotal(counts: ISourceCounts, filter: IStorageSourceFilter): number { @@ -129,6 +132,7 @@ export async function getSourceCounts( workspace: filtered.filter(i => i.storage === PromptsStorage.local).length, user: filtered.filter(i => i.storage === PromptsStorage.user).length, extension: filtered.filter(i => i.storage === PromptsStorage.extension).length, + builtin: filtered.filter(i => i.storage === BUILTIN_STORAGE).length, }; } diff --git a/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts b/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts index 9a3ce3ee942..02d70905b80 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts @@ -126,31 +126,31 @@ suite('customizationCounts', () => { suite('getSourceCountsTotal', () => { test('sums only visible sources', () => { - const counts = { workspace: 5, user: 3, extension: 2 }; + const counts = { workspace: 5, user: 3, extension: 2, builtin: 0 }; const filter: IStorageSourceFilter = { sources: [PromptsStorage.local, PromptsStorage.user] }; assert.strictEqual(getSourceCountsTotal(counts, filter), 8); }); test('returns 0 for empty sources', () => { - const counts = { workspace: 5, user: 3, extension: 2 }; + const counts = { workspace: 5, user: 3, extension: 2, builtin: 0 }; const filter: IStorageSourceFilter = { sources: [] }; assert.strictEqual(getSourceCountsTotal(counts, filter), 0); }); test('sums all sources', () => { - const counts = { workspace: 5, user: 3, extension: 2 }; + const counts = { workspace: 5, user: 3, extension: 2, builtin: 0 }; const filter: IStorageSourceFilter = { sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension] }; assert.strictEqual(getSourceCountsTotal(counts, filter), 10); }); test('handles single source', () => { - const counts = { workspace: 7, user: 0, extension: 0 }; + const counts = { workspace: 7, user: 0, extension: 0, builtin: 0 }; const filter: IStorageSourceFilter = { sources: [PromptsStorage.local] }; assert.strictEqual(getSourceCountsTotal(counts, filter), 7); }); test('ignores plugin storage in totals (not in ISourceCounts)', () => { - const counts = { workspace: 1, user: 1, extension: 1 }; + const counts = { workspace: 1, user: 1, extension: 1, builtin: 0 }; const filter: IStorageSourceFilter = { sources: [PromptsStorage.plugin] }; assert.strictEqual(getSourceCountsTotal(counts, filter), 0); }); @@ -334,7 +334,7 @@ suite('customizationCounts', () => { workspaceService, ); - assert.deepStrictEqual(counts, { workspace: 1, user: 1, extension: 1 }); + assert.deepStrictEqual(counts, { workspace: 1, user: 1, extension: 1, builtin: 0 }); }); test('empty agents returns all zeros', async () => { @@ -348,7 +348,7 @@ suite('customizationCounts', () => { contextService, workspaceService, ); - assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0 }); + assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0, builtin: 0 }); }); }); @@ -386,7 +386,7 @@ suite('customizationCounts', () => { contextService, workspaceService, ); - assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0 }); + assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0, builtin: 0 }); }); test('skills filtered by storage source filter', async () => { @@ -450,7 +450,7 @@ suite('customizationCounts', () => { contextService, workspaceService, ); - assert.deepStrictEqual(counts, { workspace: 1, user: 1, extension: 0 }); + assert.deepStrictEqual(counts, { workspace: 1, user: 1, extension: 0, builtin: 0 }); }); test('all skills are excluded from prompt counts', async () => { @@ -469,7 +469,7 @@ suite('customizationCounts', () => { contextService, workspaceService, ); - assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0 }); + assert.deepStrictEqual(counts, { workspace: 0, user: 0, extension: 0, builtin: 0 }); }); }); diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 290fa8b309c..0469553aa77 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; -import { isEqualOrParent } from '../../../../base/common/extpath.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; @@ -16,12 +15,15 @@ import { IWorkbenchContribution, getWorkbenchContribution, registerWorkbenchCont import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ITerminalInstance, ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { Menus } from '../../../browser/menus.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; +import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { TERMINAL_VIEW_ID } from '../../../../workbench/contrib/terminal/common/terminal.js'; /** * Returns the cwd URI for the given session: worktree or repository path for @@ -38,17 +40,14 @@ function getSessionCwd(session: IActiveSessionItem | undefined): URI | undefined /** * Manages terminal instances in the sessions window, ensuring: * - A terminal exists for the active session's worktree (or repository if no worktree). - * - A path→instanceId mapping tracks which terminal belongs to which worktree. + * - Terminals are shown/hidden based on their initial cwd matching the active path. * - All terminals for a worktree are closed when the session is archived. */ export class SessionsTerminalContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.sessionsTerminal'; - /** Maps worktree/repository fsPath (lower-cased) to terminal instance ids. */ - private readonly _pathToInstanceIds = new Map>(); private _activeKey: string | undefined; - private _isCreatingTerminal = false; constructor( @ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService, @@ -65,6 +64,20 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben this._onActiveSessionChanged(session); })); + // Hide restored terminals from a previous window session that don't + // belong to the current active session. These arrive asynchronously + // during reconnection and would otherwise flash in the foreground. + this._register(this._terminalService.onDidCreateInstance(instance => { + if (instance.shellLaunchConfig.attachPersistentProcess && this._activeKey) { + instance.getInitialCwd().then(cwd => { + if (cwd.toLowerCase() !== this._activeKey) { + this._terminalService.moveToBackground(instance); + this._logService.trace(`[SessionsTerminal] Hid restored terminal ${instance.instanceId} (cwd: ${cwd})`); + } + }); + } + })); + // When a session is archived, close all terminals for its worktree this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => { if (session.isArchived()) { @@ -74,59 +87,28 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben } } })); - - // Clean up mapping when terminals are disposed - this._register(this._terminalService.onDidDisposeInstance(instance => { - for (const [path, ids] of this._pathToInstanceIds) { - if (ids.delete(instance.instanceId) && ids.size === 0) { - this._pathToInstanceIds.delete(path); - } - } - })); - - // When terminals are created externally, try to relate them to the active session - this._register(this._terminalService.onDidCreateInstance(instance => { - if (this._isCreatingTerminal || this._activeKey === undefined) { - return; - } - // If this instance is already tracked by us, nothing to do - const activeIds = this._pathToInstanceIds.get(this._activeKey); - if (activeIds?.has(instance.instanceId)) { - return; - } - this._tryAdoptTerminal(instance); - })); } /** - * Ensures a terminal exists for the given cwd, reusing an existing one - * from the mapping or creating a new one. Sets it as active and optionally - * focuses it. + * Ensures a terminal exists for the given cwd by scanning all terminal + * instances for a matching initial cwd. If none is found, creates a new + * one. Sets it as active and optionally focuses it. */ - async ensureTerminal(cwd: URI, focus: boolean): Promise { + async ensureTerminal(cwd: URI, focus: boolean): Promise { const key = cwd.fsPath.toLowerCase(); - const ids = this._pathToInstanceIds.get(key); - const existingId = ids ? ids.values().next().value : undefined; - const existing = existingId !== undefined ? this._terminalService.getInstanceFromId(existingId) : undefined; + let existing = await this._findTerminalsForKey(key); - if (existing) { - await this._terminalService.showBackgroundTerminal(existing); - this._terminalService.setActiveInstance(existing); - } else { - this._isCreatingTerminal = true; - try { - const instance = await this._terminalService.createTerminal({ config: { cwd } }); - this._addInstanceToPath(key, instance.instanceId); - this._terminalService.setActiveInstance(instance); - this._logService.trace(`[SessionsTerminal] Created terminal ${instance.instanceId} for ${cwd.fsPath}`); - } finally { - this._isCreatingTerminal = false; - } + if (existing.length === 0) { + existing = [await this._terminalService.createTerminal({ config: { cwd } })]; + this._terminalService.setActiveInstance(existing[0]); + this._logService.trace(`[SessionsTerminal] Created terminal ${existing[0].instanceId} for ${cwd.fsPath}`); } if (focus) { await this._terminalService.focusActiveInstance(); } + + return existing; } private async _onActiveSessionChanged(session: IActiveSessionItem | undefined): Promise { @@ -143,139 +125,116 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben } this._activeKey = targetKey; - await this.ensureTerminal(targetPath, false); + const instances = await this.ensureTerminal(targetPath, false); // If the active key changed while we were awaiting, a newer call has // taken over — skip the visibility update to avoid flicker. if (this._activeKey !== targetKey) { return; } - this._updateTerminalVisibility(targetKey); - } - - private _addInstanceToPath(key: string, instanceId: number): void { - let ids = this._pathToInstanceIds.get(key); - if (!ids) { - ids = new Set(); - this._pathToInstanceIds.set(key, ids); - } - ids.add(instanceId); + await this._updateTerminalVisibility(targetKey, instances.map(instance => instance.instanceId)); } /** - * Attempts to associate an externally-created terminal with the active - * session by checking whether its initial cwd falls within the active - * session's worktree or repository. Hides the terminal if it cannot be - * related. + * Finds the first terminal instance whose initial cwd (lower-cased) matches + * the given key. */ - private async _tryAdoptTerminal(instance: ITerminalInstance): Promise { - let cwd: string | undefined; - try { - cwd = await instance.getInitialCwd(); - } catch { - return; + private async _findTerminalsForKey(key: string): Promise { + const result: ITerminalInstance[] = []; + for (const instance of this._terminalService.instances) { + try { + const cwd = await instance.getInitialCwd(); + if (cwd.toLowerCase() === key) { + result.push(instance); + } + } catch { + // ignore terminals whose cwd cannot be resolved + } + } + return result; + } + + /** + * Shows background terminals whose initial cwd matches the active key and + * hides foreground terminals whose initial cwd does not match. + */ + private async _updateTerminalVisibility(activeKey: string, forceForegroundTerminalIds: number[]): Promise { + const toShow: ITerminalInstance[] = []; + const toHide: ITerminalInstance[] = []; + + for (const instance of [...this._terminalService.instances]) { + let cwd: string | undefined; + try { + cwd = (await instance.getInitialCwd()).toLowerCase(); + } catch { + continue; + } + + const isForeground = this._terminalService.foregroundInstances.includes(instance); + const isForceVisible = forceForegroundTerminalIds.includes(instance.instanceId); + const belongsToActiveSession = cwd === activeKey; + if ((belongsToActiveSession || isForceVisible) && !isForeground) { + toShow.push(instance); + } else if (!belongsToActiveSession && !isForceVisible && isForeground) { + toHide.push(instance); + } } - if (instance.isDisposed) { - return; + for (const instance of toShow) { + await this._terminalService.showBackgroundTerminal(instance, true); } - - const activeKey = this._activeKey; - if (!activeKey) { - return; - } - - // Re-check tracking — the terminal may have been adopted while awaiting - const activeIds = this._pathToInstanceIds.get(activeKey); - if (activeIds?.has(instance.instanceId)) { - return; - } - - const session = this._sessionsManagementService.activeSession.get(); - if (cwd && this._isRelatedToSession(cwd, session, activeKey)) { - this._addInstanceToPath(activeKey, instance.instanceId); - this._logService.trace(`[SessionsTerminal] Adopted terminal ${instance.instanceId} with cwd ${cwd}`); - } else { + for (const instance of toHide) { this._terminalService.moveToBackground(instance); } - } - /** - * Returns whether the given cwd falls within the active session's - * worktree, repository, or the current active key (home dir fallback). - */ - private _isRelatedToSession(cwd: string, session: IActiveSessionItem | undefined, activeKey: string): boolean { - if (isEqualOrParent(cwd, activeKey, true)) { - return true; - } - if (session?.providerType === AgentSessionProviders.Background && session.repository) { - return isEqualOrParent(cwd, session.repository.fsPath, true); - } - return false; - } - - /** - * Hides all foreground terminals that do not belong to the given active key - * and shows all background terminals that do belong to it. - */ - private _updateTerminalVisibility(activeKey: string): void { - const activeIds = this._pathToInstanceIds.get(activeKey); - - // Hide foreground terminals not belonging to the active session - for (const instance of [...this._terminalService.foregroundInstances]) { - if (!activeIds?.has(instance.instanceId)) { - this._terminalService.moveToBackground(instance); + // Set the terminal with the most recent command as active + const foreground = this._terminalService.foregroundInstances; + let mostRecent: ITerminalInstance | undefined; + let mostRecentTimestamp = -1; + for (const instance of foreground) { + const cmdDetection = instance.capabilities.get(TerminalCapability.CommandDetection); + const lastCmd = cmdDetection?.commands.at(-1); + if (lastCmd && lastCmd.timestamp > mostRecentTimestamp) { + mostRecentTimestamp = lastCmd.timestamp; + mostRecent = instance; } } - - // Show background terminals belonging to the active session - if (activeIds) { - for (const id of activeIds) { - const instance = this._terminalService.getInstanceFromId(id); - if (instance && !this._terminalService.foregroundInstances.includes(instance)) { - this._terminalService.showBackgroundTerminal(instance, true); - } - } + if (mostRecent) { + this._terminalService.setActiveInstance(mostRecent); } } - private _closeTerminalsForPath(fsPath: string): void { + private async _closeTerminalsForPath(fsPath: string): Promise { const key = fsPath.toLowerCase(); - const ids = this._pathToInstanceIds.get(key); - if (ids) { - for (const instanceId of ids) { - const instance = this._terminalService.getInstanceFromId(instanceId); - if (instance) { + for (const instance of [...this._terminalService.instances]) { + try { + const cwd = (await instance.getInitialCwd()).toLowerCase(); + if (cwd === key) { this._terminalService.safeDisposeTerminal(instance); - this._logService.trace(`[SessionsTerminal] Closed archived terminal ${instanceId}`); + this._logService.trace(`[SessionsTerminal] Closed archived terminal ${instance.instanceId}`); } + } catch { + // ignore } - this._pathToInstanceIds.delete(key); } } async dumpTracking(): Promise { - const trackedInstanceIds = new Set(); - - console.log('[SessionsTerminal] === Tracked Terminals ==='); - for (const [key, ids] of this._pathToInstanceIds) { - for (const instanceId of ids) { - trackedInstanceIds.add(instanceId); - const instance = this._terminalService.getInstanceFromId(instanceId); - let cwd = ''; - if (instance) { - try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } - } - console.log(` ${instanceId} - ${cwd} - ${key}`); - } - } - - console.log('[SessionsTerminal] === Untracked Terminals ==='); + console.log(`[SessionsTerminal] Active key: ${this._activeKey ?? ''}`); + console.log('[SessionsTerminal] === All Terminals ==='); for (const instance of this._terminalService.instances) { - if (!trackedInstanceIds.has(instance.instanceId)) { - let cwd = ''; - try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } - console.log(` ${instance.instanceId} - ${cwd}`); + let cwd = ''; + try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } + const isForeground = this._terminalService.foregroundInstances.includes(instance); + console.log(` ${instance.instanceId} - ${cwd} - ${isForeground ? 'foreground' : 'background'}`); + } + } + + async showAllTerminals(): Promise { + for (const instance of this._terminalService.instances) { + if (!this._terminalService.foregroundInstances.includes(instance)) { + await this._terminalService.showBackgroundTerminal(instance, true); + this._logService.trace(`[SessionsTerminal] Moved terminal ${instance.instanceId} to foreground`); } } } @@ -303,10 +262,12 @@ class OpenSessionInTerminalAction extends Action2 { const contribution = getWorkbenchContribution(SessionsTerminalContribution.ID); const sessionsManagementService = _accessor.get(ISessionsManagementService); const pathService = _accessor.get(IPathService); + const viewsService = _accessor.get(IViewsService); const activeSession = sessionsManagementService.activeSession.get(); const cwd = getSessionCwd(activeSession) ?? await pathService.userHome(); await contribution.ensureTerminal(cwd, true); + viewsService.openView(TERMINAL_VIEW_ID); } } @@ -329,3 +290,21 @@ class DumpTerminalTrackingAction extends Action2 { } registerAction2(DumpTerminalTrackingAction); + +class ShowAllTerminalsAction extends Action2 { + + constructor() { + super({ + id: 'agentSession.showAllTerminals', + title: localize2('showAllTerminals', "Show All Terminals"), + f1: true, + }); + } + + override async run(): Promise { + const contribution = getWorkbenchContribution(SessionsTerminalContribution.ID); + await contribution.showAllTerminals(); + } +} + +registerAction2(ShowAllTerminalsAction); diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index 7305d4591b5..0d84c3735df 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -6,13 +6,14 @@ import assert from 'assert'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; -import { Emitter } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { NullLogService, ILogService } from '../../../../../platform/log/common/log.js'; import { ITerminalInstance, ITerminalService } from '../../../../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalCapabilityStore, ICommandDetectionCapability, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { IAgentSession, IAgentSessionsModel } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { AgentSessionProviders } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; @@ -51,23 +52,36 @@ function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerT } as IActiveSessionItem; } -function makeTerminalInstance(id: number, cwd: string): ITerminalInstance { +function makeTerminalInstance(id: number, cwd: string): ITerminalInstance & { _testCommandHistory: { timestamp: number }[] } { + const commandHistory: { timestamp: number }[] = []; + const capabilities = { + get(cap: TerminalCapability) { + if (cap === TerminalCapability.CommandDetection && commandHistory.length > 0) { + return { commands: commandHistory } as unknown as ICommandDetectionCapability; + } + return undefined; + } + } as ITerminalCapabilityStore; + return { instanceId: id, isDisposed: false, getInitialCwd: () => Promise.resolve(cwd), - } as unknown as ITerminalInstance; + capabilities, + _testCommandHistory: commandHistory, + } as unknown as ITerminalInstance & { _testCommandHistory: { timestamp: number }[] }; +} + +function addCommandToInstance(instance: ITerminalInstance, timestamp: number): void { + (instance as ITerminalInstance & { _testCommandHistory: { timestamp: number }[] })._testCommandHistory.push({ timestamp }); } suite('SessionsTerminalContribution', () => { - const store = new DisposableStore(); let contribution: SessionsTerminalContribution; let activeSessionObs: ReturnType>; let onDidChangeSessionArchivedState: Emitter; - let onDidDisposeInstance: Emitter; - let onDidCreateInstance: Emitter; let createdTerminals: { cwd: URI }[]; let activeInstanceSet: number[]; let focusCalls: number; @@ -93,8 +107,6 @@ suite('SessionsTerminalContribution', () => { activeSessionObs = observableValue('activeSession', undefined); onDidChangeSessionArchivedState = store.add(new Emitter()); - onDidDisposeInstance = store.add(new Emitter()); - onDidCreateInstance = store.add(new Emitter()); instantiationService.stub(ILogService, new NullLogService()); @@ -103,8 +115,10 @@ suite('SessionsTerminalContribution', () => { }); instantiationService.stub(ITerminalService, new class extends mock() { - override onDidDisposeInstance = onDidDisposeInstance.event; - override onDidCreateInstance = onDidCreateInstance.event; + override onDidCreateInstance = Event.None; + override get instances(): readonly ITerminalInstance[] { + return [...terminalInstances.values()]; + } override get foregroundInstances(): readonly ITerminalInstance[] { return [...terminalInstances.values()].filter(i => !backgroundedInstances.has(i.instanceId)); } @@ -115,7 +129,6 @@ suite('SessionsTerminalContribution', () => { const instance = makeTerminalInstance(id, cwdStr); createdTerminals.push({ cwd: opts?.config?.cwd }); terminalInstances.set(id, instance); - onDidCreateInstance.fire(instance); return instance; } override getInstanceFromId(id: number): ITerminalInstance | undefined { @@ -285,7 +298,7 @@ suite('SessionsTerminalContribution', () => { await contribution.ensureTerminal(cwd, false); assert.strictEqual(createdTerminals.length, 1, 'should reuse the existing terminal'); - assert.strictEqual(activeInstanceSet.length, 2, 'should set active instance both times'); + assert.strictEqual(activeInstanceSet.length, 1, 'should only set active instance on creation'); }); test('ensureTerminal creates new terminal for different path', async () => { @@ -315,6 +328,7 @@ suite('SessionsTerminalContribution', () => { worktreePath: worktreeUri.fsPath, }); onDidChangeSessionArchivedState.fire(session); + await tick(); assert.strictEqual(disposedInstances.length, 1); }); @@ -328,6 +342,7 @@ suite('SessionsTerminalContribution', () => { worktreePath: worktreeUri.fsPath, }); onDidChangeSessionArchivedState.fire(session); + await tick(); assert.strictEqual(disposedInstances.length, 0); }); @@ -338,27 +353,11 @@ suite('SessionsTerminalContribution', () => { const session = makeAgentSession({ isArchived: true }); onDidChangeSessionArchivedState.fire(session); + await tick(); assert.strictEqual(disposedInstances.length, 0); }); - // --- onDidDisposeInstance --- - - test('cleans up path mapping when terminal is disposed externally', async () => { - const cwd = URI.file('/test-cwd'); - await contribution.ensureTerminal(cwd, false); - assert.strictEqual(createdTerminals.length, 1); - - // Simulate external disposal of the terminal - const instanceId = activeInstanceSet[0]; - const instance = terminalInstances.get(instanceId)!; - onDidDisposeInstance.fire(instance); - - // Now ensureTerminal should create a new one since the mapping was cleaned up - await contribution.ensureTerminal(cwd, false); - assert.strictEqual(createdTerminals.length, 2, 'should create a new terminal after the old one was disposed'); - }); - // --- switching back to previously used path reuses terminal --- test('switching back to a previously used background path reuses the existing terminal', async () => { @@ -379,7 +378,7 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals.length, 2, 'should reuse the terminal for cwd1'); }); - // --- Terminal visibility management --- + // --- Terminal visibility management (cwd-based) --- test('hides terminals from previous session when switching to a new session', async () => { const cwd1 = URI.file('/cwd1'); @@ -387,8 +386,7 @@ suite('SessionsTerminalContribution', () => { activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); await tick(); - const firstTerminalId = createdTerminals.length; - assert.strictEqual(firstTerminalId, 1); + assert.strictEqual(createdTerminals.length, 1); activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); await tick(); @@ -439,33 +437,33 @@ suite('SessionsTerminalContribution', () => { assert.ok(!backgroundedInstances.has(3), 'terminal for cwd3 should be foreground'); }); - test('hides restored terminals that do not belong to the active session', async () => { - // Set an active session first - const cwd1 = URI.file('/cwd1'); - activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + test('shows pre-existing terminal with matching cwd instead of creating a new one', async () => { + // Manually add a terminal that already exists with a matching cwd + const cwd = URI.file('/worktree'); + const existingInstance = makeTerminalInstance(nextInstanceId++, cwd.fsPath); + terminalInstances.set(existingInstance.instanceId, existingInstance); + backgroundedInstances.add(existingInstance.instanceId); + + activeSessionObs.set(makeAgentSession({ worktree: cwd, providerType: AgentSessionProviders.Background }), undefined); await tick(); - // Simulate a terminal being restored (e.g. on startup) that is not tracked - const restoredInstance = makeTerminalInstance(nextInstanceId++, '/some/other/path'); - terminalInstances.set(restoredInstance.instanceId, restoredInstance); - onDidCreateInstance.fire(restoredInstance); - await tick(); - - // The restored terminal should be moved to background - assert.ok(moveToBackgroundCalls.includes(restoredInstance.instanceId), 'restored terminal should be backgrounded'); + assert.strictEqual(createdTerminals.length, 0, 'should reuse existing terminal, not create a new one'); + assert.ok(showBackgroundCalls.includes(existingInstance.instanceId), 'should show the existing terminal'); }); - test('does not hide restored terminals before any session is active', async () => { - // Simulate a terminal being restored before any session is active - const restoredInstance = makeTerminalInstance(nextInstanceId++, '/some/path'); - terminalInstances.set(restoredInstance.instanceId, restoredInstance); - onDidCreateInstance.fire(restoredInstance); + test('hides pre-existing terminal with non-matching cwd when session changes', async () => { + // Manually add a terminal that already exists with a different cwd + const otherInstance = makeTerminalInstance(nextInstanceId++, '/other/path'); + terminalInstances.set(otherInstance.instanceId, otherInstance); + + const cwd = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree: cwd, providerType: AgentSessionProviders.Background }), undefined); await tick(); - assert.strictEqual(moveToBackgroundCalls.length, 0, 'should not background before any session is active'); + assert.ok(moveToBackgroundCalls.includes(otherInstance.instanceId), 'non-matching terminal should be backgrounded'); }); - test('ensureTerminal shows a backgrounded terminal instead of creating a new one', async () => { + test('ensureTerminal finds a backgrounded terminal instead of creating a new one', async () => { const cwd = URI.file('/test-cwd'); await contribution.ensureTerminal(cwd, false); const instanceId = activeInstanceSet[0]; @@ -473,69 +471,71 @@ suite('SessionsTerminalContribution', () => { // Manually background it backgroundedInstances.add(instanceId); - // ensureTerminal should show it, not create a new one - await contribution.ensureTerminal(cwd, false); + // ensureTerminal should find it by cwd, not create a new one + const result = await contribution.ensureTerminal(cwd, false); assert.strictEqual(createdTerminals.length, 1, 'should not create a new terminal'); - assert.ok(showBackgroundCalls.includes(instanceId), 'should show the backgrounded terminal'); + assert.strictEqual(result[0].instanceId, instanceId, 'should return the existing backgrounded terminal'); }); - // --- Terminal adoption --- + test('visibility is determined by initial cwd, not by stored IDs', async () => { + // Create a terminal externally (not via ensureTerminal) with a known cwd + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + const ext1 = makeTerminalInstance(nextInstanceId++, cwd1.fsPath); + const ext2 = makeTerminalInstance(nextInstanceId++, cwd2.fsPath); + terminalInstances.set(ext1.instanceId, ext1); + terminalInstances.set(ext2.instanceId, ext2); - test('adopts externally-created terminal whose cwd matches the active worktree', async () => { - const worktree = URI.file('/worktree'); - activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + // Switch to cwd1 — ext1 should stay visible, ext2 should be hidden + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); await tick(); - const externalInstance = makeTerminalInstance(nextInstanceId++, worktree.fsPath); - terminalInstances.set(externalInstance.instanceId, externalInstance); - onDidCreateInstance.fire(externalInstance); + assert.ok(!backgroundedInstances.has(ext1.instanceId), 'ext1 should be foreground (matching cwd)'); + assert.ok(backgroundedInstances.has(ext2.instanceId), 'ext2 should be backgrounded (non-matching cwd)'); + + // Switch to cwd2 — ext2 should be shown, ext1 should be hidden + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); await tick(); - assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'should not be hidden'); - // Verify it was adopted — ensureTerminal should reuse it - await contribution.ensureTerminal(worktree, false); - assert.strictEqual(createdTerminals.length, 1, 'should reuse adopted terminal, not create a second'); + assert.ok(backgroundedInstances.has(ext1.instanceId), 'ext1 should now be backgrounded'); + assert.ok(!backgroundedInstances.has(ext2.instanceId), 'ext2 should now be foreground'); }); - test('adopts externally-created terminal whose cwd is a subdirectory of the active worktree', async () => { - const worktree = URI.file('/worktree'); - activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + // --- Most-recent-command active terminal selection --- + + test('sets the terminal with the most recent command as active after visibility update', async () => { + const cwd = URI.file('/worktree'); + const t1 = makeTerminalInstance(nextInstanceId++, cwd.fsPath); + const t2 = makeTerminalInstance(nextInstanceId++, cwd.fsPath); + terminalInstances.set(t1.instanceId, t1); + terminalInstances.set(t2.instanceId, t2); + + // t1 ran a command at timestamp 100, t2 at timestamp 200 (more recent) + addCommandToInstance(t1, 100); + addCommandToInstance(t2, 200); + + activeSessionObs.set(makeAgentSession({ worktree: cwd, providerType: AgentSessionProviders.Background }), undefined); await tick(); - const externalInstance = makeTerminalInstance(nextInstanceId++, URI.file('/worktree/subdir').fsPath); - terminalInstances.set(externalInstance.instanceId, externalInstance); - onDidCreateInstance.fire(externalInstance); - await tick(); - - assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'subdirectory terminal should not be hidden'); + // The most recent setActiveInstance call should be for t2 + assert.strictEqual(activeInstanceSet.at(-1), t2.instanceId, 'should set the terminal with the most recent command as active'); }); - test('adopts externally-created terminal whose cwd matches the session repository', async () => { - const worktree = URI.file('/worktree'); - const repo = URI.file('/repo'); - activeSessionObs.set(makeAgentSession({ worktree, repository: repo, providerType: AgentSessionProviders.Background }), undefined); + test('does not change active instance when no terminals have command history', async () => { + const cwd = URI.file('/worktree'); + const t1 = makeTerminalInstance(nextInstanceId++, cwd.fsPath); + const t2 = makeTerminalInstance(nextInstanceId++, cwd.fsPath); + terminalInstances.set(t1.instanceId, t1); + terminalInstances.set(t2.instanceId, t2); + + const activeCountBefore = activeInstanceSet.length; + + activeSessionObs.set(makeAgentSession({ worktree: cwd, providerType: AgentSessionProviders.Background }), undefined); await tick(); - const externalInstance = makeTerminalInstance(nextInstanceId++, repo.fsPath); - terminalInstances.set(externalInstance.instanceId, externalInstance); - onDidCreateInstance.fire(externalInstance); - await tick(); - - assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'terminal at repository path should not be hidden'); - }); - - test('hides externally-created terminal whose cwd does not match the active session', async () => { - const worktree = URI.file('/worktree'); - activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); - await tick(); - - const externalInstance = makeTerminalInstance(nextInstanceId++, '/unrelated/path'); - terminalInstances.set(externalInstance.instanceId, externalInstance); - onDidCreateInstance.fire(externalInstance); - await tick(); - - assert.ok(moveToBackgroundCalls.includes(externalInstance.instanceId), 'unrelated terminal should be hidden'); + // No setActiveInstance calls from visibility update since no commands were run + assert.strictEqual(activeInstanceSet.length, activeCountBefore, 'should not call setActiveInstance when no command history exists'); }); }); diff --git a/src/vs/sessions/prompts/create-pr.prompt.md b/src/vs/sessions/prompts/create-pr.prompt.md new file mode 100644 index 00000000000..28cb057aeea --- /dev/null +++ b/src/vs/sessions/prompts/create-pr.prompt.md @@ -0,0 +1,11 @@ +--- +description: Create a pull request for the current session +--- + + +Use the GitHub MCP server to create a pull request — do NOT use the `gh` CLI. + +1. Review all changes in the current session +2. Write a clear, concise PR title with a short area prefix (e.g. "sessions: …", "editor: …") +3. Write a description covering what changed, why, and anything reviewers should know +4. Create the pull request diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index efe6d190c0d..6d98af657ae 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -206,6 +206,7 @@ import './contrib/chat/browser/customizationsDebugLog.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; +import './contrib/codeReview/browser/codeReview.contributions.js'; import './contrib/files/browser/files.contribution.js'; import './contrib/gitSync/browser/gitSync.contribution.js'; import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 14235489654..ffadc12ea96 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -429,7 +429,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_MAXIMIZE MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_LOCK_GROUP_COMMAND_ID, title: localize('lockGroup', "Lock Group"), toggled: ActiveEditorGroupLockedContext }, group: '8_group_operations', order: 10, when: IsAuxiliaryWindowContext.toNegated() /* already a primary action for aux windows */ }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: ConfigureEditorAction.ID, title: localize('configureEditors', "Configure Editors") }, group: '9_configure', order: 10 }); -function appendEditorToolItem(primary: ICommandAction, when: ContextKeyExpression | undefined, order: number, alternative?: ICommandAction, precondition?: ContextKeyExpression | undefined, enableInCompactMode?: boolean): void { +function appendEditorToolItem(primary: ICommandAction, when: ContextKeyExpression | undefined, order: number, alternative?: ICommandAction, precondition?: ContextKeyExpression | undefined, enableInCompactMode?: boolean, enableInModalMode?: boolean): void { const item: IMenuItem = { command: { id: primary.id, @@ -455,6 +455,9 @@ function appendEditorToolItem(primary: ICommandAction, when: ContextKeyExpressio if (enableInCompactMode) { MenuRegistry.appendMenuItem(MenuId.CompactWindowEditorTitle, item); } + if (enableInModalMode) { + MenuRegistry.appendMenuItem(MenuId.ModalEditorEditorTitle, item); + } } const SPLIT_ORDER = 100000; // towards the end @@ -601,6 +604,7 @@ appendEditorToolItem( 10, undefined, EditorContextKeys.hasChanges, + true, true ); @@ -616,6 +620,7 @@ appendEditorToolItem( 11, undefined, EditorContextKeys.hasChanges, + true, true ); diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index 6f5271cc84c..3b95d6bf310 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -138,5 +138,13 @@ color: inherit; } } + + .modal-editor-action-separator { + width: 1px; + height: 16px; + margin: 0 4px; + background-color: var(--vscode-titleBar-activeForeground); + opacity: 0.3; + } } } diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 2e8ed96d010..4f164d14403 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import './media/modalEditorPart.css'; -import { $, addDisposableListener, append, EventHelper, EventType, hide, isHTMLElement, show } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventHelper, EventType, hide, isHTMLElement, setVisibility, show } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { prepareActions } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -231,7 +232,30 @@ export class ModalEditorPart { [IEditorService, modalEditorService] ))); - // Create toolbar + // Create editor toolbar + const editorActionsToolbarContainer = append(actionBarContainer, $('div.modal-editor-editor-actions')); + const editorActionsToolbar = disposables.add(scopedInstantiationService.createInstance(WorkbenchToolBar, editorActionsToolbarContainer, { + hiddenItemStrategy: HiddenItemStrategy.NoHide, + highlightToggledItems: true, + })); + + const editorActionsSeparator = append(actionBarContainer, $('div.modal-editor-action-separator')); + const editorActionsDisposables = disposables.add(new DisposableStore()); + const updateEditorActions = () => { + editorActionsDisposables.clear(); + + const editorActions = editorPart.activeGroup.createEditorActions(editorActionsDisposables, MenuId.ModalEditorEditorTitle); + editorActionsDisposables.add(editorActions.onDidChange(() => updateEditorActions())); + + const { primary, secondary } = editorActions.actions; + editorActionsToolbar.setActions(prepareActions(primary), prepareActions(secondary)); + + const hasActions = primary.length > 0 || secondary.length > 0; + setVisibility(hasActions, editorActionsSeparator); + }; + disposables.add(Event.runAndSubscribe(modalEditorService.onDidActiveEditorChange, () => updateEditorActions())); + + // Create global toolbar disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.ModalEditorTitle, { hiddenItemStrategy: HiddenItemStrategy.NoHide, highlightToggledItems: true, @@ -259,8 +283,6 @@ export class ModalEditorPart { } else { label.element.clear(); } - - editorPart.notifyActiveEditorChanged(); })); // Handle double-click on header to toggle maximize @@ -379,23 +401,18 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { } private enforceModalPartOptions(): void { - const editorCount = this.groups.reduce((count, group) => count + group.count, 0); this.optionsDisposable.value = this.enforcePartOptions({ - showTabs: editorCount > 1 ? 'multiple' : 'none', + showTabs: 'none', enablePreview: true, closeEmptyGroups: true, - tabActionCloseVisibility: editorCount > 1, - editorActionsLocation: 'default', + tabActionCloseVisibility: false, + editorActionsLocation: 'hidden', tabHeight: 'default', wrapTabs: false, allowDropIntoGroup: false }); } - notifyActiveEditorChanged(): void { - this.enforceModalPartOptions(); - } - updateOptions(options?: IModalEditorPartOptions): void { if (typeof options?.maximized === 'boolean' && options.maximized !== this._maximized) { this.toggleMaximized(); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index b5335cbe676..9fba2ec928c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -23,6 +23,7 @@ import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { Throttler } from '../../../../../base/common/async.js'; +import { observableValue } from '../../../../../base/common/observable.js'; import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js'; import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; import { Separator } from '../../../../../base/common/actions.js'; @@ -241,7 +242,8 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo const sorter = new AgentSessionsSorter(this.options); const approvalModel = this.options.enableApprovalRow ? this._register(this.instantiationService.createInstance(AgentSessionApprovalModel)) : undefined; - const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options, approvalModel)); + const activeSessionResource = observableValue(this, undefined); + const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options, approvalModel, activeSessionResource)); const sessionFilter = this._register(new AgentSessionsDataSource(this.options.filter, sorter)); const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'AgentSessionsView', @@ -313,10 +315,12 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.focusedAgentSessionArchivedContextKey.set(focused.isArchived()); this.focusedAgentSessionReadContextKey.set(focused.isRead()); this.focusedAgentSessionTypeContextKey.set(focused.providerType); + activeSessionResource.set(focused.resource, undefined); } else { this.focusedAgentSessionArchivedContextKey.reset(); this.focusedAgentSessionReadContextKey.reset(); this.focusedAgentSessionTypeContextKey.reset(); + activeSessionResource.set(undefined, undefined); } const selection = list.getSelection().filter(isAgentSession); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 7d46e899058..58b5b41e5ab 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -43,7 +43,8 @@ import { MarkdownString, IMarkdownString } from '../../../../../base/common/html import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js'; import { AgentSessionProviders, getAgentSessionTime } from './agentSessions.js'; import { AgentSessionsGrouping } from './agentSessionsFilter.js'; -import { autorun } from '../../../../../base/common/observable.js'; +import { autorun, IObservable } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; @@ -115,6 +116,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre constructor( private readonly options: IAgentSessionRendererOptions, private readonly _approvalModel: AgentSessionApprovalModel | undefined, + private readonly _activeSessionResource: IObservable, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @IProductService private readonly productService: IProductService, @IHoverService private readonly hoverService: IHoverService, @@ -487,8 +489,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre })); template.approvalButtonContainer.textContent = ''; + const isActive = this._activeSessionResource.read(reader)?.toString() === session.element.resource.toString(); const button = buttonStore.add(new Button(template.approvalButtonContainer, { title: localize('allowActionOnce', "Allow once"), + secondary: isActive, ...defaultButtonStyles })); button.label = localize('allowAction', "Allow"); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts index f6937026881..2d2d087fac3 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts @@ -67,6 +67,11 @@ export const extensionIcon = registerIcon('ai-customization-extension', Codicon. */ export const pluginIcon = registerIcon('ai-customization-plugin', Codicon.plug, localize('aiCustomizationPluginIcon', "Icon for plugin-contributed items.")); +/** + * Icon for built-in storage. + */ +export const builtinIcon = registerIcon('ai-customization-builtin', Codicon.starFull, localize('aiCustomizationBuiltinIcon', "Icon for built-in items.")); + /** * Icon for MCP servers. */ diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 8a77a8e5223..2ae76f14240 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -19,8 +19,8 @@ import { WorkbenchList } from '../../../../../platform/list/browser/listService. import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../../base/browser/ui/list/list.js'; import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, userIcon, workspaceIcon, extensionIcon, pluginIcon } from './aiCustomizationIcons.js'; -import { AICustomizationManagementItemMenuId, AICustomizationManagementSection } from './aiCustomizationManagement.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, userIcon, workspaceIcon, extensionIcon, pluginIcon, builtinIcon } from './aiCustomizationIcons.js'; +import { AICustomizationManagementItemMenuId, AICustomizationManagementSection, BUILTIN_STORAGE } from './aiCustomizationManagement.js'; import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js'; import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { Delayer } from '../../../../../base/common/async.js'; @@ -889,6 +889,7 @@ export class AICustomizationListWidget extends Disposable { const userItems = allItems.filter(item => item.storage === PromptsStorage.user); const extensionItems = allItems.filter(item => item.storage === PromptsStorage.extension); const pluginItems = allItems.filter(item => item.storage === PromptsStorage.plugin); + const builtinItems = allItems.filter(item => item.storage === BUILTIN_STORAGE); const mapToListItem = (item: IPromptPath): IAICustomizationListItem => { const filename = basename(item.uri); @@ -909,6 +910,7 @@ export class AICustomizationListWidget extends Disposable { items.push(...userItems.map(mapToListItem)); items.push(...extensionItems.map(mapToListItem)); items.push(...pluginItems.map(mapToListItem)); + items.push(...builtinItems.map(mapToListItem)); } // Apply storage source filter (removes items not in visible sources or excluded user roots) @@ -983,6 +985,7 @@ export class AICustomizationListWidget extends Disposable { { groupKey: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, { groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, { groupKey: PromptsStorage.plugin, label: localize('pluginGroup', "Plugins"), icon: pluginIcon, description: localize('pluginGroupDescription', "Read-only customizations provided by installed plugins."), items: [] }, + { groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] }, { groupKey: 'agents', label: localize('agentsGroup', "Agents"), icon: agentIcon, description: localize('agentsGroupDescription', "Hooks defined in agent files."), items: [] }, ].filter(g => visibleSources.has(g.groupKey as PromptsStorage) || g.groupKey === 'agents'); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts index 6aa4d4ba4d4..e9ed6863b8f 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts @@ -5,12 +5,24 @@ import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js'; +import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { localize } from '../../../../../nls.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; // Re-export for convenience — consumers import from this file export { AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js'; +/** + * Extended storage type for AI Customization that includes built-in prompts + * shipped with the application, alongside the core `PromptsStorage` values. + */ +export type AICustomizationPromptsStorage = PromptsStorage | 'builtin'; + +/** + * Storage type discriminator for built-in prompts shipped with the application. + */ +export const BUILTIN_STORAGE: AICustomizationPromptsStorage = 'builtin'; + /** * Editor pane ID for the AI Customizations Management Editor. */ diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index ed1667fbed9..82cfd7a06ad 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -39,6 +39,7 @@ import { AI_CUSTOMIZATION_MANAGEMENT_SIDEBAR_WIDTH_KEY, AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, AICustomizationManagementSection, + BUILTIN_STORAGE, CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_EDITOR, CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_SECTION, SIDEBAR_DEFAULT_WIDTH, @@ -452,7 +453,7 @@ export class AICustomizationManagementEditor extends EditorPane { // Handle item selection this.editorDisposables.add(this.listWidget.onDidSelectItem(item => { const isWorkspaceFile = item.storage === PromptsStorage.local; - const isReadOnly = item.storage === PromptsStorage.extension || item.storage === PromptsStorage.plugin; + const isReadOnly = item.storage === PromptsStorage.extension || item.storage === PromptsStorage.plugin || item.storage === BUILTIN_STORAGE; this.showEmbeddedEditor(item.uri, item.name, isWorkspaceFile, isReadOnly); })); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index e16ecec8d57..962e0854053 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -81,6 +81,9 @@ class McpServerItemDelegate implements IListVirtualDelegate { if (element.type === 'group-header') { return element.isFirst ? MCP_GROUP_HEADER_HEIGHT : MCP_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; } + if (element.type === 'server-item' && element.server.gallery && !element.server.local) { + return 62; + } return MCP_ITEM_HEIGHT; } @@ -284,15 +287,16 @@ class McpGalleryItemRenderer implements IListRenderer .details > .footer { + display: flex; + align-items: center; +} + +.mcp-gallery-item.extension-list-item .mcp-gallery-action { + margin-left: auto; +} + .mcp-gallery-item .mcp-gallery-install-button { font-size: 11px; padding: 2px 10px; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts index ff1135b9a58..bd990886dca 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -83,6 +83,9 @@ class PluginItemDelegate implements IListVirtualDelegate { if (element.type === 'group-header') { return element.isFirst ? PLUGIN_GROUP_HEADER_HEIGHT : PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; } + if (element.type === 'marketplace-item') { + return 62; + } return PLUGIN_ITEM_HEIGHT; } @@ -244,15 +247,16 @@ class PluginMarketplaceItemRenderer implements IListRenderer { + CommandsRegistry.registerCommand(coreCommand, async (accessor, ...args) => { const commandService = accessor.get(ICommandService); const codeEditorService = accessor.get(ICodeEditorService); const markerService = accessor.get(IMarkerService); @@ -499,6 +499,10 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr if (result) { await commandService.executeCommand(actualCommand); } + break; + } + case 'chat.internal.codeReview.run': { + return commandService.executeCommand(actualCommand, ...args); } } }); @@ -506,6 +510,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr registerGenerateCodeCommand('chat.internal.explain', 'github.copilot.chat.explain'); registerGenerateCodeCommand('chat.internal.fix', 'github.copilot.chat.fix'); registerGenerateCodeCommand('chat.internal.review', 'github.copilot.chat.review'); + registerGenerateCodeCommand('chat.internal.codeReview.run', 'github.copilot.chat.codeReview.run'); const internalGenerateCodeContext = ContextKeyExpr.and( ChatContextKeys.Setup.hidden.negate(), diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index 286257580e2..2b6c01066fe 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -35,9 +35,9 @@ export type AICustomizationManagementSection = typeof AICustomizationManagementS */ export interface IStorageSourceFilter { /** - * Which storage groups to display (e.g. workspace, user, extension). + * Which storage groups to display (e.g. workspace, user, extension, builtin). */ - readonly sources: readonly PromptsStorage[]; + readonly sources: readonly string[]; /** * If set, only user files under these roots are shown (allowlist). @@ -51,7 +51,7 @@ export interface IStorageSourceFilter { * Removes items whose storage is not in the filter's source list, * and for user-storage items, removes those not under an allowed root. */ -export function applyStorageSourceFilter(items: readonly T[], filter: IStorageSourceFilter): readonly T[] { +export function applyStorageSourceFilter(items: readonly T[], filter: IStorageSourceFilter): readonly T[] { const sourceSet = new Set(filter.sources); return items.filter(item => { if (!sourceSet.has(item.storage)) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 5ac483ffef2..6fce0410f84 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -15,7 +15,6 @@ import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/h import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IAction, Action, Separator } from '../../../../base/common/actions.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -43,13 +42,13 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { MenuRegistry, MenuId, isIMenuItem } from '../../../../platform/actions/common/actions.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { WORKBENCH_BACKGROUND } from '../../../common/theme.js'; -import { IKeybindingItemEntry, IKeybindingsEditorPane, IPreferencesService } from '../../../services/preferences/common/preferences.js'; +import { IKeybindingItemEntry, IKeybindingsEditorPane } from '../../../services/preferences/common/preferences.js'; import { keybindingsRecordKeysIcon, keybindingsSortIcon, keybindingsAddIcon, preferencesClearInputIcon, keybindingsEditIcon } from './preferencesIcons.js'; import { ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js'; import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js'; import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js'; -import { defaultButtonStyles, defaultKeybindingLabelStyles, defaultToggleStyles, getInputBoxStyle } from '../../../../platform/theme/browser/defaultStyles.js'; +import { defaultKeybindingLabelStyles, defaultToggleStyles, getInputBoxStyle } from '../../../../platform/theme/browser/defaultStyles.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { isString } from '../../../../base/common/types.js'; @@ -131,8 +130,7 @@ export class KeybindingsEditor extends EditorPane imp @IEditorService private readonly editorService: IEditorService, @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IPreferencesService private readonly preferencesService: IPreferencesService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = this._register(new Delayer(300)); @@ -366,8 +364,7 @@ export class KeybindingsEditor extends EditorPane imp const clearInputAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults())); - const searchRowContainer = DOM.append(this.headerContainer, $('.search-row-container')); - const searchContainer = DOM.append(searchRowContainer, $('.search-container')); + const searchContainer = DOM.append(this.headerContainer, $('.search-container')); this.searchWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, searchContainer, { ariaLabel: fullTextSearchPlaceholder, placeholder: fullTextSearchPlaceholder, @@ -429,11 +426,6 @@ export class KeybindingsEditor extends EditorPane imp })); toolBar.setActions(actions); this._register(this.keybindingsService.onDidUpdateKeybindings(() => toolBar.setActions(actions))); - - const openKeybindingsJsonContainer = DOM.append(searchRowContainer, $('.open-keybindings-json')); - const openKeybindingsJsonButton = this._register(new Button(openKeybindingsJsonContainer, { secondary: true, title: true, ...defaultButtonStyles })); - openKeybindingsJsonButton.label = localize('openKeybindingsJson', "Edit as JSON"); - this._register(openKeybindingsJsonButton.onDidClick(() => this.preferencesService.openGlobalKeybindingSettings(true, { groupId: this.group.id }))); } private updateSearchOptions(): void { diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css index f93706d6120..96a94b07cde 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css @@ -23,13 +23,11 @@ padding: 0px 10px 11px 0; } -.keybindings-editor > .keybindings-header > .search-row-container > .search-container { +.keybindings-editor > .keybindings-header > .search-container { position: relative; - flex: 1; - min-width: 0; } -.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container { +.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container { position: absolute; top: 0; right: 10px; @@ -37,22 +35,22 @@ display: flex; } -.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container > .recording-badge { +.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container > .recording-badge { margin-right: 8px; padding: 4px; } -.keybindings-editor > .keybindings-header.small > .search-row-container > .search-container > .keybindings-search-actions-container > .recording-badge, -.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container > .recording-badge.disabled { +.keybindings-editor > .keybindings-header.small > .search-container > .keybindings-search-actions-container > .recording-badge, +.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container > .recording-badge.disabled { display: none; } -.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item > .icon { +.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item > .icon { width: 16px; height: 18px; } -.keybindings-editor > .keybindings-header > .search-row-container > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item { +.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item { margin-right: 4px; } @@ -88,21 +86,6 @@ opacity: 1; } -.keybindings-editor > .keybindings-header > .search-row-container { - display: flex; - align-items: center; - gap: 8px; -} - -.keybindings-editor > .keybindings-header > .search-row-container > .open-keybindings-json { - flex-shrink: 0; -} - -.keybindings-editor > .keybindings-header > .search-row-container > .open-keybindings-json > .monaco-button { - padding: 2px 8px; - line-height: 18px; -} - /** Table styling **/ .keybindings-editor > .keybindings-body .keybindings-table-container { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index b88b3073a9d..f2540882206 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -96,13 +96,6 @@ flex: auto; } -.settings-editor > .settings-header > .settings-header-controls > .settings-right-controls { - display: flex; - align-items: center; - gap: 8px; - padding-bottom: 4px; -} - .settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label { opacity: 0.9; border-radius: 0; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index c1bd907895e..3f491f41b3a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -835,6 +835,12 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon group: 'navigation', order: 1, }, + { + id: MenuId.ModalEditorEditorTitle, + when: ResourceContextKey.Resource.isEqualTo(that.userDataProfileService.currentProfile.keybindingsResource.toString()), + group: 'navigation', + order: 1, + }, { id: MenuId.GlobalActivity, group: '2_configuration', @@ -883,6 +889,11 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: MenuId.EditorTitle, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), group: 'navigation', + }, + { + id: MenuId.ModalEditorEditorTitle, + when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), + group: 'navigation', } ] }); @@ -1246,13 +1257,24 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon const commandId = '_workbench.openWorkspaceSettingsEditor'; if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE && !CommandsRegistry.getCommand(commandId)) { CommandsRegistry.registerCommand(commandId, () => this.preferencesService.openWorkspaceSettings({ jsonEditor: false })); + const when = ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.preferencesService.workspaceSettingsResource!.toString()), WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.not('isInDiffEditor')); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: commandId, title: OPEN_USER_SETTINGS_UI_TITLE, icon: preferencesOpenSettingsIcon }, - when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.preferencesService.workspaceSettingsResource!.toString()), WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.not('isInDiffEditor')), + when, + group: 'navigation', + order: 1 + }); + MenuRegistry.appendMenuItem(MenuId.ModalEditorEditorTitle, { + command: { + id: commandId, + title: OPEN_USER_SETTINGS_UI_TITLE, + icon: preferencesOpenSettingsIcon + }, + when, group: 'navigation', order: 1 }); @@ -1272,13 +1294,24 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon return this.preferencesService.openFolderSettings({ folderUri: folder.uri, jsonEditor: false, groupId }); } }); + const when = ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.preferencesService.getFolderSettingsResource(folder.uri)!.toString()), ContextKeyExpr.not('isInDiffEditor')); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: commandId, title: OPEN_USER_SETTINGS_UI_TITLE, icon: preferencesOpenSettingsIcon }, - when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.preferencesService.getFolderSettingsResource(folder.uri)!.toString()), ContextKeyExpr.not('isInDiffEditor')), + when, + group: 'navigation', + order: 1 + }); + MenuRegistry.appendMenuItem(MenuId.ModalEditorEditorTitle, { + command: { + id: commandId, + title: OPEN_USER_SETTINGS_UI_TITLE, + icon: preferencesOpenSettingsIcon + }, + when, group: 'navigation', order: 1 }); @@ -1320,6 +1353,11 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo when: openUserSettingsEditorWhen, group: 'navigation', order: 1 + }, { + id: MenuId.ModalEditorEditorTitle, + when: openUserSettingsEditorWhen, + group: 'navigation', + order: 1 }] }); } @@ -1349,6 +1387,11 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo when: openSettingsJsonWhen, group: 'navigation', order: 1 + }, { + id: MenuId.ModalEditorEditorTitle, + when: openSettingsJsonWhen, + group: 'navigation', + order: 1 }] }); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 76b4bbb9c16..17e5663deb8 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -783,15 +783,8 @@ export class SettingsEditor2 extends EditorPane { } })); - const headerRightControlsContainer = DOM.append(headerControlsContainer, $('.settings-right-controls')); - - const openSettingsJsonContainer = DOM.append(headerRightControlsContainer, $('.open-settings-json')); - const openSettingsJsonButton = this._register(new Button(openSettingsJsonContainer, { secondary: true, title: true, ...defaultButtonStyles })); - openSettingsJsonButton.label = localize('openSettingsJson', "Edit as JSON"); - this._register(openSettingsJsonButton.onDidClick(() => this.openSettingsFile())); - if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerRightControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -2163,9 +2156,10 @@ class SyncControls extends Disposable { ) { super(); - const turnOnSyncButtonContainer = DOM.append(container, $('.turn-on-sync')); + const headerRightControlsContainer = DOM.append(container, $('.settings-right-controls')); + const turnOnSyncButtonContainer = DOM.append(headerRightControlsContainer, $('.turn-on-sync')); this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true, ...defaultButtonStyles })); - this.lastSyncedLabel = DOM.append(container, $('.last-synced-label')); + this.lastSyncedLabel = DOM.append(headerRightControlsContainer, $('.last-synced-label')); DOM.hide(this.lastSyncedLabel); this.turnOnSyncButton.enabled = true; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 3fb7f02494e..e9e6d9d2cca 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -52,6 +52,11 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.EditorTitle, description: localize('menus.editorTitle', "The editor title menu") }, + { + key: 'modalEditor/editorTitle', + id: MenuId.ModalEditorEditorTitle, + description: localize('menus.modalEditorEditorTitle', "The editor title menu in the modal editor") + }, { key: 'editor/title/run', id: MenuId.EditorTitleRun, diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 839e48159e6..29189be6086 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -579,6 +579,16 @@ export class EditorService extends Disposable implements EditorServiceImpl { } } + // Modal group: override `preserveFocus` to move focus into the modal because there is nothing to preserve if this is the first modal editor + if ( + options?.preserveFocus && + this.editorGroupService.activeModalEditorPart?.groups.some(modalGroup => modalGroup.id === group.id) && + this.editorGroupService.activeModalEditorPart.count === 1 && + this.editorGroupService.activeModalEditorPart.groups[0].isEmpty + ) { + options = { ...options, preserveFocus: false }; + } + return group.openEditor(typedEditor, options); } @@ -637,6 +647,16 @@ export class EditorService extends Disposable implements EditorServiceImpl { } } + // Modal group: override `preserveFocus` to move focus into the modal there is nothing to preserve if this is the first modal editor + if ( + typedEditor.options?.preserveFocus && + this.editorGroupService.activeModalEditorPart?.groups.some(modalGroup => modalGroup.id === group.id) && + this.editorGroupService.activeModalEditorPart.count === 1 && + this.editorGroupService.activeModalEditorPart.groups[0].isEmpty + ) { + typedEditor = { ...typedEditor, options: { ...typedEditor.options, preserveFocus: false } }; + } + // Update map of groups to editors let targetGroupEditors = mapGroupToTypedEditors.get(group); if (!targetGroupEditors) { diff --git a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts index 4b65d8b11ce..5538bf425da 100644 --- a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts +++ b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts @@ -14,10 +14,11 @@ import { MockScopableContextKeyService } from '../../../../../platform/keybindin import { SideBySideEditorInput } from '../../../../common/editor/sideBySideEditorInput.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { MODAL_GROUP, MODAL_GROUP_TYPE } from '../../common/editorService.js'; +import { IEditorService, MODAL_GROUP, MODAL_GROUP_TYPE } from '../../common/editorService.js'; import { findGroup } from '../../common/editorGroupFinder.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { EditorService } from '../../browser/editorService.js'; suite('Modal Editor Group', () => { @@ -643,5 +644,23 @@ suite('Modal Editor Group', () => { assert.strictEqual(parts.activeModalEditorPart, undefined); }); + test('openEditor with MODAL_GROUP ignores preserveFocus', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + const editorService = disposables.add(instantiationService.createInstance(EditorService, undefined)); + instantiationService.stub(IEditorService, editorService); + + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const pane = await editorService.openEditor(input, { pinned: true, preserveFocus: true }, MODAL_GROUP); + + assert.ok(pane); + assert.strictEqual(pane.options?.preserveFocus, false); + + parts.activeModalEditorPart?.close(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts index 789f54a5c3e..2b098ab3158 100644 --- a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts @@ -108,7 +108,7 @@ function renderSessionItem(ctx: ComponentFixtureContext, session: IAgentSession, markdownRendererService.setDefaultCodeBlockRenderer(instantiationService.createInstance(EditorMarkdownCodeBlockRenderer)); const renderer = disposableStore.add( - instantiationService.createInstance(AgentSessionRenderer, rendererOptions, approvalModel ?? undefined) + instantiationService.createInstance(AgentSessionRenderer, rendererOptions, approvalModel ?? undefined, observableValue('activeSessionResource', undefined)) ); container.style.width = '350px'; From 6bb50a9e22c68f6f5b74918115deb66e91c1b693 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:16:16 -0800 Subject: [PATCH 298/448] fix: accept Azure DevOps Git URLs without .git suffix in plugin marketplace (#299576) * Initial plan * fix: accept Azure DevOps Git URLs without .git suffix in plugin marketplace - Remove the `.git` suffix requirement from `normalizeGitRepoPath`; the function now accepts any URL path that has at least two segments. - Update `parseUriMarketplaceReference` to handle paths with and without `.git`: - `cacheSegments` are built without the suffix in both cases. - `canonicalId` is always normalized to include `.git` so that the same repo specified with and without the suffix deduplicates correctly. - Add a JSDoc comment on `normalizeGitRepoPath` explaining the new semantics. - Update the test that expected HTTPS/SSH URLs without `.git` to be rejected; these are now accepted. SCP-style (`git@host:path`) still requires `.git`. - Add tests for Azure DevOps-style URLs and cross-suffix deduplication. Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> * refactor: use gitSuffix constant instead of magic number -4 Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --- .../plugins/pluginMarketplaceService.ts | 24 +++++++++---- .../plugins/pluginMarketplaceService.test.ts | 35 +++++++++++++++++-- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index 86054f48450..8fdfc8a3ba6 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -639,13 +639,18 @@ function parseUriMarketplaceReference(rawValue: string): IMarketplaceReference | return undefined; } + const gitSuffix = '.git'; const sanitizedAuthority = sanitizePathSegment(uri.authority.toLowerCase()); - const pathSegments = normalizedPath.slice(1, -4).split('/').map(sanitizePathSegment); + const pathHasGitSuffix = normalizedPath.toLowerCase().endsWith(gitSuffix); + const pathWithoutGit = pathHasGitSuffix ? normalizedPath.slice(1, normalizedPath.length - gitSuffix.length) : normalizedPath.slice(1); + const pathSegments = pathWithoutGit.split('/').map(sanitizePathSegment); + // Always normalize the canonical path to include .git so that URLs with and without the suffix deduplicate. + const canonicalPath = pathHasGitSuffix ? normalizedPath.slice(1).toLowerCase() : `${normalizedPath.slice(1).toLowerCase()}${gitSuffix}`; return { rawValue, displayLabel: rawValue, cloneUrl: rawValue, - canonicalId: `git:${uri.authority.toLowerCase()}/${normalizedPath.slice(1).toLowerCase()}`, + canonicalId: `git:${uri.authority.toLowerCase()}/${canonicalPath}`, cacheSegments: [sanitizedAuthority, ...pathSegments], kind: MarketplaceReferenceKind.GitUri, }; @@ -674,14 +679,21 @@ function parseScpMarketplaceReference(rawValue: string): IMarketplaceReference | }; } +/** + * Normalizes a Git repository path and validates that it has at least two segments + * (i.e., at least one owner/repo pair below the root). Accepts paths with or without + * a `.git` suffix — the suffix is preserved in the returned value so callers can decide + * how to treat it. + */ function normalizeGitRepoPath(path: string): string | undefined { + const gitSuffix = '.git'; const trimmed = path.replace(/\/+/g, '/').replace(/\/+$/g, ''); - if (!trimmed.toLowerCase().endsWith('.git')) { - return undefined; - } const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`; - const pathWithoutGit = withLeadingSlash.slice(1, -4); + // Strip .git suffix (if present) only for the purposes of validating path depth. + const pathWithoutGit = withLeadingSlash.toLowerCase().endsWith(gitSuffix) + ? withLeadingSlash.slice(1, withLeadingSlash.length - gitSuffix.length) + : withLeadingSlash.slice(1); if (!pathWithoutGit || !pathWithoutGit.includes('/')) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts index da94ecb5615..879e3a33850 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts @@ -76,12 +76,41 @@ suite('PluginMarketplaceService', () => { assert.deepStrictEqual(parsed.cacheSegments, []); }); - test('rejects non-shorthand marketplace entries without .git', () => { - assert.strictEqual(parseMarketplaceReference('https://example.com/org/repo'), undefined); - assert.strictEqual(parseMarketplaceReference('ssh://git@example.com/org/repo'), undefined); + test('accepts HTTPS and SSH marketplace entries without .git suffix', () => { + const https = parseMarketplaceReference('https://example.com/org/repo'); + assert.ok(https); + assert.strictEqual(https?.kind, MarketplaceReferenceKind.GitUri); + assert.strictEqual(https?.canonicalId, 'git:example.com/org/repo.git'); + assert.deepStrictEqual(https?.cacheSegments, ['example.com', 'org', 'repo']); + + const ssh = parseMarketplaceReference('ssh://git@example.com/org/repo'); + assert.ok(ssh); + assert.strictEqual(ssh?.kind, MarketplaceReferenceKind.GitUri); + assert.strictEqual(ssh?.canonicalId, 'git:git@example.com/org/repo.git'); + + // SCP-style (git@host:path) still requires .git because the colon-path syntax is + // unambiguous only for traditional git SSH URLs where .git is conventional. assert.strictEqual(parseMarketplaceReference('git@example.com:org/repo'), undefined); }); + test('parses Azure DevOps HTTPS clone URLs without .git suffix', () => { + const parsed = parseMarketplaceReference('https://dev.azure.com/org/project/_git/repo'); + assert.ok(parsed); + assert.strictEqual(parsed?.kind, MarketplaceReferenceKind.GitUri); + assert.strictEqual(parsed?.cloneUrl, 'https://dev.azure.com/org/project/_git/repo'); + assert.strictEqual(parsed?.canonicalId, 'git:dev.azure.com/org/project/_git/repo.git'); + assert.deepStrictEqual(parsed?.cacheSegments, ['dev.azure.com', 'org', 'project', '_git', 'repo']); + }); + + test('deduplicates Azure DevOps URLs with and without .git suffix', () => { + const parsed = parseMarketplaceReferences([ + 'https://dev.azure.com/org/project/_git/repo', + 'https://dev.azure.com/org/project/_git/repo.git', + ]); + assert.strictEqual(parsed.length, 1); + assert.strictEqual(parsed[0].canonicalId, 'git:dev.azure.com/org/project/_git/repo.git'); + }); + test('parses HTTPS URI with trailing slash after .git', () => { const parsed = parseMarketplaceReference('https://example.com/org/repo.git/'); assert.ok(parsed); From 8734c3f392da7840223e7e592105f55287bc7ddf Mon Sep 17 00:00:00 2001 From: Sergei Druzhkov Date: Fri, 6 Mar 2026 20:16:30 +0300 Subject: [PATCH 299/448] debug: fix variable updating after set response (#299473) --- src/vs/workbench/contrib/debug/common/debugModel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 9c943348a97..5de69ed78ca 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -246,7 +246,8 @@ function handleSetResponse(expression: ExpressionContainer, response: DebugProto expression.reference = response.body.variablesReference; expression.namedVariables = response.body.namedVariables; expression.indexedVariables = response.body.indexedVariables; - // todo @weinand: the set responses contain most properties, but not memory references. Should they? + expression.memoryReference = response.body.memoryReference; + expression.valueLocationReference = response.body.valueLocationReference; } } From 3b4da4f334931ae2da5691def65229c566d15272 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 6 Mar 2026 17:10:23 +0100 Subject: [PATCH 300/448] =?UTF-8?q?Revert=20"chore=20-=20Refactor=20inline?= =?UTF-8?q?=20chat=20classes=20to=20use=20private=20class=20fields=20(#29?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 81f2b5cd2fdf2c2ceb61899f79332db8551f2c35. --- .../inlineChat/browser/inlineChatActions.ts | 11 +- .../browser/inlineChatController.ts | 297 ++++++++--------- .../browser/inlineChatEditorAffordance.ts | 112 ++++--- .../browser/inlineChatGutterAffordance.ts | 6 +- .../inlineChat/browser/inlineChatNotebook.ts | 6 +- .../browser/inlineChatOverlayWidget.ts | 299 +++++++++--------- .../browser/inlineChatSessionServiceImpl.ts | 87 +++-- .../inlineChat/browser/inlineChatWidget.ts | 178 +++++------ .../browser/inlineChatZoneWidget.ts | 75 ++--- .../test/browser/testWorkerService.ts | 24 +- 10 files changed, 503 insertions(+), 592 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 204ae20da8d..aef9aeefc52 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -91,11 +91,11 @@ export class StartSessionAction extends Action2 { logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize()); return; } - return this.#runEditorCommand(editorAccessor, editor, ...args); + return this._runEditorCommand(editorAccessor, editor, ...args); }); } - async #runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { + private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { const configServce = accessor.get(IConfigurationService); @@ -262,15 +262,12 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction { class KeepOrUndoSessionAction extends AbstractInlineChatAction { - readonly #keep: boolean; - - constructor(keep: boolean, desc: IAction2Options) { + constructor(private readonly _keep: boolean, desc: IAction2Options) { super(desc); - this.#keep = keep; } override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise { - if (this.#keep) { + if (this._keep) { await ctrl.acceptSession(); } else { await ctrl.rejectSession(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 869084cff1d..76aa39da942 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -112,93 +112,63 @@ export class InlineChatController implements IEditorContribution { * Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session. * When set, this takes priority over the inlineChat.defaultModel setting. */ - static #userSelectedModel: string | undefined; + private static _userSelectedModel: string | undefined; - readonly #store = new DisposableStore(); - readonly #isActiveController = observableValue(this, false); - readonly #renderMode: IObservable<'zone' | 'hover'>; - readonly #zone: Lazy; + private readonly _store = new DisposableStore(); + private readonly _isActiveController = observableValue(this, false); + private readonly _renderMode: IObservable<'zone' | 'hover'>; + private readonly _zone: Lazy; readonly inputOverlayWidget: InlineChatAffordance; - readonly #inputWidget: InlineChatInputWidget; + private readonly _inputWidget: InlineChatInputWidget; - readonly #currentSession: IObservable; - - readonly #editor: ICodeEditor; - readonly #instaService: IInstantiationService; - readonly #notebookEditorService: INotebookEditorService; - readonly #inlineChatSessionService: IInlineChatSessionService; - readonly #configurationService: IConfigurationService; - readonly #webContentExtractorService: ISharedWebContentExtractorService; - readonly #fileService: IFileService; - readonly #chatAttachmentResolveService: IChatAttachmentResolveService; - readonly #editorService: IEditorService; - readonly #markerDecorationsService: IMarkerDecorationsService; - readonly #languageModelService: ILanguageModelsService; - readonly #logService: ILogService; - readonly #chatEditingService: IChatEditingService; - readonly #chatService: IChatService; + private readonly _currentSession: IObservable; get widget(): EditorBasedInlineChatWidget { - return this.#zone.value.widget; + return this._zone.value.widget; } get isActive() { - return Boolean(this.#currentSession.get()); + return Boolean(this._currentSession.get()); } get inputWidget(): InlineChatInputWidget { - return this.#inputWidget; + return this._inputWidget; } constructor( - editor: ICodeEditor, - @IInstantiationService instaService: IInstantiationService, - @INotebookEditorService notebookEditorService: INotebookEditorService, - @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, + @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService, - @ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService, - @IFileService fileService: IFileService, - @IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService, - @IEditorService editorService: IEditorService, - @IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService, - @ILanguageModelsService languageModelService: ILanguageModelsService, - @ILogService logService: ILogService, - @IChatEditingService chatEditingService: IChatEditingService, - @IChatService chatService: IChatService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, + @IFileService private readonly _fileService: IFileService, + @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService, + @IEditorService private readonly _editorService: IEditorService, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, + @ILogService private readonly _logService: ILogService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IChatService private readonly _chatService: IChatService, ) { - this.#editor = editor; - this.#instaService = instaService; - this.#notebookEditorService = notebookEditorService; - this.#inlineChatSessionService = inlineChatSessionService; - this.#configurationService = configurationService; - this.#webContentExtractorService = webContentExtractorService; - this.#fileService = fileService; - this.#chatAttachmentResolveService = chatAttachmentResolveService; - this.#editorService = editorService; - this.#markerDecorationsService = markerDecorationsService; - this.#languageModelService = languageModelService; - this.#logService = logService; - this.#chatEditingService = chatEditingService; - this.#chatService = chatService; - - const editorObs = observableCodeEditor(editor); + const editorObs = observableCodeEditor(_editor); const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const ctxPendingConfirmation = CTX_INLINE_CHAT_PENDING_CONFIRMATION.bindTo(contextKeyService); - const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService); - this.#renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this.#configurationService); + const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); + this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); // Track whether the current editor's file is being edited by any chat editing session - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const model = editorObs.model.read(r); if (!model) { ctxFileBelongsToChat.set(false); return; } - const sessions = this.#chatEditingService.editingSessionsObs.read(r); + const sessions = this._chatEditingService.editingSessionsObs.read(r); let hasEdits = false; for (const session of sessions) { const entries = session.entries.read(r); @@ -215,25 +185,25 @@ export class InlineChatController implements IEditorContribution { ctxFileBelongsToChat.set(hasEdits); })); - const overlayWidget = this.#inputWidget = this.#store.add(this.#instaService.createInstance(InlineChatInputWidget, editorObs)); - const sessionOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); - this.inputOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatAffordance, this.#editor, overlayWidget)); + const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); + const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); + this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); - this.#zone = new Lazy(() => { + this._zone = new Lazy(() => { - assertType(this.#editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); + assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); const location: IChatWidgetLocationOptions = { location: ChatAgentLocation.EditorInline, resolveData: () => { - assertType(this.#editor.hasModel()); - const wholeRange = this.#editor.getSelection(); - const document = this.#editor.getModel().uri; + assertType(this._editor.hasModel()); + const wholeRange = this._editor.getSelection(); + const document = this._editor.getModel().uri; return { type: ChatAgentLocation.EditorInline, - id: getEditorId(this.#editor, this.#editor.getModel()), - selection: this.#editor.getSelection(), + id: getEditorId(this._editor, this._editor.getModel()), + selection: this._editor.getSelection(), document, wholeRange }; @@ -243,22 +213,22 @@ export class InlineChatController implements IEditorContribution { // inline chat in notebooks // check if this editor is part of a notebook editor // if so, update the location and use the notebook specific widget - const notebookEditor = this.#notebookEditorService.getNotebookForPossibleCell(this.#editor); + const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor); if (!!notebookEditor) { location.location = ChatAgentLocation.Notebook; if (notebookAgentConfig.get()) { location.resolveData = () => { - assertType(this.#editor.hasModel()); + assertType(this._editor.hasModel()); return { type: ChatAgentLocation.Notebook, - sessionInputUri: this.#editor.getModel().uri, + sessionInputUri: this._editor.getModel().uri, }; }; } } - const result = this.#instaService.createInstance(InlineChatZoneWidget, + const result = this._instaService.createInstance(InlineChatZoneWidget, location, { enableWorkingSet: 'implicit', @@ -278,33 +248,33 @@ export class InlineChatController implements IEditorContribution { }, defaultMode: ChatMode.Ask }, - { editor: this.#editor, notebookEditor }, + { editor: this._editor, notebookEditor }, () => Promise.resolve(), ); - this.#store.add(result); + this._store.add(result); result.domNode.classList.add('inline-chat-2'); return result; }); - const sessionsSignal = observableSignalFromEvent(this, this.#inlineChatSessionService.onDidChangeSessions); + const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); - this.#currentSession = derived(r => { + this._currentSession = derived(r => { sessionsSignal.read(r); const model = editorObs.model.read(r); - const session = model && this.#inlineChatSessionService.getSessionByTextModel(model.uri); + const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri); return session ?? undefined; }); let lastSession: IInlineChatSession2 | undefined = undefined; - this.#store.add(autorun(r => { - const session = this.#currentSession.read(r); + this._store.add(autorun(r => { + const session = this._currentSession.read(r); if (!session) { - this.#isActiveController.set(false, undefined); + this._isActiveController.set(false, undefined); if (lastSession && !lastSession.chatModel.hasRequests) { const state = lastSession.chatModel.inputModel.state.read(undefined); @@ -320,24 +290,23 @@ export class InlineChatController implements IEditorContribution { let foundOne = false; for (const editor of codeEditorService.listCodeEditors()) { - const ctrl = InlineChatController.get(editor); - if (ctrl && ctrl.#isActiveController.read(undefined)) { + if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) { foundOne = true; break; } } if (!foundOne && editorObs.isFocused.read(r)) { - this.#isActiveController.set(true, undefined); + this._isActiveController.set(true, undefined); } })); const visibleSessionObs = observableValue(this, undefined); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const model = editorObs.model.read(r); - const session = this.#currentSession.read(r); - const isActive = this.#isActiveController.read(r); + const session = this._currentSession.read(r); + const isActive = this._isActiveController.read(r); if (!session || !isActive || !model) { visibleSessionObs.set(undefined, undefined); @@ -353,38 +322,38 @@ export class InlineChatController implements IEditorContribution { }); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { // HIDE/SHOW const session = visibleSessionObs.read(r); - const renderMode = this.#renderMode.read(r); + const renderMode = this._renderMode.read(r); if (!session) { - this.#zone.rawValue?.hide(); - this.#zone.rawValue?.widget.chatWidget.setModel(undefined); - this.#editor.focus(); + this._zone.rawValue?.hide(); + this._zone.rawValue?.widget.chatWidget.setModel(undefined); + _editor.focus(); ctxInlineChatVisible.reset(); } else if (renderMode === 'hover') { // hover mode: set model but don't show zone, keep focus in editor - this.#zone.value.widget.chatWidget.setModel(session.chatModel); - this.#zone.rawValue?.hide(); + this._zone.value.widget.chatWidget.setModel(session.chatModel); + this._zone.rawValue?.hide(); ctxInlineChatVisible.set(true); } else { ctxInlineChatVisible.set(true); - this.#zone.value.widget.chatWidget.setModel(session.chatModel); - if (!this.#zone.value.position) { - this.#zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); - this.#zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug - this.#zone.value.show(session.initialPosition); + this._zone.value.widget.chatWidget.setModel(session.chatModel); + if (!this._zone.value.position) { + this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); + this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug + this._zone.value.show(session.initialPosition); } - this.#zone.value.reveal(this.#zone.value.position!); - this.#zone.value.widget.focus(); + this._zone.value.reveal(this._zone.value.position!); + this._zone.value.widget.focus(); } })); // Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const session = visibleSessionObs.read(r); - const renderMode = this.#renderMode.read(r); + const renderMode = this._renderMode.read(r); if (!session || renderMode !== 'hover') { ctxPendingConfirmation.set(false); sessionOverlayWidget.hide(); @@ -406,7 +375,7 @@ export class InlineChatController implements IEditorContribution { } })); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const session = visibleSessionObs.read(r); if (session) { const entries = session.editingSession.entries.read(r); @@ -424,7 +393,7 @@ export class InlineChatController implements IEditorContribution { for (const entry of otherEntries) { // OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend // that modifies other files - this.#editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); + this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); } } })); @@ -445,36 +414,36 @@ export class InlineChatController implements IEditorContribution { }); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const response = lastResponseObs.read(r); - this.#zone.rawValue?.widget.updateInfo(''); + this._zone.rawValue?.widget.updateInfo(''); if (!response?.isInProgress.read(r)) { if (response?.result?.errorDetails) { // ERROR case - this.#zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); + this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); alert(response.result.errorDetails.message); } // no response or not in progress - this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); - this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); + this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); + this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); } else { - this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); + this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); let placeholder = response.request?.message.text; const lastProgress = lastResponseProgressObs.read(r); if (lastProgress) { placeholder = renderAsPlaintext(lastProgress.content); } - this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); + this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); } })); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const session = visibleSessionObs.read(r); if (!session) { return; @@ -487,25 +456,25 @@ export class InlineChatController implements IEditorContribution { })); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const session = visibleSessionObs.read(r); const entry = session?.editingSession.readEntry(session.uri, r); // make sure there is an editor integration - const pane = this.#editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this.#editor || isNotebookWithCellEditor(candidate, this.#editor)); + const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor)); if (pane && entry) { entry?.getEditorIntegration(pane); } // make sure the ZONE isn't inbetween a diff and move above if so - if (entry?.diffInfo && this.#zone.value.position) { - const { position } = this.#zone.value; + if (entry?.diffInfo && this._zone.value.position) { + const { position } = this._zone.value; const diff = entry.diffInfo.read(r); for (const change of diff.changes) { if (change.modified.contains(position.lineNumber)) { - this.#zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1)); + this._zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1)); break; } } @@ -514,90 +483,90 @@ export class InlineChatController implements IEditorContribution { } dispose(): void { - this.#store.dispose(); + this._store.dispose(); } getWidgetPosition(): Position | undefined { - return this.#zone.rawValue?.position; + return this._zone.rawValue?.position; } focus() { - this.#zone.rawValue?.widget.focus(); + this._zone.rawValue?.widget.focus(); } async run(arg?: InlineChatRunOptions): Promise { - assertType(this.#editor.hasModel()); - const uri = this.#editor.getModel().uri; + assertType(this._editor.hasModel()); + const uri = this._editor.getModel().uri; - const existingSession = this.#inlineChatSessionService.getSessionByTextModel(uri); + const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri); if (existingSession) { await existingSession.editingSession.accept(); existingSession.dispose(); } - this.#isActiveController.set(true, undefined); + this._isActiveController.set(true, undefined); - const session = this.#inlineChatSessionService.createSession(this.#editor); + const session = this._inlineChatSessionService.createSession(this._editor); // Store for tracking model changes during this session const sessionStore = new DisposableStore(); try { - await this.#applyModelDefaults(session, sessionStore); + await this._applyModelDefaults(session, sessionStore); if (arg) { - arg.attachDiagnostics ??= this.#configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone'; + arg.attachDiagnostics ??= this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone'; } // ADD diagnostics (only when explicitly requested) if (arg?.attachDiagnostics) { const entries: IChatRequestVariableEntry[] = []; - for (const [range, marker] of this.#markerDecorationsService.getLiveMarkers(uri)) { - if (range.intersectRanges(this.#editor.getSelection())) { + for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) { + if (range.intersectRanges(this._editor.getSelection())) { const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter)); } } if (entries.length > 0) { - this.#zone.value.widget.chatWidget.attachmentModel.addContext(...entries); + this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries); const msg = entries.length > 1 ? localize('fixN', "Fix the attached problems") : localize('fix1', "Fix the attached problem"); - this.#zone.value.widget.chatWidget.input.setValue(msg, true); + this._zone.value.widget.chatWidget.input.setValue(msg, true); arg.message = msg; - this.#zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } } // Check args if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { if (arg.initialRange) { - this.#editor.revealRange(arg.initialRange); + this._editor.revealRange(arg.initialRange); } if (arg.initialSelection) { - this.#editor.setSelection(arg.initialSelection); + this._editor.setSelection(arg.initialSelection); } if (arg.attachments) { await Promise.all(arg.attachments.map(async attachment => { - await this.#zone.value.widget.chatWidget.attachmentModel.addFile(attachment); + await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment); })); delete arg.attachments; } if (arg.modelSelector) { - const id = (await this.#languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); + const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); if (!id) { throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`); } - const model = this.#languageModelService.lookupLanguageModel(id); + const model = this._languageModelService.lookupLanguageModel(id); if (!model) { throw new Error(`Language model not loaded: ${id}.`); } - this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); + this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); } if (arg.message) { - this.#zone.value.widget.chatWidget.setInput(arg.message); + this._zone.value.widget.chatWidget.setInput(arg.message); if (arg.autoSend) { - await this.#zone.value.widget.chatWidget.acceptInput(); + await this._zone.value.widget.chatWidget.acceptInput(); } } } @@ -623,7 +592,7 @@ export class InlineChatController implements IEditorContribution { } async acceptSession() { - const session = this.#currentSession.get(); + const session = this._currentSession.get(); if (!session) { return; } @@ -632,23 +601,23 @@ export class InlineChatController implements IEditorContribution { } async rejectSession() { - const session = this.#currentSession.get(); + const session = this._currentSession.get(); if (!session) { return; } - this.#chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); + this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); await session.editingSession.reject(); session.dispose(); } - async #selectVendorDefaultModel(session: IInlineChatSession2): Promise { - const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get(); + private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise { + const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get(); if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { - const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); + const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); for (const identifier of ids) { - const candidate = this.#languageModelService.lookupLanguageModel(identifier); + const candidate = this._languageModelService.lookupLanguageModel(identifier); if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) { - this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); + this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); break; } } @@ -659,39 +628,39 @@ export class InlineChatController implements IEditorContribution { * Applies model defaults based on settings and tracks user model changes. * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ - async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { - const userSelectedModel = InlineChatController.#userSelectedModel; - const defaultModelSetting = this.#configurationService.getValue(InlineChatConfigKeys.DefaultModel); + private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { + const userSelectedModel = InlineChatController._userSelectedModel; + const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); let modelApplied = false; // 1. Try user's explicitly chosen model from a previous inline chat in the same session if (userSelectedModel) { - modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); + modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); if (!modelApplied) { // User's previously selected model is no longer available, clear it - InlineChatController.#userSelectedModel = undefined; + InlineChatController._userSelectedModel = undefined; } } // 2. Try inlineChat.defaultModel setting if (!modelApplied && defaultModelSetting) { - modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); + modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); if (!modelApplied) { - this.#logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); + this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); } } // 3. Fall back to vendor default if (!modelApplied) { - await this.#selectVendorDefaultModel(session); + await this._selectVendorDefaultModel(session); } // Track model changes - store user's explicit choice in the given sessions. // NOTE: This currently detects any model change, not just user-initiated ones. let initialModelId: string | undefined; sessionStore.add(autorun(r => { - const newModel = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); + const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); if (!newModel) { return; } @@ -701,25 +670,25 @@ export class InlineChatController implements IEditorContribution { } if (initialModelId !== newModel.identifier) { // User explicitly changed model, store their choice as qualified name - InlineChatController.#userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); + InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); initialModelId = newModel.identifier; } })); } async createImageAttachment(attachment: URI): Promise { - const value = this.#currentSession.get(); + const value = this._currentSession.get(); if (!value) { return undefined; } if (attachment.scheme === Schemas.file) { - if (await this.#fileService.canHandleResource(attachment)) { - return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); + if (await this._fileService.canHandleResource(attachment)) { + return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); } } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None); + const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); if (extractedImages) { - return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); + return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); } } return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 361441642d6..e7773395fce 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -33,13 +33,12 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j class QuickFixActionViewItem extends MenuEntryActionViewItem { - readonly #lightBulbStore = this._store.add(new MutableDisposable()); - readonly #editor: ICodeEditor; - #currentTitle: string | undefined; + private readonly _lightBulbStore = this._store.add(new MutableDisposable()); + private _currentTitle: string | undefined; constructor( action: MenuItemAction, - editor: ICodeEditor, + private readonly _editor: ICodeEditor, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -56,7 +55,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { elementGetter: () => HTMLElement | undefined = () => undefined; override async run(...args: unknown[]): Promise { - const controller = CodeActionController.get(editor); + const controller = CodeActionController.get(_editor); const info = controller?.lightBulbState.get(); const element = this.elementGetter(); if (controller && info && element) { @@ -68,27 +67,26 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { super(wrappedAction, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); - this.#editor = editor; wrappedAction.elementGetter = () => this.element; } override render(container: HTMLElement): void { super.render(container); - this.#updateFromLightBulb(); + this._updateFromLightBulb(); } protected override getTooltip(): string { - return this.#currentTitle ?? super.getTooltip(); + return this._currentTitle ?? super.getTooltip(); } - #updateFromLightBulb(): void { - const controller = CodeActionController.get(this.#editor); + private _updateFromLightBulb(): void { + const controller = CodeActionController.get(this._editor); if (!controller) { return; } const store = new DisposableStore(); - this.#lightBulbStore.value = store; + this._lightBulbStore.value = store; store.add(autorun(reader => { const info = controller.lightBulbState.read(reader); @@ -101,7 +99,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { } // Update tooltip - this.#currentTitle = info?.title; + this._currentTitle = info?.title; this.updateTooltip(); })); } @@ -109,7 +107,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { - readonly #kbLabel: string | undefined; + private readonly _kbLabel: string | undefined; constructor( action: MenuItemAction, @@ -123,14 +121,14 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); this.options.label = true; this.options.icon = false; - this.#kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined; + this._kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined; } protected override updateLabel(): void { if (this.label) { dom.reset(this.label, this.action.label, - ...(this.#kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this.#kbLabel)] : []) + ...(this._kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this._kbLabel)] : []) ); } } @@ -142,42 +140,38 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { */ export class InlineChatEditorAffordance extends Disposable implements IContentWidget { - static #idPool = 0; + private static _idPool = 0; - readonly #id = `inline-chat-content-widget-${InlineChatEditorAffordance.#idPool++}`; - readonly #domNode: HTMLElement; - #position: IContentWidgetPosition | null = null; - #isVisible = false; + private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`; + private readonly _domNode: HTMLElement; + private _position: IContentWidgetPosition | null = null; + private _isVisible = false; - readonly #onDidRunAction = this._store.add(new Emitter()); - readonly onDidRunAction: Event = this.#onDidRunAction.event; + private readonly _onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this._onDidRunAction.event; readonly allowEditorOverflow = true; readonly suppressMouseDown = false; - readonly #editor: ICodeEditor; - constructor( - editor: ICodeEditor, + private readonly _editor: ICodeEditor, selection: IObservable, @IInstantiationService instantiationService: IInstantiationService, ) { super(); - this.#editor = editor; - // Create the widget DOM - this.#domNode = dom.$('.inline-chat-content-widget'); + this._domNode = dom.$('.inline-chat-content-widget'); // Create toolbar with the inline chat start action - const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#domNode, MenuId.InlineChatEditorAffordance, { + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, { telemetrySource: 'inlineChatEditorAffordance', hiddenItemStrategy: HiddenItemStrategy.Ignore, menuOptions: { renderShortTitle: true }, toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true }, actionViewItemProvider: (action: IAction) => { if (action instanceof MenuItemAction && action.id === quickFixCommandId) { - return instantiationService.createInstance(QuickFixActionViewItem, action, this.#editor); + return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); } if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT || action.id === 'inlineChat.fixDiagnostics')) { return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action); @@ -186,37 +180,37 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi } })); this._store.add(toolbar.actionRunner.onDidRun((e) => { - this.#onDidRunAction.fire(e.action.id); - this.#hide(); + this._onDidRunAction.fire(e.action.id); + this._hide(); })); this._store.add(autorun(r => { const sel = selection.read(r); if (sel) { - this.#show(sel); + this._show(sel); } else { - this.#hide(); + this._hide(); } })); } - #show(selection: Selection): void { + private _show(selection: Selection): void { if (selection.isEmpty()) { - this.#showAtLineStart(selection.getPosition().lineNumber); + this._showAtLineStart(selection.getPosition().lineNumber); } else { - this.#showAtSelection(selection); + this._showAtSelection(selection); } - if (this.#isVisible) { - this.#editor.layoutContentWidget(this); + if (this._isVisible) { + this._editor.layoutContentWidget(this); } else { - this.#editor.addContentWidget(this); - this.#isVisible = true; + this._editor.addContentWidget(this); + this._isVisible = true; } } - #showAtSelection(selection: Selection): void { + private _showAtSelection(selection: Selection): void { const cursorPosition = selection.getPosition(); const direction = selection.getDirection(); @@ -224,20 +218,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW; - this.#position = { + this._position = { position: cursorPosition, preference: [preference], }; } - #showAtLineStart(lineNumber: number): void { - const model = this.#editor.getModel(); + private _showAtLineStart(lineNumber: number): void { + const model = this._editor.getModel(); if (!model) { return; } const tabSize = model.getOptions().tabSize; - const fontInfo = this.#editor.getOptions().get(EditorOption.fontInfo); + const fontInfo = this._editor.getOptions().get(EditorOption.fontInfo); const lineContent = model.getLineContent(lineNumber); const indent = computeIndentLevel(lineContent, tabSize); const lineHasSpace = indent < 0 ? true : fontInfo.spaceWidth * indent > 22; @@ -260,43 +254,43 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi const effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1; - this.#position = { + this._position = { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: [ContentWidgetPositionPreference.EXACT], }; } - #hide(): void { - if (this.#isVisible) { - this.#isVisible = false; - this.#editor.removeContentWidget(this); + private _hide(): void { + if (this._isVisible) { + this._isVisible = false; + this._editor.removeContentWidget(this); } } getId(): string { - return this.#id; + return this._id; } getDomNode(): HTMLElement { - return this.#domNode; + return this._domNode; } getPosition(): IContentWidgetPosition | null { - return this.#position; + return this._position; } beforeRender(): IDimension | null { - const position = this.#editor.getPosition(); - const lineHeight = position ? this.#editor.getLineHeightForPosition(position) : this.#editor.getOption(EditorOption.lineHeight); + const position = this._editor.getPosition(); + const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight); - this.#domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); + this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); return null; } override dispose(): void { - if (this.#isVisible) { - this.#editor.removeContentWidget(this); + if (this._isVisible) { + this._editor.removeContentWidget(this); } super.dispose(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 03a77669046..3d82cec90ec 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -26,8 +26,8 @@ import { IUserInteractionService } from '../../../../platform/userInteraction/br export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { - readonly #onDidRunAction = this._store.add(new Emitter()); - readonly onDidRunAction: Event = this.#onDidRunAction.event; + private readonly _onDidRunAction = this._store.add(new Emitter()); + readonly onDidRunAction: Event = this._onDidRunAction.event; constructor( myEditorObs: ObservableCodeEditor, @@ -108,6 +108,6 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { this._store.add(menu); - this._store.add(this.onDidCloseWithCommand(commandId => this.#onDidRunAction.fire(commandId))); + this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index ca722843a32..539e8197ee0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -14,7 +14,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js export class InlineChatNotebookContribution { - readonly #store = new DisposableStore(); + private readonly _store = new DisposableStore(); constructor( @IInlineChatSessionService sessionService: IInlineChatSessionService, @@ -22,7 +22,7 @@ export class InlineChatNotebookContribution { @INotebookEditorService notebookEditorService: INotebookEditorService, ) { - this.#store.add(sessionService.onWillStartSession(newSessionEditor => { + this._store.add(sessionService.onWillStartSession(newSessionEditor => { const candidate = CellUri.parse(newSessionEditor.getModel().uri); if (!candidate) { return; @@ -51,6 +51,6 @@ export class InlineChatNotebookContribution { } dispose(): void { - this.#store.dispose(); + this._store.dispose(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 5112e2fe443..04a76d7327a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -44,58 +44,51 @@ import { assertType } from '../../../../base/common/types.js'; */ export class InlineChatInputWidget extends Disposable { - readonly #domNode: HTMLElement; - readonly #container: HTMLElement; - readonly #inputContainer: HTMLElement; - readonly #toolbarContainer: HTMLElement; - readonly #input: IActiveCodeEditor; - readonly #position = observableValue(this, null); - readonly position: IObservable = this.#position; + private readonly _domNode: HTMLElement; + private readonly _container: HTMLElement; + private readonly _inputContainer: HTMLElement; + private readonly _toolbarContainer: HTMLElement; + private readonly _input: IActiveCodeEditor; + private readonly _position = observableValue(this, null); + readonly position: IObservable = this._position; - readonly #showStore = this._store.add(new DisposableStore()); - readonly #stickyScrollHeight: IObservable; - readonly #layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; - #anchorLineNumber: number = 0; - #anchorLeft: number = 0; - #anchorAbove: boolean = false; + private readonly _showStore = this._store.add(new DisposableStore()); + private readonly _stickyScrollHeight: IObservable; + private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; + private _anchorLineNumber: number = 0; + private _anchorLeft: number = 0; + private _anchorAbove: boolean = false; - readonly #editorObs: ObservableCodeEditor; - readonly #contextKeyService: IContextKeyService; - readonly #menuService: IMenuService; constructor( - editorObs: ObservableCodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService menuService: IMenuService, + private readonly _editorObs: ObservableCodeEditor, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, ) { super(); - this.#editorObs = editorObs; - this.#contextKeyService = contextKeyService; - this.#menuService = menuService; - // Create container - this.#domNode = dom.$('.inline-chat-gutter-menu'); + this._domNode = dom.$('.inline-chat-gutter-menu'); // Create inner container (background + focus border) - this.#container = dom.append(this.#domNode, dom.$('.inline-chat-gutter-container')); + this._container = dom.append(this._domNode, dom.$('.inline-chat-gutter-container')); // Create input editor container - this.#inputContainer = dom.append(this.#container, dom.$('.input')); + this._inputContainer = dom.append(this._container, dom.$('.input')); // Create toolbar container - this.#toolbarContainer = dom.append(this.#container, dom.$('.toolbar')); + this._toolbarContainer = dom.append(this._container, dom.$('.toolbar')); // Create vertical actions bar below the input container - const actionsContainer = dom.append(this.#domNode, dom.$('.inline-chat-gutter-actions')); + const actionsContainer = dom.append(this._domNode, dom.$('.inline-chat-gutter-actions')); const actionBar = this._store.add(new ActionBar(actionsContainer, { orientation: ActionsOrientation.VERTICAL, preventLoopNavigation: true, })); - const actionsMenu = this._store.add(this.#menuService.createMenu(MenuId.ChatEditorInlineMenu, this.#contextKeyService)); + const actionsMenu = this._store.add(this._menuService.createMenu(MenuId.ChatEditorInlineMenu, this._contextKeyService)); const updateActions = () => { const actions = getFlatActionBarActions(actionsMenu.getActions({ shouldForwardArgs: true })); actionBar.clear(); @@ -130,13 +123,13 @@ export class InlineChatInputWidget extends Disposable { ]) }; - this.#input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this.#inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); - this.#input.setModel(model); + this._input.setModel(model); // Create toolbar - const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#toolbarContainer, MenuId.InlineChatInput, { + const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MenuId.InlineChatInput, { telemetrySource: 'inlineChatInput.toolbar', hiddenItemStrategy: HiddenItemStrategy.NoHide, toolbarOptions: { @@ -146,8 +139,8 @@ export class InlineChatInputWidget extends Disposable { })); // Initialize sticky scroll height observable - const stickyScrollController = StickyScrollController.get(this.#editorObs.editor); - this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + const stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); // Track toolbar width changes const toolbarWidth = observableValue(this, 0); @@ -157,24 +150,24 @@ export class InlineChatInputWidget extends Disposable { this._store.add(resizeObserver); this._store.add(resizeObserver.observe(toolbar.getElement())); - const contentWidth = observableFromEvent(this, this.#input.onDidChangeModelContent, () => this.#input.getContentWidth()); - const contentHeight = observableFromEvent(this, this.#input.onDidContentSizeChange, () => this.#input.getContentHeight()); + const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); + const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); - this.#layoutData = derived(r => { + this._layoutData = derived(r => { const editorPad = 6; const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); const minWidth = 220; const maxWidth = 600; - const clampedWidth = this.#input.getOption(EditorOption.wordWrap) === 'on' + const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' ? maxWidth : Math.max(minWidth, Math.min(totalWidth, maxWidth)); - const lineHeight = this.#input.getOption(EditorOption.lineHeight); + const lineHeight = this._input.getOption(EditorOption.lineHeight); const clampedHeight = Math.min(contentHeight.read(r), (3 * lineHeight)); if (totalWidth > clampedWidth) { // enable word wrap - this.#input.updateOptions({ wordWrap: 'on', }); + this._input.updateOptions({ wordWrap: 'on', }); } return { @@ -187,42 +180,42 @@ export class InlineChatInputWidget extends Disposable { // Update container width and editor layout when width changes this._store.add(autorun(r => { - const { editorPad, toolbarWidth, totalWidth, height } = this.#layoutData.read(r); + const { editorPad, toolbarWidth, totalWidth, height } = this._layoutData.read(r); const inputWidth = totalWidth - toolbarWidth - editorPad; - this.#container.style.width = `${totalWidth}px`; - this.#inputContainer.style.width = `${inputWidth}px`; - this.#input.layout({ width: inputWidth, height }); + this._container.style.width = `${totalWidth}px`; + this._inputContainer.style.width = `${inputWidth}px`; + this._input.layout({ width: inputWidth, height }); })); // Toggle focus class on the container - this._store.add(this.#input.onDidFocusEditorText(() => this.#container.classList.add('focused'))); - this._store.add(this.#input.onDidBlurEditorText(() => this.#container.classList.remove('focused'))); + this._store.add(this._input.onDidFocusEditorText(() => this._container.classList.add('focused'))); + this._store.add(this._input.onDidBlurEditorText(() => this._container.classList.remove('focused'))); // Toggle scroll decoration on the toolbar - this._store.add(this.#input.onDidScrollChange(e => { - this.#toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); + this._store.add(this._input.onDidScrollChange(e => { + this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); })); // Track input text for context key and adjust width based on content - const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this.#contextKeyService); - this._store.add(this.#input.onDidChangeModelContent(() => { - inputHasText.set(this.#input.getModel().getValue().trim().length > 0); + const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService); + this._store.add(this._input.onDidChangeModelContent(() => { + inputHasText.set(this._input.getModel().getValue().trim().length > 0); })); this._store.add(toDisposable(() => inputHasText.reset())); // Track focus state - const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this.#contextKeyService); - this._store.add(this.#input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); - this._store.add(this.#input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); + const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService); + this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); + this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); this._store.add(toDisposable(() => inputWidgetFocused.reset())); // Handle key events: ArrowDown to move to actions - this._store.add(this.#input.onKeyDown(e => { + this._store.add(this._input.onKeyDown(e => { if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { - const model = this.#input.getModel(); - const position = this.#input.getPosition(); + const model = this._input.getModel(); + const position = this._input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { e.preventDefault(); e.stopPropagation(); @@ -244,18 +237,18 @@ export class InlineChatInputWidget extends Disposable { if (firstItem?.element && dom.isAncestorOfActiveElement(firstItem.element)) { event.preventDefault(); event.stopPropagation(); - this.#input.focus(); + this._input.focus(); } } }, true)); // Track focus - hide when focus leaves - const focusTracker = this._store.add(dom.trackFocus(this.#domNode)); + const focusTracker = this._store.add(dom.trackFocus(this._domNode)); this._store.add(focusTracker.onDidBlur(() => this.hide())); } get value(): string { - return this.#input.getModel().getValue().trim(); + return this._input.getModel().getValue().trim(); } /** @@ -265,77 +258,77 @@ export class InlineChatInputWidget extends Disposable { * @param anchorAbove Whether to anchor above the position (widget grows upward) */ show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { - this.#showStore.clear(); + this._showStore.clear(); // Clear input state - this.#input.updateOptions({ wordWrap: 'off', placeholder }); - this.#input.getModel().setValue(''); + this._input.updateOptions({ wordWrap: 'off', placeholder }); + this._input.getModel().setValue(''); // Store anchor info for scroll updates - this.#anchorLineNumber = lineNumber; - this.#anchorLeft = left; - this.#anchorAbove = anchorAbove; + this._anchorLineNumber = lineNumber; + this._anchorLeft = left; + this._anchorAbove = anchorAbove; // Set initial position - this.#updatePosition(); + this._updatePosition(); // Create overlay widget via observable pattern - this.#showStore.add(this.#editorObs.createOverlayWidget({ - domNode: this.#domNode, - position: this.#position, + this._showStore.add(this._editorObs.createOverlayWidget({ + domNode: this._domNode, + position: this._position, minContentWidthInPx: constObservable(0), allowEditorOverflow: true, })); // If anchoring above, adjust position after render to account for widget height if (anchorAbove) { - this.#updatePosition(); + this._updatePosition(); } // Update position on scroll, hide if anchor line is out of view (only when input is empty) - this.#showStore.add(this.#editorObs.editor.onDidScrollChange(() => { - const visibleRanges = this.#editorObs.editor.getVisibleRanges(); + this._showStore.add(this._editorObs.editor.onDidScrollChange(() => { + const visibleRanges = this._editorObs.editor.getVisibleRanges(); const isLineVisible = visibleRanges.some(range => - this.#anchorLineNumber >= range.startLineNumber && this.#anchorLineNumber <= range.endLineNumber + this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber ); - const hasContent = !!this.#input.getModel().getValue(); + const hasContent = !!this._input.getModel().getValue(); if (!isLineVisible && !hasContent) { this.hide(); } else { - this.#updatePosition(); + this._updatePosition(); } })); // Focus the input editor - setTimeout(() => this.#input.focus(), 0); + setTimeout(() => this._input.focus(), 0); } - #updatePosition(): void { - const editor = this.#editorObs.editor; + private _updatePosition(): void { + const editor = this._editorObs.editor; const lineHeight = editor.getOption(EditorOption.lineHeight); - const top = editor.getTopForLineNumber(this.#anchorLineNumber) - editor.getScrollTop(); + const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop(); let adjustedTop = top; - if (this.#anchorAbove) { - const widgetHeight = this.#domNode.offsetHeight; + if (this._anchorAbove) { + const widgetHeight = this._domNode.offsetHeight; adjustedTop = top - widgetHeight; } else { adjustedTop = top + lineHeight; } // Clamp to viewport bounds when anchor line is out of view - const stickyScrollHeight = this.#stickyScrollHeight.get(); + const stickyScrollHeight = this._stickyScrollHeight.get(); const layoutInfo = editor.getLayoutInfo(); - const widgetHeight = this.#domNode.offsetHeight; + const widgetHeight = this._domNode.offsetHeight; const minTop = stickyScrollHeight; const maxTop = layoutInfo.height - widgetHeight; const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop)); const isClamped = clampedTop !== adjustedTop; - this.#domNode.classList.toggle('clamped', isClamped); + this._domNode.classList.toggle('clamped', isClamped); - this.#position.set({ - preference: { top: clampedTop, left: this.#anchorLeft }, + this._position.set({ + preference: { top: clampedTop, left: this._anchorLeft }, stackOrdinal: 10000, }, undefined); } @@ -345,13 +338,13 @@ export class InlineChatInputWidget extends Disposable { */ hide(): void { // Focus editor if focus is still within the editor's DOM - const editorDomNode = this.#editorObs.editor.getDomNode(); + const editorDomNode = this._editorObs.editor.getDomNode(); if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) { - this.#editorObs.editor.focus(); + this._editorObs.editor.focus(); } - this.#position.set(null, undefined); - this.#input.getModel().setValue(''); - this.#showStore.clear(); + this._position.set(null, undefined); + this._input.getModel().setValue(''); + this._showStore.clear(); } } @@ -360,62 +353,52 @@ export class InlineChatInputWidget extends Disposable { */ export class InlineChatSessionOverlayWidget extends Disposable { - readonly #domNode: HTMLElement = document.createElement('div'); - readonly #container: HTMLElement; - readonly #statusNode: HTMLElement; - readonly #icon: HTMLElement; - readonly #message: HTMLElement; - readonly #toolbarNode: HTMLElement; + private readonly _domNode: HTMLElement = document.createElement('div'); + private readonly _container: HTMLElement; + private readonly _statusNode: HTMLElement; + private readonly _icon: HTMLElement; + private readonly _message: HTMLElement; + private readonly _toolbarNode: HTMLElement; - readonly #showStore = this._store.add(new DisposableStore()); - readonly #position = observableValue(this, null); - readonly #minContentWidthInPx = constObservable(0); + private readonly _showStore = this._store.add(new DisposableStore()); + private readonly _position = observableValue(this, null); + private readonly _minContentWidthInPx = constObservable(0); - readonly #stickyScrollHeight: IObservable; - - readonly #editorObs: ObservableCodeEditor; - readonly #instaService: IInstantiationService; - readonly #keybindingService: IKeybindingService; - readonly #logService: ILogService; + private readonly _stickyScrollHeight: IObservable; constructor( - editorObs: ObservableCodeEditor, - @IInstantiationService instaService: IInstantiationService, - @IKeybindingService keybindingService: IKeybindingService, - @ILogService logService: ILogService, + private readonly _editorObs: ObservableCodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ILogService private readonly _logService: ILogService, ) { super(); - this.#editorObs = editorObs; - this.#instaService = instaService; - this.#keybindingService = keybindingService; - this.#logService = logService; + this._domNode.classList.add('inline-chat-session-overlay-widget'); - this.#domNode.classList.add('inline-chat-session-overlay-widget'); - - this.#container = document.createElement('div'); - this.#domNode.appendChild(this.#container); - this.#container.classList.add('inline-chat-session-overlay-container'); + this._container = document.createElement('div'); + this._domNode.appendChild(this._container); + this._container.classList.add('inline-chat-session-overlay-container'); // Create status node with icon and message - this.#statusNode = document.createElement('div'); - this.#statusNode.classList.add('status'); - this.#icon = dom.append(this.#statusNode, dom.$('span')); - this.#message = dom.append(this.#statusNode, dom.$('span.message')); - this.#container.appendChild(this.#statusNode); + this._statusNode = document.createElement('div'); + this._statusNode.classList.add('status'); + this._icon = dom.append(this._statusNode, dom.$('span')); + this._message = dom.append(this._statusNode, dom.$('span.message')); + this._container.appendChild(this._statusNode); // Create toolbar node - this.#toolbarNode = document.createElement('div'); - this.#toolbarNode.classList.add('toolbar'); + this._toolbarNode = document.createElement('div'); + this._toolbarNode.classList.add('toolbar'); // Initialize sticky scroll height observable - const stickyScrollController = StickyScrollController.get(this.#editorObs.editor); - this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + const stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); } show(session: IInlineChatSession2): void { - assertType(this.#editorObs.editor.hasModel()); - this.#showStore.clear(); + assertType(this._editorObs.editor.hasModel()); + this._showStore.clear(); // Derived entry observable for this session const entry = derived(r => session.editingSession.readEntry(session.uri, r)); @@ -475,34 +458,34 @@ export class InlineChatSessionOverlayWidget extends Disposable { } }); - this.#showStore.add(autorun(r => { + this._showStore.add(autorun(r => { const value = requestMessage.read(r); if (value) { - this.#message.innerText = renderAsPlaintext(value.message); - this.#icon.className = ''; - this.#icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); + this._message.innerText = renderAsPlaintext(value.message); + this._icon.className = ''; + this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); } else { - this.#message.innerText = ''; - this.#icon.className = ''; + this._message.innerText = ''; + this._icon.className = ''; } })); // Log when pending confirmation changes - this.#showStore.add(autorun(r => { + this._showStore.add(autorun(r => { const response = session.chatModel.lastRequestObs.read(r)?.response; const pending = response?.isPendingConfirmation.read(r); if (pending) { - this.#logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`); + this._logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`); } })); // Add toolbar - this.#container.appendChild(this.#toolbarNode); - this.#showStore.add(toDisposable(() => this.#toolbarNode.remove())); + this._container.appendChild(this._toolbarNode); + this._showStore.add(toDisposable(() => this._toolbarNode.remove())); const that = this; - this.#showStore.add(this.#instaService.createInstance(MenuWorkbenchToolBar, this.#toolbarNode, MenuId.ChatEditorInlineExecute, { + this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditorInlineExecute, { telemetrySource: 'inlineChatProgress.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -518,52 +501,52 @@ export class InlineChatSessionOverlayWidget extends Disposable { return undefined; // use default action view item with label } - return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that.#keybindingService, primaryActions); + return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that._keybindingService, primaryActions); } })); // Position in top right of editor, below sticky scroll - const lineHeight = this.#editorObs.getOption(EditorOption.lineHeight); + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight); // Track widget width changes const widgetWidth = observableValue(this, 0); const resizeObserver = new dom.DisposableResizeObserver(() => { - widgetWidth.set(this.#domNode.offsetWidth, undefined); + widgetWidth.set(this._domNode.offsetWidth, undefined); }); - this.#showStore.add(resizeObserver); - this.#showStore.add(resizeObserver.observe(this.#domNode)); + this._showStore.add(resizeObserver); + this._showStore.add(resizeObserver.observe(this._domNode)); - this.#showStore.add(autorun(r => { - const layoutInfo = this.#editorObs.layoutInfo.read(r); - const stickyScrollHeight = this.#stickyScrollHeight.read(r); + this._showStore.add(autorun(r => { + const layoutInfo = this._editorObs.layoutInfo.read(r); + const stickyScrollHeight = this._stickyScrollHeight.read(r); const width = widgetWidth.read(r); const padding = Math.round(lineHeight.read(r) * 2 / 3); // Cap max-width to the editor viewport (content area) const maxWidth = layoutInfo.contentWidth - 2 * padding; - this.#domNode.style.maxWidth = `${maxWidth}px`; + this._domNode.style.maxWidth = `${maxWidth}px`; // Position: top right, below sticky scroll with padding, left of minimap and scrollbar const top = stickyScrollHeight + padding; const left = layoutInfo.width - width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - padding; - this.#position.set({ + this._position.set({ preference: { top, left }, stackOrdinal: 10000, }, undefined); })); // Create overlay widget - this.#showStore.add(this.#editorObs.createOverlayWidget({ - domNode: this.#domNode, - position: this.#position, - minContentWidthInPx: this.#minContentWidthInPx, + this._showStore.add(this._editorObs.createOverlayWidget({ + domNode: this._domNode, + position: this._position, + minContentWidthInPx: this._minContentWidthInPx, allowEditorOverflow: false, })); } hide(): void { - this.#position.set(null, undefined); - this.#showStore.clear(); + this._position.set(null, undefined); + this._showStore.clear(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 4f36ec8bba0..4a008a59b0f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -41,58 +41,55 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; - readonly #store = new DisposableStore(); - readonly #sessions = new ResourceMap(); + private readonly _store = new DisposableStore(); + private readonly _sessions = new ResourceMap(); - readonly #onWillStartSession = this.#store.add(new Emitter()); - readonly onWillStartSession: Event = this.#onWillStartSession.event; + private readonly _onWillStartSession = this._store.add(new Emitter()); + readonly onWillStartSession: Event = this._onWillStartSession.event; - readonly #onDidChangeSessions = this.#store.add(new Emitter()); - readonly onDidChangeSessions: Event = this.#onDidChangeSessions.event; - - readonly #chatService: IChatService; + private readonly _onDidChangeSessions = this._store.add(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; constructor( - @IChatService chatService: IChatService, + @IChatService private readonly _chatService: IChatService, @IChatAgentService chatAgentService: IChatAgentService, ) { - this.#chatService = chatService; // Listen for agent changes and dispose all sessions when there is no agent const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const agent = agentObs.read(r); if (!agent) { // No agent available, dispose all sessions - dispose(this.#sessions.values()); - this.#sessions.clear(); + dispose(this._sessions.values()); + this._sessions.clear(); } })); } dispose() { - this.#store.dispose(); + this._store.dispose(); } createSession(editor: IActiveCodeEditor): IInlineChatSession2 { const uri = editor.getModel().uri; - if (this.#sessions.has(uri)) { + if (this._sessions.has(uri)) { throw new Error('Session already exists'); } - this.#onWillStartSession.fire(editor); + this._onWillStartSession.fire(editor); - const chatModelRef = this.#chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); + const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); const chatModel = chatModelRef.object; chatModel.startEditingSession(false); const store = new DisposableStore(); store.add(toDisposable(() => { - this.#chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); + this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); chatModel.editingSession?.reject(); - this.#sessions.delete(uri); - this.#onDidChangeSessions.fire(this); + this._sessions.delete(uri); + this._onDidChangeSessions.fire(this); })); store.add(chatModelRef); @@ -107,7 +104,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) { const response = chatModel.getRequests().at(-1)?.response; if (response) { - this.#chatService.notifyUserAction({ + this._chatService.notifyUserAction({ sessionResource: response.session.sessionResource, requestId: response.requestId, agentId: response.agent?.id, @@ -141,16 +138,16 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { editingSession: chatModel.editingSession!, dispose: store.dispose.bind(store) }; - this.#sessions.set(uri, result); - this.#onDidChangeSessions.fire(this); + this._sessions.set(uri, result); + this._onDidChangeSessions.fire(this); return result; } getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { - let result = this.#sessions.get(uri); + let result = this._sessions.get(uri); if (!result) { // no direct session, try to find an editing session which has a file entry for the uri - for (const [_, candidate] of this.#sessions) { + for (const [_, candidate] of this._sessions) { const entry = candidate.editingSession.getEntry(uri); if (entry) { result = candidate; @@ -162,7 +159,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { - for (const session of this.#sessions.values()) { + for (const session of this._sessions.values()) { if (isEqual(session.chatModel.sessionResource, sessionResource)) { return session; } @@ -175,11 +172,11 @@ export class InlineChatEnabler { static Id = 'inlineChat.enabler'; - readonly #ctxHasProvider2: IContextKey; - readonly #ctxHasNotebookProvider: IContextKey; - readonly #ctxPossible: IContextKey; + private readonly _ctxHasProvider2: IContextKey; + private readonly _ctxHasNotebookProvider: IContextKey; + private readonly _ctxPossible: IContextKey; - readonly #store = new DisposableStore(); + private readonly _store = new DisposableStore(); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -187,41 +184,41 @@ export class InlineChatEnabler { @IEditorService editorService: IEditorService, @IConfigurationService configService: IConfigurationService, ) { - this.#ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); - this.#ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); - this.#ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); + this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); + this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); + this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService); - this.#store.add(autorun(r => { + this._store.add(autorun(r => { const agent = agentObs.read(r); if (!agent) { - this.#ctxHasProvider2.reset(); + this._ctxHasProvider2.reset(); } else { - this.#ctxHasProvider2.set(true); + this._ctxHasProvider2.set(true); } })); - this.#store.add(autorun(r => { - this.#ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); + this._store.add(autorun(r => { + this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); })); const updateEditor = () => { const ctrl = editorService.activeEditorPane?.getControl(); const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl); - this.#ctxPossible.set(isCodeEditorLike); + this._ctxPossible.set(isCodeEditorLike); }; - this.#store.add(editorService.onDidActiveEditorChange(updateEditor)); + this._store.add(editorService.onDidActiveEditorChange(updateEditor)); updateEditor(); } dispose() { - this.#ctxPossible.reset(); - this.#ctxHasProvider2.reset(); - this.#store.dispose(); + this._ctxPossible.reset(); + this._ctxHasProvider2.reset(); + this._store.dispose(); } } @@ -232,7 +229,7 @@ export class InlineChatEscapeToolContribution extends Disposable { static readonly DONT_ASK_AGAIN_KEY = 'inlineChat.dontAskMoveToPanelChat'; - static readonly #data: IToolData = { + private static readonly _data: IToolData = { id: 'inline_chat_exit', source: ToolDataSource.Internal, canBeReferencedInPrompt: false, @@ -254,7 +251,7 @@ export class InlineChatEscapeToolContribution extends Disposable { super(); - this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution.#data, { + this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, { invoke: async (invocation, _tokenCountFn, _progress, _token) => { const sessionResource = invocation.context?.sessionResource; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 9d3dc3edbcb..a449ed2a512 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -95,59 +95,37 @@ export class InlineChatWidget { protected readonly _store = new DisposableStore(); - readonly #ctxInputEditorFocused: IContextKey; - readonly #ctxResponseFocused: IContextKey; + private readonly _ctxInputEditorFocused: IContextKey; + private readonly _ctxResponseFocused: IContextKey; - readonly #chatWidget: ChatWidget; + private readonly _chatWidget: ChatWidget; protected readonly _onDidChangeHeight = this._store.add(new Emitter()); - readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting); + readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); - readonly #requestInProgress = observableValue(this, false); - readonly requestInProgress: IObservable = this.#requestInProgress; + private readonly _requestInProgress = observableValue(this, false); + readonly requestInProgress: IObservable = this._requestInProgress; - #isLayouting: boolean = false; + private _isLayouting: boolean = false; readonly scopedContextKeyService: IContextKeyService; - readonly #options: IInlineChatWidgetConstructionOptions; - readonly #contextKeyService: IContextKeyService; - readonly #keybindingService: IKeybindingService; - readonly #accessibilityService: IAccessibilityService; - readonly #configurationService: IConfigurationService; - readonly #accessibleViewService: IAccessibleViewService; - readonly #chatService: IChatService; - readonly #hoverService: IHoverService; - readonly #chatEntitlementService: IChatEntitlementService; - readonly #markdownRendererService: IMarkdownRendererService; - constructor( location: IChatWidgetLocationOptions, - options: IInlineChatWidgetConstructionOptions, + private readonly _options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IKeybindingService keybindingService: IKeybindingService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IConfigurationService configurationService: IConfigurationService, - @IAccessibleViewService accessibleViewService: IAccessibleViewService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, @ITextModelService protected readonly _textModelResolverService: ITextModelService, - @IChatService chatService: IChatService, - @IHoverService hoverService: IHoverService, - @IChatEntitlementService chatEntitlementService: IChatEntitlementService, - @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, + @IChatService private readonly _chatService: IChatService, + @IHoverService private readonly _hoverService: IHoverService, + @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, ) { - this.#options = options; - this.#contextKeyService = contextKeyService; - this.#keybindingService = keybindingService; - this.#accessibilityService = accessibilityService; - this.#configurationService = configurationService; - this.#accessibleViewService = accessibleViewService; - this.#chatService = chatService; - this.#hoverService = hoverService; - this.#chatEntitlementService = chatEntitlementService; - this.#markdownRendererService = markdownRendererService; - - this.scopedContextKeyService = this._store.add(contextKeyService.createScoped(this._elements.chatWidget)); + this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ IContextKeyService, @@ -156,7 +134,7 @@ export class InlineChatWidget { this._store ); - this.#chatWidget = scopedInstaService.createInstance( + this._chatWidget = scopedInstaService.createInstance( ChatWidget, location, { isInlineChat: true }, @@ -176,14 +154,14 @@ export class InlineChatWidget { if (emptyResponse) { return false; } - if (item.response.value.every(item => item.kind === 'textEditGroup' && this.#options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { + if (item.response.value.every(item => item.kind === 'textEditGroup' && _options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) { return false; } return true; }, dndContainer: this._elements.root, defaultMode: ChatMode.Ask, - ...this.#options.chatWidgetViewOptions + ..._options.chatWidgetViewOptions }, { listForeground: inlineChatForeground, @@ -193,11 +171,11 @@ export class InlineChatWidget { resultEditorBackground: editorBackground } ); - this._elements.root.classList.toggle('in-zone-widget', !!this.#options.inZoneWidget); - this.#chatWidget.render(this._elements.chatWidget); + this._elements.root.classList.toggle('in-zone-widget', !!_options.inZoneWidget); + this._chatWidget.render(this._elements.chatWidget); this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); - this.#chatWidget.setVisible(true); - this._store.add(this.#chatWidget); + this._chatWidget.setVisible(true); + this._store.add(this._chatWidget); const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService); const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService); @@ -206,10 +184,10 @@ export class InlineChatWidget { const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService); const viewModelStore = this._store.add(new DisposableStore()); - this._store.add(this.#chatWidget.onDidChangeViewModel(() => { + this._store.add(this._chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); - const viewModel = this.#chatWidget.viewModel; + const viewModel = this._chatWidget.viewModel; if (!viewModel) { return; } @@ -225,7 +203,7 @@ export class InlineChatWidget { viewModelStore.add(viewModel.onDidChange(() => { - this.#requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); + this._requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); const last = viewModel.getItems().at(-1); toolbar2.context = last; @@ -246,22 +224,22 @@ export class InlineChatWidget { })); // context keys - this.#ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this.#contextKeyService); + this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); const tracker = this._store.add(trackFocus(this.domNode)); - this._store.add(tracker.onDidBlur(() => this.#ctxResponseFocused.set(false))); - this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true))); + this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false))); + this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true))); - this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this.#contextKeyService); - this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); - this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); + this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService); + this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true))); + this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); - const statusMenuId = this.#options.statusMenuId instanceof MenuId ? this.#options.statusMenuId : this.#options.statusMenuId.menu; + const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu; // BUTTON bar - const statusMenuOptions = this.#options.statusMenuId instanceof MenuId ? undefined : this.#options.statusMenuId.options; + const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options; const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar1, statusMenuId, { toolbarOptions: { primaryGroup: '0_main' }, - telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource, + telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource, menuOptions: { renderShortTitle: true }, ...statusMenuOptions, }); @@ -269,8 +247,8 @@ export class InlineChatWidget { this._store.add(statusButtonBar); // secondary toolbar - const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, this.#options.secondaryMenuId ?? MenuId.for(''), { - telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource, + const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, _options.secondaryMenuId ?? MenuId.for(''), { + telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource, menuOptions: { renderShortTitle: true, shouldForwardArgs: true }, actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId) { @@ -283,60 +261,60 @@ export class InlineChatWidget { this._store.add(toolbar2); - this._store.add(this.#configurationService.onDidChangeConfiguration(e => { + this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this.#updateAriaLabel(); + this._updateAriaLabel(); } })); this._elements.root.tabIndex = 0; this._elements.statusLabel.tabIndex = 0; - this.#updateAriaLabel(); - this.#setupDisclaimer(); + this._updateAriaLabel(); + this._setupDisclaimer(); - this._store.add(this.#hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { + this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { return this._elements.statusLabel.dataset['title']; })); - this._store.add(this.#chatService.onDidPerformUserAction(e => { - if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { + this._store.add(this._chatService.onDidPerformUserAction(e => { + if (isEqual(e.sessionResource, this._chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 }); } })); } - #updateAriaLabel(): void { + private _updateAriaLabel(): void { - this._elements.root.ariaLabel = this.#accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); - if (this.#accessibilityService.isScreenReaderOptimized()) { + if (this._accessibilityService.isScreenReaderOptimized()) { let label = defaultAriaLabel; - if (this.#configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { - const kbLabel = this.#keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + if (this._configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { + const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); label = kbLabel ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label }); + this._chatWidget.inputEditor.updateOptions({ ariaLabel: label }); } } - #setupDisclaimer(): void { + private _setupDisclaimer(): void { const disposables = this._store.add(new DisposableStore()); this._store.add(autorun(reader => { disposables.clear(); reset(this._elements.disclaimerLabel); - const sentiment = this.#chatEntitlementService.sentimentObs.read(reader); - const anonymous = this.#chatEntitlementService.anonymousObs.read(reader); - const requestInProgress = this.#chatService.requestInProgressObs.read(reader); + const sentiment = this._chatEntitlementService.sentimentObs.read(reader); + const anonymous = this._chatEntitlementService.anonymousObs.read(reader); + const requestInProgress = this._chatService.requestInProgressObs.read(reader); const showDisclaimer = !sentiment.installed && anonymous && !requestInProgress; this._elements.disclaimerLabel.classList.toggle('hidden', !showDisclaimer); if (showDisclaimer) { - const renderedMarkdown = disposables.add(this.#markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); + const renderedMarkdown = disposables.add(this._markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); this._elements.disclaimerLabel.appendChild(renderedMarkdown.element); } @@ -353,20 +331,20 @@ export class InlineChatWidget { } get chatWidget(): ChatWidget { - return this.#chatWidget; + return this._chatWidget; } saveState() { - this.#chatWidget.saveState(); + this._chatWidget.saveState(); } layout(widgetDim: Dimension) { const contentHeight = this.contentHeight; - this.#isLayouting = true; + this._isLayouting = true; try { this._doLayout(widgetDim); } finally { - this.#isLayouting = false; + this._isLayouting = false; if (this.contentHeight !== contentHeight) { this._onDidChangeHeight.fire(); @@ -383,7 +361,7 @@ export class InlineChatWidget { this._elements.root.style.height = `${dimension.height - extraHeight}px`; this._elements.root.style.width = `${dimension.width}px`; - this.#chatWidget.layout( + this._chatWidget.layout( dimension.height - statusHeight - extraHeight, dimension.width ); @@ -394,7 +372,7 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - chatWidgetContentHeight: this.#chatWidget.contentHeight, + chatWidgetContentHeight: this._chatWidget.contentHeight, statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; @@ -407,7 +385,7 @@ export class InlineChatWidget { // at least "maxWidgetHeight" high and at most the content height. let maxWidgetOutputHeight = 100; - for (const item of this.#chatWidget.viewModel?.getItems() ?? []) { + for (const item of this._chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { maxWidgetOutputHeight = 270; break; @@ -415,29 +393,29 @@ export class InlineChatWidget { } let value = this.contentHeight; - value -= this.#chatWidget.contentHeight; - value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight); + value -= this._chatWidget.contentHeight; + value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; } protected _getExtraHeight(): number { - return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); + return this._options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); } get value(): string { - return this.#chatWidget.getInput(); + return this._chatWidget.getInput(); } set value(value: string) { - this.#chatWidget.setInput(value); + this._chatWidget.setInput(value); } selectAll() { - this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } set placeholder(value: string) { - this.#chatWidget.setInputPlaceholder(value); + this._chatWidget.setInputPlaceholder(value); } toggleStatus(show: boolean) { @@ -458,7 +436,7 @@ export class InlineChatWidget { } async getCodeBlockInfo(codeBlockIndex: number): Promise { - const { viewModel } = this.#chatWidget; + const { viewModel } = this._chatWidget; if (!viewModel) { return undefined; } @@ -505,18 +483,18 @@ export class InlineChatWidget { } get responseContent(): string | undefined { - const requests = this.#chatWidget.viewModel?.model.getRequests(); + const requests = this._chatWidget.viewModel?.model.getRequests(); return requests?.at(-1)?.response?.response.toString(); } getChatModel(): IChatModel | undefined { - return this.#chatWidget.viewModel?.model; + return this._chatWidget.viewModel?.model; } setChatModel(chatModel: IChatModel) { chatModel.inputModel.setState({ inputText: '', selections: [] }); - this.#chatWidget.setModel(chatModel); + this._chatWidget.setModel(chatModel); } updateInfo(message: string): void { @@ -555,8 +533,8 @@ export class InlineChatWidget { } reset() { - this.#chatWidget.attachmentModel.clear(true); - this.#chatWidget.saveState(); + this._chatWidget.attachmentModel.clear(true); + this._chatWidget.saveState(); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); @@ -569,7 +547,7 @@ export class InlineChatWidget { } focus() { - this.#chatWidget.focusInput(); + this._chatWidget.focusInput(); } hasFocus() { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 5f172c19672..21113b9d0de 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -28,7 +28,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; export class InlineChatZoneWidget extends ZoneWidget { - static readonly #options: IOptions = { + private static readonly _options: IOptions = { showFrame: true, frameWidth: 1, // frameColor: 'var(--vscode-inlineChat-border)', @@ -43,12 +43,9 @@ export class InlineChatZoneWidget extends ZoneWidget { readonly widget: EditorBasedInlineChatWidget; - readonly #ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - #dimension?: Dimension; - #notebookEditor?: INotebookEditor; - - readonly #instaService: IInstantiationService; - #logService: ILogService; + private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + private _dimension?: Dimension; + private notebookEditor?: INotebookEditor; constructor( location: IChatWidgetLocationOptions, @@ -56,22 +53,20 @@ export class InlineChatZoneWidget extends ZoneWidget { editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor }, /** @deprecated should go away with inline2 */ clearDelegate: () => Promise, - @IInstantiationService instaService: IInstantiationService, - @ILogService logService: ILogService, + @IInstantiationService private readonly _instaService: IInstantiationService, + @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super(editors.editor, InlineChatZoneWidget.#options); - this.#instaService = instaService; - this.#logService = logService; - this.#notebookEditor = editors.notebookEditor; + super(editors.editor, InlineChatZoneWidget._options); + this.notebookEditor = editors.notebookEditor; - this.#ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); this._disposables.add(toDisposable(() => { - this.#ctxCursorPosition.reset(); + this._ctxCursorPosition.reset(); })); - this.widget = this.#instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { + this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { statusMenuId: { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { @@ -110,14 +105,14 @@ export class InlineChatZoneWidget extends ZoneWidget { let revealFn: (() => void) | undefined; this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => { if (this.position) { - revealFn = this.#createZoneAndScrollRestoreFn(this.position); + revealFn = this._createZoneAndScrollRestoreFn(this.position); } })); this._disposables.add(this.widget.onDidChangeHeight(() => { if (this.position && !this._usesResizeHeight) { // only relayout when visible - revealFn ??= this.#createZoneAndScrollRestoreFn(this.position); - const height = this.#computeHeight(); + revealFn ??= this._createZoneAndScrollRestoreFn(this.position); + const height = this._computeHeight(); this._relayout(height.linesValue); revealFn?.(); revealFn = undefined; @@ -141,13 +136,13 @@ export class InlineChatZoneWidget extends ZoneWidget { // todo@jrieken listen ONLY when showing const updateCursorIsAboveContextKey = () => { if (!this.position || !this.editor.hasModel()) { - this.#ctxCursorPosition.reset(); + this._ctxCursorPosition.reset(); } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this.#ctxCursorPosition.set('above'); + this._ctxCursorPosition.set('above'); } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this.#ctxCursorPosition.set('below'); + this._ctxCursorPosition.set('below'); } else { - this.#ctxCursorPosition.reset(); + this._ctxCursorPosition.reset(); } }; this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); @@ -164,19 +159,19 @@ export class InlineChatZoneWidget extends ZoneWidget { protected override _doLayout(heightInPixel: number): void { - this.#updatePadding(); + this._updatePadding(); const info = this.editor.getLayoutInfo(); const width = info.contentWidth - info.verticalScrollbarWidth; // width = Math.min(850, width); - this.#dimension = new Dimension(width, heightInPixel); - this.widget.layout(this.#dimension); + this._dimension = new Dimension(width, heightInPixel); + this.widget.layout(this._dimension); } - #computeHeight(): { linesValue: number; pixelsValue: number } { + private _computeHeight(): { linesValue: number; pixelsValue: number } { const chatContentHeight = this.widget.contentHeight; - const editorHeight = this.#notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height; + const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height; const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42)); const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight); @@ -197,25 +192,25 @@ export class InlineChatZoneWidget extends ZoneWidget { } protected override _onWidth(_widthInPixel: number): void { - if (this.#dimension) { - this._doLayout(this.#dimension.height); + if (this._dimension) { + this._doLayout(this._dimension.height); } } override show(position: Position): void { assertType(this.container); - this.#updatePadding(); + this._updatePadding(); - const revealZone = this.#createZoneAndScrollRestoreFn(position); - super.show(position, this.#computeHeight().linesValue); + const revealZone = this._createZoneAndScrollRestoreFn(position); + super.show(position, this._computeHeight().linesValue); this.widget.chatWidget.setVisible(true); this.widget.focus(); revealZone(); } - #updatePadding() { + private _updatePadding() { assertType(this.container); const info = this.editor.getLayoutInfo(); @@ -231,12 +226,12 @@ export class InlineChatZoneWidget extends ZoneWidget { } override updatePositionAndHeight(position: Position): void { - const revealZone = this.#createZoneAndScrollRestoreFn(position); - super.updatePositionAndHeight(position, !this._usesResizeHeight ? this.#computeHeight().linesValue : undefined); + const revealZone = this._createZoneAndScrollRestoreFn(position); + super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined); revealZone(); } - #createZoneAndScrollRestoreFn(position: Position): () => void { + private _createZoneAndScrollRestoreFn(position: Position): () => void { const scrollState = StableEditorBottomScrollState.capture(this.editor); @@ -247,7 +242,7 @@ export class InlineChatZoneWidget extends ZoneWidget { const scrollTop = this.editor.getScrollTop(); const lineTop = this.editor.getTopForLineNumber(lineNumber); - const zoneTop = lineTop - this.#computeHeight().pixelsValue; + const zoneTop = lineTop - this._computeHeight().pixelsValue; const editorHeight = this.editor.getLayoutInfo().height; const lineBottom = this.editor.getBottomForLineNumber(lineNumber); @@ -262,7 +257,7 @@ export class InlineChatZoneWidget extends ZoneWidget { } if (newScrollTop < scrollTop || forceScrollTop) { - this.#logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); + this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } }; @@ -274,7 +269,7 @@ export class InlineChatZoneWidget extends ZoneWidget { override hide(): void { const scrollState = StableEditorBottomScrollState.capture(this.editor); - this.#ctxCursorPosition.reset(); + this._ctxCursorPosition.reset(); this.widget.chatWidget.setVisible(false); super.hide(); aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts b/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts index 8e1573fdaed..2a6ea9759da 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/testWorkerService.ts @@ -20,49 +20,47 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc export class TestWorkerService extends mock() implements IDisposable { - readonly #store = new DisposableStore(); - readonly #worker = this.#store.add(new EditorWorker()); - readonly #modelService: IModelService; + private readonly _store = new DisposableStore(); + private readonly _worker = this._store.add(new EditorWorker()); - constructor(@IModelService modelService: IModelService) { + constructor(@IModelService private readonly _modelService: IModelService) { super(); - this.#modelService = modelService; } dispose(): void { - this.#store.dispose(); + this._store.dispose(); } override async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean | undefined): Promise { return undefined; } override async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise { - await new Promise(resolve => disposableTimeout(() => resolve(), 0, this.#store)); - if (this.#store.isDisposed) { + await new Promise(resolve => disposableTimeout(() => resolve(), 0, this._store)); + if (this._store.isDisposed) { return null; } - const originalModel = this.#modelService.getModel(original); - const modifiedModel = this.#modelService.getModel(modified); + const originalModel = this._modelService.getModel(original); + const modifiedModel = this._modelService.getModel(modified); assertType(originalModel); assertType(modifiedModel); - this.#worker.$acceptNewModel({ + this._worker.$acceptNewModel({ url: originalModel.uri.toString(), versionId: originalModel.getVersionId(), lines: originalModel.getLinesContent(), EOL: originalModel.getEOL(), }); - this.#worker.$acceptNewModel({ + this._worker.$acceptNewModel({ url: modifiedModel.uri.toString(), versionId: modifiedModel.getVersionId(), lines: modifiedModel.getLinesContent(), EOL: modifiedModel.getEOL(), }); - const result = await this.#worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm); + const result = await this._worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm); if (!result) { return result; } From be9986cffdad13477ad7735dedcc7c546301409f Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 6 Mar 2026 18:00:56 +0100 Subject: [PATCH 301/448] Fixes problem with code coverage on windows --- .../vscode-selfhost-test-provider/src/testOutputScanner.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 296ed1e9f12..09e9a2af6d2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -545,6 +545,9 @@ export class SourceMapStore { } } + if (/^[a-zA-Z]:/.test(source) || source.startsWith('/')) { + return vscode.Uri.file(source); + } return vscode.Uri.parse(source); } From 77375f1dc78bb14f929de41726c330acb362dabf Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 6 Mar 2026 18:18:20 +0100 Subject: [PATCH 302/448] Ensures ColorId.DefaultBackground is set when no colors are registered. --- src/vs/editor/common/viewModel/minimapTokensColorTracker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/viewModel/minimapTokensColorTracker.ts b/src/vs/editor/common/viewModel/minimapTokensColorTracker.ts index 45473f77c4d..72ea2bcd16b 100644 --- a/src/vs/editor/common/viewModel/minimapTokensColorTracker.ts +++ b/src/vs/editor/common/viewModel/minimapTokensColorTracker.ts @@ -38,7 +38,10 @@ export class MinimapTokensColorTracker extends Disposable { private _updateColorMap(): void { const colorMap = TokenizationRegistry.getColorMap(); if (!colorMap) { - this._colors = [RGBA8.Empty]; + this._colors = []; + for (let i = 0; i <= ColorId.DefaultBackground; i++) { + this._colors[i] = RGBA8.Empty; + } this._backgroundIsLight = true; return; } From 844d9b263b892c8c68e8b92260bca37b1af7c300 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:52:09 -0800 Subject: [PATCH 303/448] Always require `tooltips` for markdown command links Fixes #299657 Also gives the displayed text argument a clearer name --- src/vs/base/common/htmlContent.ts | 4 ++-- .../contrib/chat/browser/tools/languageModelToolsService.ts | 4 ++-- .../chatContentParts/chatDisabledClaudeHooksContentPart.ts | 3 ++- .../chatMcpServersInteractionContentPart.ts | 6 ++++-- .../toolInvocationParts/chatToolPartUtilities.ts | 4 ++-- src/vs/workbench/contrib/mcp/browser/mcpCommands.ts | 3 ++- src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts | 6 +++--- src/vs/workbench/contrib/mcp/browser/mcpServersView.ts | 2 +- 8 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index 070279f045a..16049d7e6f7 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -210,9 +210,9 @@ export function createMarkdownLink(text: string, href: string, title?: string, e return `[${escapeTokens ? escapeMarkdownSyntaxTokens(text) : text}](${href}${title ? ` "${escapeMarkdownSyntaxTokens(title)}"` : ''})`; } -export function createMarkdownCommandLink(command: { title: string; id: string; arguments?: unknown[]; tooltip?: string }, escapeTokens = true): string { +export function createMarkdownCommandLink(command: { text: string; id: string; arguments?: unknown[]; tooltip: string }, escapeTokens = true): string { const uri = createCommandUri(command.id, ...(command.arguments || [])).toString(); - return createMarkdownLink(command.title, uri, command.tooltip, escapeTokens); + return createMarkdownLink(command.text, uri, command.tooltip, escapeTokens); } export function createCommandUri(commandId: string, ...commandArgs: unknown[]): URI { diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 27b43d79cc7..f00d1d149b7 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -838,14 +838,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo ...prepared.confirmationMessages, title: localize('defaultToolConfirmation.title', 'Confirm tool execution'), message: localize('defaultToolConfirmation.message', 'Run the \'{0}\' tool?', fullReferenceName), - disclaimer: tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ title: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval] }, false)), { isTrusted: true }), + disclaimer: tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ text: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval], tooltip: localize('openSettings.autoApproval.tooltip', 'Open settings to configure auto-approval') }, false)), { isTrusted: true }), allowAutoConfirm: false, }; } if (!isEligibleForAutoApproval && prepared?.confirmationMessages?.title) { // Always overwrite the disclaimer if not eligible for auto-approval - prepared.confirmationMessages.disclaimer = tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ title: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval] }, false)), { isTrusted: true }); + prepared.confirmationMessages.disclaimer = tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ text: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval], tooltip: localize('openSettings.autoApproval.tooltip', 'Open settings to configure auto-approval') }, false)), { isTrusted: true }); } if (prepared?.confirmationMessages?.title) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts index 7939aaa3099..67b9be3d84d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts @@ -33,9 +33,10 @@ export class ChatDisabledClaudeHooksContentPart extends Disposable implements IC icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); const enableLink = createMarkdownCommandLink({ - title: localize('chat.disabledClaudeHooks.enableLink', "Enable"), + text: localize('chat.disabledClaudeHooks.enableLink', "Enable"), id: 'workbench.action.openSettings', arguments: [PromptsConfig.USE_CLAUDE_HOOKS], + tooltip: localize('chat.disabledClaudeHooks.enableLink.tooltip', "Open settings to enable Claude Code hooks"), }); const message = localize('chat.disabledClaudeHooks.message', "Claude Code hooks are available for this workspace. {0}", enableLink); const content = new MarkdownString(message, { isTrusted: true }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts index 101b84d9810..0fec722d8a2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMcpServersInteractionContentPart.ts @@ -105,16 +105,18 @@ export class ChatMcpServersInteractionContentPart extends Disposable implements private createServerCommandLinks(servers: Array<{ id: string; label: string }>): string { return servers.map(s => createMarkdownCommandLink({ - title: '`' + escapeMarkdownSyntaxTokens(s.label) + '`', + text: '`' + escapeMarkdownSyntaxTokens(s.label) + '`', id: McpCommandIds.ServerOptions, arguments: [s.id], + tooltip: localize('mcp.server.options.tooltip', 'Show options for {0}', s.label), }, false)).join(', '); } private updateDetailedProgress(state: IAutostartResult): void { const skipText = createMarkdownCommandLink({ - title: localize('mcp.skip.link', 'Skip?'), + text: localize('mcp.skip.link', 'Skip?'), id: McpCommandIds.SkipCurrentAutostart, + tooltip: localize('mcp.skip.tooltip', 'Skip starting this MCP server'), }); let content: MarkdownString; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts index aa7a82177c6..0aba9ec84a9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts @@ -30,7 +30,7 @@ export function getApprovalMessageFromReason(reason: ConfirmedReason): IMarkdown let md: string; switch (reason.type) { case ToolConfirmKind.Setting: - md = localize('chat.autoapprove.setting', 'Auto approved by {0}', createMarkdownCommandLink({ title: '`' + reason.id + '`', id: 'workbench.action.openSettings', arguments: [reason.id] }, false)); + md = localize('chat.autoapprove.setting', 'Auto approved by {0}', createMarkdownCommandLink({ text: '`' + reason.id + '`', id: 'workbench.action.openSettings', arguments: [reason.id], tooltip: localize('openSettings.tooltip', 'Open settings') }, false)); break; case ToolConfirmKind.LmServicePerTool: md = reason.scope === 'session' @@ -38,7 +38,7 @@ export function getApprovalMessageFromReason(reason: ConfirmedReason): IMarkdown : reason.scope === 'workspace' ? localize('chat.autoapprove.lmServicePerTool.workspace', 'Auto approved for this workspace') : localize('chat.autoapprove.lmServicePerTool.profile', 'Auto approved for this profile'); - md += ' (' + createMarkdownCommandLink({ title: localize('edit', 'Edit'), id: 'workbench.action.chat.editToolApproval', arguments: [reason.scope] }) + ')'; + md += ' (' + createMarkdownCommandLink({ text: localize('edit', 'Edit'), id: 'workbench.action.chat.editToolApproval', arguments: [reason.scope], tooltip: localize('editToolApproval.tooltip', 'Edit tool approval settings') }) + ')'; break; case ToolConfirmKind.ConfirmationNotNeeded: if (reason.reason) { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 71fd9bcd6c8..204a7326b88 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -550,9 +550,10 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo protected override getHoverContents({ state, servers } = displayedStateCurrent.get()): string | undefined | IManagedHoverTooltipHTMLElement { const link = (s: IMcpServer) => createMarkdownCommandLink({ - title: s.definition.label, + text: s.definition.label, id: McpCommandIds.ServerOptions, arguments: [s.definition.id], + tooltip: localize('mcp.server.options.tooltip', 'Show server options for {0}', s.definition.label), }); const single = servers.length === 1; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts index 254537d67c9..285e27f0370 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts @@ -388,9 +388,9 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib function pushAnnotation(savedId: string, offset: number, saved: IResolvedValue): InlayHint { const tooltip = new MarkdownString([ - createMarkdownCommandLink({ id: McpCommandIds.EditStoredInput, title: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target] }), - createMarkdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId] }), - createMarkdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope] }), + createMarkdownCommandLink({ id: McpCommandIds.EditStoredInput, text: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target], tooltip: localize('edit.savedValue.tooltip', 'Edit saved value') }), + createMarkdownCommandLink({ id: McpCommandIds.RemoveStoredInput, text: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId], tooltip: localize('clear.savedValue.tooltip', 'Clear saved value') }), + createMarkdownCommandLink({ id: McpCommandIds.RemoveStoredInput, text: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope], tooltip: localize('clearAll.savedValues.tooltip', 'Clear all saved values') }), ].join(' | '), { isTrusted: true }); const hint: InlayHint = { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 9e82f119e88..864cc4f5f8c 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -270,7 +270,7 @@ export class McpServersListView extends AbstractExtensionsListView Date: Fri, 6 Mar 2026 09:58:10 -0800 Subject: [PATCH 304/448] [MCP_Sandboxing]: Notifying network domains that need access (#299701) * changes to ensure all the network requests are passed through proxy * changes to ensure all the network requests are passed through proxy --- .../contrib/mcp/common/mcpSandboxService.ts | 2 +- .../contrib/mcp/common/mcpServerConnection.ts | 23 ++---------- .../test/common/mcpServerConnection.test.ts | 36 +++++++++++++++++++ 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts index 7f930eabb54..4cfa41a6cdf 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts @@ -276,7 +276,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService private async _getSandboxEnvVariables(baseEnv: McpServerTransportStdio['env'], tempDir: URI | undefined, rgPath: string | undefined, remoteAuthority?: string): Promise { let env: McpServerTransportStdio['env'] = { ...baseEnv }; if (tempDir) { - env = { ...env, TMPDIR: tempDir.path, SRT_DEBUG: 'true' }; + env = { ...env, TMPDIR: tempDir.path, SRT_DEBUG: 'true', NODE_USE_ENV_PROXY: '1' }; } if (rgPath) { env = { ...env, PATH: env['PATH'] ? `${env['PATH']}${await this._getPathDelimiter(remoteAuthority)}${dirname(rgPath)}` : dirname(rgPath) }; diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts index b67ba4f17a7..2b493d9cfc1 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts @@ -162,15 +162,7 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect }; } - if (/\b(?:EAI_AGAIN|ENOTFOUND)\b/i.test(message)) { - return { - kind: 'network', - message, - host: this._extractSandboxHost(message), - }; - } - - if (/(?:\b(?:EACCES|EPERM|ENOENT|fail(?:ed|ure)?)\b|not accessible)/i.test(message)) { + if (/(?:\b(?:EACCES|EPERM|ENOENT|EROFS|fail(?:ed|ure)?)\b|\bnot accessible\b|read only)/i.test(message)) { return { kind: 'filesystem', message, @@ -197,16 +189,7 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect } private _extractSandboxHost(value: string): string | undefined { - const deniedMatch = value.match(/No matching config rule, denying:\s+(.+)$/i); - const matchTarget = deniedMatch?.[1] ?? value; - const trimmed = matchTarget.trim().replace(/^["'`]+|["'`,.;]+$/g, ''); - if (!trimmed) { - return undefined; - } - - const withoutProtocol = trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ''); - const firstToken = withoutProtocol.split(/[\s/]/, 1)[0] ?? ''; - const host = firstToken.replace(/:\d+$/, ''); - return host || undefined; + const match = value.match(/No matching config rule, denying:\s+(?[^:\s]+):\d+\.?$/i); + return match?.groups?.host; } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index 44f180efe2f..490efd09409 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -338,6 +338,42 @@ suite('Workbench - MCP - ServerConnection', () => { await timeout(10); }); + test('should emit a sandbox network block with the denied host', async () => { + const sandboxedDefinition: McpServerDefinition = { + ...serverDefinition, + sandboxEnabled: true, + }; + + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + sandboxedDefinition, + delegate, + sandboxedDefinition.launch, + new NullLogger(), + false, + store.add(new McpTaskManager()), + ); + store.add(connection); + + const sandboxBlock = Event.toPromise(connection.onPotentialSandboxBlock); + const startPromise = connection.start({}); + + transport.simulateLog('No matching config rule, denying: api.example.com:443.'); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + assert.deepStrictEqual(await sandboxBlock, { + kind: 'network', + message: 'No matching config rule, denying: api.example.com:443.', + host: 'api.example.com', + }); + + await startPromise; + + connection.dispose(); + await timeout(10); + }); + test('should correctly handle transitions to and from error state', async () => { // Create server connection const connection = instantiationService.createInstance( From 41c93525460b38533f59a9396bbd653ee6ca4c42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:02:12 +0000 Subject: [PATCH 305/448] fix: pass Codicon.vscode directly instead of registering a new icon Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- .../workbench/contrib/update/browser/releaseNotesEditor.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 59967d3cd39..92c942f9d5e 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -20,7 +20,6 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { asTextOrError, IRequestService } from '../../../../platform/request/common/request.js'; -import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js'; import { WebviewInput } from '../../webviewPanel/browser/webviewEditorInput.js'; import { IWebviewWorkbenchService } from '../../webviewPanel/browser/webviewWorkbenchService.js'; @@ -40,8 +39,6 @@ import { asWebviewUri } from '../../webview/common/webview.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -const ReleaseNotesEditorIcon = registerIcon('release-notes-view-icon', Codicon.vscode, nls.localize('releaseNotesViewIcon', 'Icon of the release notes editor.')); - export class ReleaseNotesManager extends Disposable { private readonly _simpleSettingRenderer: SimpleSettingRenderer; private readonly _releaseNotesCache = new Map>(); @@ -127,7 +124,7 @@ export class ReleaseNotesManager extends Disposable { }, 'releaseNotes', title, - ReleaseNotesEditorIcon, + Codicon.vscode, { group: ACTIVE_GROUP, preserveFocus: false }); const disposables = new DisposableStore(); From 1b0e9461dee82a4c456c543105f8c6a2db5137a6 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 6 Mar 2026 18:35:21 +0100 Subject: [PATCH 306/448] groups component explorer updates --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fc5cda5555b..07296619597 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,11 @@ updates: allow: - dependency-name: "@vscode/component-explorer" - dependency-name: "@vscode/component-explorer-cli" + groups: + component-explorer: + patterns: + - "@vscode/component-explorer" + - "@vscode/component-explorer-cli" - package-ecosystem: "npm" directory: "/build/vite" schedule: @@ -22,3 +27,8 @@ updates: allow: - dependency-name: "@vscode/component-explorer" - dependency-name: "@vscode/component-explorer-vite-plugin" + groups: + component-explorer: + patterns: + - "@vscode/component-explorer" + - "@vscode/component-explorer-vite-plugin" From 140fced2733a71f802d89d5730c63729583f3790 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Mar 2026 13:05:41 -0500 Subject: [PATCH 307/448] ensure deleting attachment works on windows (#299824) fixes #299733 --- .../contrib/chat/browser/attachments/chatAttachmentWidgets.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 8a20bd70982..a502dd82f1e 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -158,6 +158,8 @@ abstract class AbstractChatAttachmentWidget extends Disposable { })); this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => { if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) { + e.preventDefault(); + e.stopPropagation(); this._onDidDelete.fire(e.browserEvent); } })); From cac47a6efca3d1a4e8dbba24f68e84685481ba30 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:41:25 -0800 Subject: [PATCH 308/448] Update build TS versions --- package-lock.json | 72 +++++++++---------- package.json | 4 +- src/vs/editor/common/languages.ts | 2 +- src/vs/monaco.d.ts | 6 +- .../remote/browser/browserSocketFactory.ts | 6 +- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fc1351c51f..8cc48045ffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", - "@typescript/native-preview": "^7.0.0-dev.20260130", + "@typescript/native-preview": "^7.0.0-dev.20260306", "@vscode/component-explorer": "^0.1.1-19", "@vscode/component-explorer-cli": "^0.1.1-15", "@vscode/gulp-electron": "1.40.1", @@ -157,7 +157,7 @@ "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20260130", + "typescript": "^6.0.0-dev.20260306", "typescript-eslint": "^8.45.0", "util": "^0.12.4", "webpack": "^5.105.0", @@ -3034,28 +3034,28 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-lvt9sECmBkrABxl3rMNRAX2unzhYcoNhlTyR7rOvbyM//QTXKUctVD7ByWBvk02et2caUUwIWq2vnygaeW8Mew==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-4m7cOjtKu+iLazWW5MuJuI2ZZMkQkS42+GxN6FVdja1nL0t47l1wpaTnzUa1Ny9Xa0opIJ7psPAMBKYAPKbCKA==", "dev": true, "license": "Apache-2.0", "bin": { "tsgo": "bin/tsgo.js" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260130.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260130.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20260130.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260130.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20260130.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260130.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20260130.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260306.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260306.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260306.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260306.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260306.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260306.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260306.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-Jo5kVoxaewKPn/3bKWyUB/gPR+Tjhj6isLc8VshV4OyFX4n6pkvVyk3ANivl7Kwmiv3WGKGUotbZ71DKCZATwA==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-4vuh4VlPydMS/nymDzjJIKDk3dntnEEB5UzyJV9mM4kxF5+geFgJih1DTtZS3qVafhHLB3e4l8omtvGftMnb8g==", "cpu": [ "arm64" ], @@ -3067,9 +3067,9 @@ ] }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-dR0fjdcLykfiDOIKjZMGqPBHVl9Dd/C+jFU43Wr3dcPFPFf1oVYsaWAZBSkTXnN9QP8i0/ZV+ZUr1gDjoi3x0Q==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-qxYfv0aM4KCZPEe584KIjT5sO4uR+xdyuQXX5tXbnH1UoksIz7bvJ9KUgRloS/q/ww0f8UjPS2+27LnRA4y7ig==", "cpu": [ "x64" ], @@ -3081,9 +3081,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-wnx4bY/1u006U67fEkPtPVZ65VYMLgkFqOadGyrUxhtveR5WbbgFUuUBES0mPxvzS4ToZzn94jhcnAvN8VOTcA==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-8gRAFx0ExDWHOmphl8mzBrSoGWnLWDU4VpxkPRsWqaJpHVbjr9Yk2QkuJNIaDmF6q44eJmW/huSiObmHTbZ1UQ==", "cpu": [ "arm" ], @@ -3095,9 +3095,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-P/1YTpIiFd2pPtHt4sKEmUTaKf1xvuuiV0TvhQ7n2gDYskNjZ66iWCC9w7okjgsmWE9JLh/IRrNcb9FKVk3SHw==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-8G0BKvTkE+eKX1tSnyKeDaf3bWPWY7OI77SMipagCAyYi06v4gxx+IVE3Px7W7kLX2Wqp1MjWDXu2N76wfJtXQ==", "cpu": [ "arm64" ], @@ -3109,9 +3109,9 @@ ] }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-OgHVjivuOS22WIZvIm+Pnm7yqFLwonkIrBOxRdew/pPwVGLQVSo+bQ+RocQDj2VFYxXcHs2yXwCk3PDmwLIYYg==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-rsJV3Z9J/zYCEtcqvm+WfLAml3i1OAyMEUn0hja7i8C0kzE+tXKXzsJ0+I1TrSU5O7hHvqlLTvueBoCoM4aL4g==", "cpu": [ "x64" ], @@ -3123,9 +3123,9 @@ ] }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-f/DUxQtIWkZq0eUjZHFmaSxterO/ccu1NxFk0L/Oqj7AfjWVDCqrLVgZJKjvwcG5TEb5AVt7GMUpGEAYZQiUvg==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-US1WsIu9IukaFzM+w8wt0fIAkmk2WtxeVuk8nkbrnH9S3ax39r0J4ikMNZSXEJE0VMxhXJoymzfWxhj3s9yW/Q==", "cpu": [ "arm64" ], @@ -3137,9 +3137,9 @@ ] }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260130.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260130.1.tgz", - "integrity": "sha512-Isr051Cq8RbXOUMYYmwLYw8yBGaEG/Zp0sp7HNeYhVVkc3/3KeveEqCk29q1QRwiBr7HnApdzJP7f+lSZk8gmg==", + "version": "7.0.0-dev.20260306.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260306.1.tgz", + "integrity": "sha512-MlneT0RWS9Zdb8XoWvHsUgmnMJu6K3S0BXRu5ZgUYjcbQKlkz+Z87aUB8eX8qnDFd9csJcMp3+ZrgQ/LKVGP1g==", "cpu": [ "x64" ], @@ -19702,9 +19702,9 @@ "dev": true }, "node_modules/typescript": { - "version": "6.0.0-dev.20260130", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20260130.tgz", - "integrity": "sha512-flWwLX5Xzh7to9d46u3LXfVDq9F0L0FtgnsYcx/SksqP05uHBIPnWfB6wWOZphTkb7GRSRKU13X/zBHmbzhXXg==", + "version": "6.0.0-dev.20260306", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20260306.tgz", + "integrity": "sha512-ssxgK3/0yA2LEW23KzSNtnqSL9zDaVGTesx2S3EN+v8kqkPScFTin7S63KfQ4UDZGZGcvBgHCEoEz7t7v2yR8Q==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 3f5bcc61b9c..1b7eb4bc018 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", - "@typescript/native-preview": "^7.0.0-dev.20260130", + "@typescript/native-preview": "^7.0.0-dev.20260306", "@vscode/component-explorer": "^0.1.1-19", "@vscode/component-explorer-cli": "^0.1.1-15", "@vscode/gulp-electron": "1.40.1", @@ -227,7 +227,7 @@ "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20260130", + "typescript": "^6.0.0-dev.20260306", "typescript-eslint": "^8.45.0", "util": "^0.12.4", "webpack": "^5.105.0", diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index f141c78d98e..33e90ab7f75 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -826,7 +826,7 @@ export class SelectedSuggestionInfo { ) { } - public equals(other: SelectedSuggestionInfo) { + public equals(other: SelectedSuggestionInfo): boolean { return Range.lift(this.range).equalsRange(other.range) && this.text === other.text && this.completionKind === other.completionKind diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index fc9c2da70f5..1e2394bb65a 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5259,7 +5259,7 @@ declare namespace monaco.editor { export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; acceptSuggestionOnEnter: IEditorOption; - accessibilitySupport: IEditorOption; + accessibilitySupport: IEditorOption; accessibilityPageSize: IEditorOption; allowOverflow: IEditorOption; allowVariableLineHeights: IEditorOption; @@ -5322,7 +5322,7 @@ declare namespace monaco.editor { foldingMaximumRegions: IEditorOption; unfoldOnClickAfterEndOfLine: IEditorOption; fontFamily: IEditorOption; - fontInfo: IEditorOption; + fontInfo: IEditorOption; fontLigatures2: IEditorOption; fontSize: IEditorOption; fontWeight: IEditorOption; @@ -5362,7 +5362,7 @@ declare namespace monaco.editor { pasteAs: IEditorOption>>; parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; - placeholder: IEditorOption; + placeholder: IEditorOption; definitionLinkOpensInPeek: IEditorOption; quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index eafeef861a1..d558e4eef58 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -43,7 +43,7 @@ export interface IWebSocket { readonly onError: Event; traceSocketEvent?(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | unknown): void; - send(data: ArrayBuffer | ArrayBufferView): void; + send(data: ArrayBuffer | ArrayBufferView): void; close(): void; } @@ -182,7 +182,7 @@ class BrowserWebSocket extends Disposable implements IWebSocket { })); } - send(data: ArrayBuffer | ArrayBufferView): void { + send(data: ArrayBuffer | ArrayBufferView): void { if (this._isClosed) { // Refuse to write data to closed WebSocket... return; @@ -254,7 +254,7 @@ class BrowserSocket implements ISocket { } public write(buffer: VSBuffer): void { - this.socket.send(buffer.buffer); + this.socket.send(buffer.buffer as Uint8Array); } public end(): void { From 88a4ff6324aaa5212ca82da5003c6431187eeb71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:47:40 +0000 Subject: [PATCH 309/448] Fix tests to use URI.file() for platform-independent paths Co-authored-by: mjbvz <12821956+mjbvz@users.noreply.github.com> --- src/vs/base/test/browser/markdownRenderer.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index be51715f57e..16373be2101 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -308,25 +308,28 @@ suite('MarkdownRenderer', () => { }); test('Should use decoded file path as title for file:// links', () => { - const md = new MarkdownString(`[log](file:///home/user/project/lib.d.ts)`, {}); + const fileUri = URI.file('/home/user/project/lib.d.ts'); + const md = new MarkdownString(`[log](${fileUri.toString()})`, {}); const result = store.add(renderMarkdown(md)).element; const anchor = result.querySelector('a')!; assert.ok(anchor); - assert.strictEqual(anchor.title, '/home/user/project/lib.d.ts'); + assert.strictEqual(anchor.title, fileUri.fsPath); }); test('Should include fragment in title for file:// links with line numbers', () => { - const md = new MarkdownString(`[log](file:///home/user/project/lib.d.ts#L42)`, {}); + const fileUri = URI.file('/home/user/project/lib.d.ts'); + const md = new MarkdownString(`[log](${fileUri.toString()}#L42)`, {}); const result = store.add(renderMarkdown(md)).element; const anchor = result.querySelector('a')!; assert.ok(anchor); - assert.strictEqual(anchor.title, '/home/user/project/lib.d.ts#L42'); + assert.strictEqual(anchor.title, `${fileUri.fsPath}#L42`); }); test('Should not override explicit title for file:// links', () => { - const md = new MarkdownString(`[log](file:///home/user/project/lib.d.ts "Go to definition")`, {}); + const fileUri = URI.file('/home/user/project/lib.d.ts'); + const md = new MarkdownString(`[log](${fileUri.toString()} "Go to definition")`, {}); const result = store.add(renderMarkdown(md)).element; const anchor = result.querySelector('a')!; From b607547dea5b2be2feb7722e4a918755c566a567 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Mar 2026 14:21:06 -0500 Subject: [PATCH 310/448] tweak wording of chat tip for clarity (#299832) fixes #299565 --- src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index 404ab0b82b7..6a93f4d568f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -349,7 +349,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ tier: ChatTipTier.Qol, buildMessage() { return new MarkdownString( - localize('tip.subagents', "Ask the agent to work in parallel to complete large tasks faster.") + localize('tip.subagents', "Have another task to work on? Start a new session to run multiple agents at once.") ); }, when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), From abe7ae5449c8cab202f4cc8cd8a8f05936cf06b8 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:02:44 -0800 Subject: [PATCH 311/448] fix: include sessions built-in prompts in esbuild resource patterns (#299848) The new esbuild-based CI pipeline (core-ci) uses curated resource patterns in build/next/index.ts to copy non-JS assets into the bundle output. When built-in .prompt.md files were added for the sessions window, they were included in the legacy pipeline's vscodeResourceIncludes (build/gulpfile.vscode.ts) but not in the desktopResourcePatterns used by the esbuild pipeline. This caused the prompt files to be missing from release builds (out-vscode-min), even though they worked correctly when running from sources (where copyAllNonTsFiles copies everything). Add 'vs/sessions/prompts/*.prompt.md' to desktopResourcePatterns to match the existing entry in vscodeResourceIncludes. --- build/next/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/next/index.ts b/build/next/index.ts index f3043f0fa1f..a77b98b5c63 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -273,6 +273,9 @@ const desktopResourcePatterns = [ 'vs/workbench/services/extensionManagement/common/media/*.png', 'vs/workbench/browser/parts/editor/media/*.png', 'vs/workbench/contrib/debug/browser/media/*.png', + + // Sessions - built-in prompts + 'vs/sessions/prompts/*.prompt.md', ]; // Resources for server target (minimal - no UI) From c30864b3d0fa2049360c59c98b9d0c4fe8ac1de3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:08:17 +0100 Subject: [PATCH 312/448] Sessions - initial implementation for git changes (#299855) * Sessions - initial implementation of repository changes * Deduplicate resources and fix badge --- .../changesView/browser/changesView.ts | 76 +++++++++++++++---- .../browser/mainThreadGitExtensionService.ts | 22 +++++- .../workbench/api/common/extHost.protocol.ts | 10 +++ .../api/common/extHostGitExtensionService.ts | 72 +++++++++++++++--- .../contrib/git/common/gitService.ts | 10 +++ 5 files changed, 163 insertions(+), 27 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index da1e460a297..9cf004af32a 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -13,9 +13,9 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableFromPromise, observableValue } from '../../../../base/common/observable.js'; import { basename, dirname } from '../../../../base/common/path.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -58,6 +58,7 @@ import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/b import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; +import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; const $ = dom.$; @@ -101,6 +102,8 @@ interface IChangesFolderItem { interface IActiveSession { readonly resource: URI; readonly sessionType: string; + readonly repository: URI | undefined; + readonly worktree: URI | undefined; } type ChangesTreeElement = IChangesFileItem | IChangesFolderItem; @@ -230,6 +233,7 @@ export class ChangesViewPane extends ViewPane { private readonly activeSession: IObservableWithChange; private readonly activeSessionFileCountObs: IObservableWithChange; private readonly activeSessionHasChangesObs: IObservableWithChange; + private readonly activeSessionRepositoryChangesObs: IObservableWithChange; get activeSessionHasChanges(): IObservable { return this.activeSessionHasChangesObs; @@ -257,6 +261,7 @@ export class ChangesViewPane extends ViewPane { @ILabelService private readonly labelService: ILabelService, @IStorageService private readonly storageService: IStorageService, @ICodeReviewService private readonly codeReviewService: ICodeReviewService, + @IGitService private readonly gitService: IGitService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -278,16 +283,49 @@ export class ChangesViewPane extends ViewPane { return { resource: activeSession.resource, + repository: activeSession.repository, + worktree: activeSession.worktree, sessionType: getChatSessionType(activeSession.resource), }; }).recomputeInitiallyAndOnChange(this._store); + // Track active session repository changes + const repositoryObs = derived(reader => { + const activeSessionWorktree = this.activeSession.read(reader)?.worktree; + if (!activeSessionWorktree) { + return undefined; + } + + return observableFromPromise(this.gitService.openRepository(activeSessionWorktree)); + }); + + this.activeSessionRepositoryChangesObs = derived(reader => { + const repository = repositoryObs.read(reader)?.read(reader); + if (!repository) { + return undefined; + } + + const state = repository.value?.state.read(reader); + return (state?.workingTreeChanges ?? []).map(change => { + const isDeletion = change.modifiedUri === undefined; + const isAddition = change.originalUri === undefined; + return { + type: 'file', + uri: change.modifiedUri ?? change.uri, + originalUri: change.originalUri, + state: ModifiedFileEntryState.Accepted, + isDeletion, + changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', + reviewCommentCount: 0, + linesAdded: 0, + linesRemoved: 0, + } satisfies IChangesFileItem; + }); + }); + this.activeSessionFileCountObs = this.createActiveSessionFileCountObservable(); this.activeSessionHasChangesObs = this.activeSessionFileCountObs.map(fileCount => fileCount > 0).recomputeInitiallyAndOnChange(this._store); - // Setup badge tracking - this.registerBadgeTracking(); - // Set chatSessionType on the view's context key service so ViewTitle // menu items can use it in their `when` clauses. Update reactively // when the active session changes. @@ -298,14 +336,6 @@ export class ChangesViewPane extends ViewPane { })); } - private registerBadgeTracking(): void { - // Update badge when file count changes - this._register(autorun(reader => { - const fileCount = this.activeSessionFileCountObs.read(reader); - this.updateBadge(fileCount); - })); - } - private createActiveSessionFileCountObservable(): IObservableWithChange { const activeSessionResource = this.activeSession.map(a => a?.resource); @@ -532,13 +562,24 @@ export class ChangesViewPane extends ViewPane { const combinedEntriesObs = derived(reader => { const editEntries = editSessionEntriesObs.read(reader); const sessionFiles = sessionFilesObs.read(reader); - return [...editEntries, ...sessionFiles]; + const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? []; + + const resources = new Set(); + const entries: IChangesFileItem[] = []; + for (const item of [...editEntries, ...sessionFiles, ...repositoryFiles]) { + if (!resources.has(item.uri.fsPath)) { + resources.add(item.uri.fsPath); + entries.push(item); + } + } + return entries.sort((a, b) => extUriBiasedIgnorePathCase.compare(a.uri, b.uri)); }); // Calculate stats from combined entries const topLevelStats = derived(reader => { const editEntries = editSessionEntriesObs.read(reader); const sessionFiles = sessionFilesObs.read(reader); + const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? []; const entries = combinedEntriesObs.read(reader); let added = 0, removed = 0; @@ -549,7 +590,7 @@ export class ChangesViewPane extends ViewPane { } const files = entries.length; - const isSessionMenu = editEntries.length === 0 && sessionFiles.length > 0; + const isSessionMenu = editEntries.length === 0 && (sessionFiles.length > 0 || repositoryFiles.length > 0); return { files, added, removed, isSessionMenu }; }); @@ -653,6 +694,11 @@ export class ChangesViewPane extends ViewPane { dom.setVisibility(!hasEntries, this.welcomeContainer!); })); + // Update badge when file count changes + this.renderDisposables.add(autorun(reader => { + this.updateBadge(topLevelStats.read(reader).files); + })); + // Update summary text (line counts only, file count is shown in badge) if (this.summaryContainer) { dom.clearNode(this.summaryContainer); diff --git a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts index 6ed0a6d0acd..ad414190cd6 100644 --- a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts @@ -8,7 +8,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../base/common/map.js'; import { URI } from '../../../base/common/uri.js'; import { GitRepository } from '../../contrib/git/browser/gitService.js'; -import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType, GitRepositoryState, GitBranch, IGitRepository } from '../../contrib/git/common/gitService.js'; +import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType, GitRepositoryState, GitBranch, GitChange, IGitRepository } from '../../contrib/git/common/gitService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, ExtHostGitExtensionShape, GitRefTypeDto, GitRepositoryStateDto, MainContext, MainThreadGitExtensionShape } from '../common/extHost.protocol.js'; @@ -32,6 +32,26 @@ function toGitRepositoryState(dto: GitRepositoryStateDto | undefined): GitReposi ahead: dto.HEAD.ahead, behind: dto.HEAD.behind, } satisfies GitBranch : undefined, + mergeChanges: dto?.mergeChanges?.map(c => ({ + uri: URI.revive(c.uri), + originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined, + modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined, + } satisfies GitChange)) ?? [], + indexChanges: dto?.indexChanges?.map(c => ({ + uri: URI.revive(c.uri), + originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined, + modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined, + } satisfies GitChange)) ?? [], + workingTreeChanges: dto?.workingTreeChanges?.map(c => ({ + uri: URI.revive(c.uri), + originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined, + modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined, + } satisfies GitChange)) ?? [], + untrackedChanges: dto?.untrackedChanges?.map(c => ({ + uri: URI.revive(c.uri), + originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined, + modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined, + } satisfies GitChange)) ?? [], }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0bd60212242..be954021047 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3627,8 +3627,18 @@ export interface GitRefDto { readonly revision: string; } +export interface GitChangeDto { + readonly uri: UriComponents; + readonly originalUri: UriComponents | undefined; + readonly modifiedUri: UriComponents | undefined; +} + export interface GitRepositoryStateDto { readonly HEAD?: GitBranchDto; + readonly mergeChanges: readonly GitChangeDto[]; + readonly indexChanges: readonly GitChangeDto[]; + readonly workingTreeChanges: readonly GitChangeDto[]; + readonly untrackedChanges: readonly GitChangeDto[]; } export interface GitBranchDto { diff --git a/src/vs/workbench/api/common/extHostGitExtensionService.ts b/src/vs/workbench/api/common/extHostGitExtensionService.ts index 61a64e83bf3..6c2960f0383 100644 --- a/src/vs/workbench/api/common/extHostGitExtensionService.ts +++ b/src/vs/workbench/api/common/extHostGitExtensionService.ts @@ -11,7 +11,7 @@ import { ExtensionIdentifier } from '../../../platform/extensions/common/extensi import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { IExtHostExtensionService } from './extHostExtensionService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; -import { ExtHostGitExtensionShape, GitBranchDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js'; +import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js'; import { ResourceMap } from '../../../base/common/map.js'; const GIT_EXTENSION_ID = 'vscode.git'; @@ -45,6 +45,52 @@ function toGitUpstreamRefDto(upstream: UpstreamRef): GitUpstreamRefDto { }; } +// Status values from the git extension's const enum Status +const enum GitStatus { + INDEX_ADDED = 1, + INDEX_DELETED = 2, + INDEX_RENAMED = 3, + MODIFIED = 5, + DELETED = 6, + UNTRACKED = 7, + INTENT_TO_ADD = 9, + INTENT_TO_RENAME = 10, +} + +function toGitChangeDto(change: Change): GitChangeDto { + switch (change.status) { + // Added: no original + case GitStatus.INDEX_ADDED: + case GitStatus.UNTRACKED: + case GitStatus.INTENT_TO_ADD: + return { uri: change.uri, originalUri: undefined, modifiedUri: change.uri }; + + // Deleted: no modified + case GitStatus.INDEX_DELETED: + case GitStatus.DELETED: + return { uri: change.uri, originalUri: change.uri, modifiedUri: undefined }; + + // Renamed: original is old name, modified is new name + case GitStatus.INDEX_RENAMED: + case GitStatus.INTENT_TO_RENAME: + return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.renameUri }; + + // Modified and everything else: both original and modified + default: + return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.uri }; + } +} + +function toGitRepositoryStateDto(state: RepositoryState): GitRepositoryStateDto { + return { + HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined, + mergeChanges: state.mergeChanges.map(toGitChangeDto), + indexChanges: state.indexChanges.map(toGitChangeDto), + workingTreeChanges: state.workingTreeChanges.map(toGitChangeDto), + untrackedChanges: state.untrackedChanges.map(toGitChangeDto), + }; +} + interface Repository { readonly rootUri: vscode.Uri; readonly state: RepositoryState; @@ -53,8 +99,19 @@ interface Repository { getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise; } +interface Change { + readonly uri: vscode.Uri; + readonly originalUri: vscode.Uri; + readonly renameUri: vscode.Uri | undefined; + readonly status: number; +} + interface RepositoryState { readonly HEAD: Branch | undefined; + readonly mergeChanges: Change[]; + readonly indexChanges: Change[]; + readonly workingTreeChanges: Change[]; + readonly untrackedChanges: Change[]; readonly onDidChange: Event; } @@ -148,9 +205,7 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi return { handle: existingHandle, rootUri: repository.rootUri, - state: { - HEAD: repository.state.HEAD ? toGitBranchDto(repository.state.HEAD) : undefined - } + state: toGitRepositoryStateDto(repository.state), }; } @@ -178,11 +233,7 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi return { handle, rootUri: repository.rootUri, - state: { - HEAD: repository.state.HEAD - ? toGitBranchDto(repository.state.HEAD) - : undefined - } + state: toGitRepositoryStateDto(repository.state), }; } @@ -225,8 +276,7 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi return undefined; } - const state = repository.state; - return { HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined }; + return toGitRepositoryStateDto(repository.state); } private async _ensureGitApi(): Promise { diff --git a/src/vs/workbench/contrib/git/common/gitService.ts b/src/vs/workbench/contrib/git/common/gitService.ts index 353686d452a..2217f2200db 100644 --- a/src/vs/workbench/contrib/git/common/gitService.ts +++ b/src/vs/workbench/contrib/git/common/gitService.ts @@ -29,8 +29,18 @@ export interface GitRefQuery { readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; } +export interface GitChange { + readonly uri: URI; + readonly originalUri: URI | undefined; + readonly modifiedUri: URI | undefined; +} + export interface GitRepositoryState { readonly HEAD?: GitBranch; + readonly mergeChanges: readonly GitChange[]; + readonly indexChanges: readonly GitChange[]; + readonly workingTreeChanges: readonly GitChange[]; + readonly untrackedChanges: readonly GitChange[]; } export interface GitBranch extends GitRef { From 22933cea7b65c18ba29aa7f52937f13b22bd2eb5 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Mar 2026 16:11:34 -0500 Subject: [PATCH 313/448] add `closeOnResult` for editor's find widget (#299865) fixes #264818 --- src/vs/editor/common/config/editorOptions.ts | 11 ++++ .../contrib/find/browser/findController.ts | 17 ++++++ .../find/test/browser/findController.test.ts | 61 +++++++++++++++++++ src/vs/monaco.d.ts | 4 ++ .../browser/editorFindAccessibilityHelp.ts | 1 + 5 files changed, 94 insertions(+) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index bf796436167..287992d1744 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1744,6 +1744,10 @@ export interface IEditorFindOptions { * Controls whether the search result and diff result automatically restarts from the beginning (or the end) when no further matches can be found */ loop?: boolean; + /** + * Controls whether to close the Find Widget after an explicit find navigation command lands on a match. + */ + closeOnResult?: boolean; /** * @internal * Controls how the find widget search history should be stored @@ -1772,6 +1776,7 @@ class EditorFind extends BaseEditorOption(input.history, this.defaultValue.history, ['never', 'workspace']), replaceHistory: stringSet<'never' | 'workspace'>(input.replaceHistory, this.defaultValue.replaceHistory, ['never', 'workspace']), }; diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index ec2ee490e19..3260daed640 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -725,11 +725,28 @@ async function matchFindAction(editor: ICodeEditor, next: boolean): Promise { + const previousSelection = controller.editor.getSelection(); const result = next ? controller.moveToNextMatch() : controller.moveToPrevMatch(); + + let landedOnMatch = false; if (result) { + const currentSelection = controller.editor.getSelection(); + if (!previousSelection && currentSelection) { + landedOnMatch = true; + } else if (previousSelection && currentSelection && !previousSelection.equalsSelection(currentSelection)) { + landedOnMatch = true; + } + } + + if (landedOnMatch) { controller.editor.pushUndoStop(); + if (shouldCloseOnResult && wasFindWidgetVisible && controller.isFindInputFocused()) { + controller.closeFindWidget(); + } return true; } return false; diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index ed0383148f9..1823dd31e1a 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -292,6 +292,67 @@ suite('FindController', () => { }); }); + test('editor.find.closeOnResult: closes find widget when a match is found from explicit navigation', async () => { + await withAsyncTestCodeEditor([ + 'ABC', + 'ABC', + 'XYZ', + ], { serviceCollection: serviceCollection, find: { closeOnResult: true } }, async (editor, _, instantiationService) => { + const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); + const findState = findController.getState(); + + await executeAction(instantiationService, editor, StartFindAction); + assert.strictEqual(findState.isRevealed, true); + + findState.change({ searchString: 'ABC' }, true); + await editor.runAction(NextMatchFindAction); + + assert.strictEqual(findState.isRevealed, false); + findController.dispose(); + }); + }); + + test('editor.find.closeOnResult: keeps find widget open when no match is found', async () => { + await withAsyncTestCodeEditor([ + 'ABC', + 'DEF', + 'XYZ', + ], { serviceCollection: serviceCollection, find: { closeOnResult: true } }, async (editor, _, instantiationService) => { + const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); + const findState = findController.getState(); + + await executeAction(instantiationService, editor, StartFindAction); + assert.strictEqual(findState.isRevealed, true); + + findState.change({ searchString: 'NO_MATCH' }, true); + await editor.runAction(NextMatchFindAction); + + assert.strictEqual(findState.matchesCount, 0); + assert.strictEqual(findState.isRevealed, true); + findController.dispose(); + }); + }); + + test('editor.find.closeOnResult: disabled keeps find widget open after navigation', async () => { + await withAsyncTestCodeEditor([ + 'ABC', + 'ABC', + 'XYZ', + ], { serviceCollection: serviceCollection, find: { closeOnResult: false } }, async (editor, _, instantiationService) => { + const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); + const findState = findController.getState(); + + await executeAction(instantiationService, editor, StartFindAction); + assert.strictEqual(findState.isRevealed, true); + + findState.change({ searchString: 'ABC' }, true); + await editor.runAction(NextMatchFindAction); + + assert.strictEqual(findState.isRevealed, true); + findController.dispose(); + }); + }); + test('issue #9043: Clear search scope when find widget is hidden', async () => { await withAsyncTestCodeEditor([ 'var x = (3 * 5)', diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index fc9c2da70f5..7d60866f371 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4255,6 +4255,10 @@ declare namespace monaco.editor { * Controls whether the search result and diff result automatically restarts from the beginning (or the end) when no further matches can be found */ loop?: boolean; + /** + * Controls whether to close the Find Widget after an explicit find navigation command lands on a match. + */ + closeOnResult?: boolean; } export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorFindAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/editorFindAccessibilityHelp.ts index 32b0152643d..c0cef1433a2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/editorFindAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/editorFindAccessibilityHelp.ts @@ -300,6 +300,7 @@ class EditorFindAccessibilityHelpProvider extends Disposable implements IAccessi content.push(localize('find.settingSeed', "- `editor.find.seedSearchStringFromSelection`: Controls when selection text is used to seed Find.")); content.push(localize('find.settingAutoSelection', "- `editor.find.autoFindInSelection`: Automatically enables Find in Selection based on selection type.")); content.push(localize('find.settingLoop', "- `editor.find.loop`: Wraps search at the beginning or end of the file.")); + content.push(localize('find.settingCloseOnResult', "- `editor.find.closeOnResult`: Closes the Find dialog after an explicit find navigation command lands on a match.")); content.push(localize('find.settingExtraSpace', "- `editor.find.addExtraSpaceOnTop`: Adds extra scroll space so matches are not hidden behind the Find dialog.")); content.push(localize('find.settingHistory', "- `editor.find.history`: Controls whether Find search history is stored.")); content.push(localize('find.settingOccurrences', "- `editor.occurrencesHighlight`: Highlights other occurrences of the current symbol.")); From b35cc3053cbe00f1e316507cbd015ed53a3e592c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Mar 2026 16:12:01 -0500 Subject: [PATCH 314/448] alert when an image is attached via paste (#299862) fix #299859 --- .../chat/browser/widget/input/editor/chatPasteProviders.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts index eafcf7aa1c6..527e9e9becf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../../../../base/common/dataTransfer.js'; +import { alert } from '../../../../../../../base/browser/ui/aria/aria.js'; import { HierarchicalKind } from '../../../../../../../base/common/hierarchicalKind.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { revive } from '../../../../../../../base/common/marshalling.js'; @@ -23,7 +24,7 @@ import { IFileService } from '../../../../../../../platform/files/common/files.j import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { IExtensionService, isProposedApiEnabled } from '../../../../../../services/extensions/common/extensions.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, isImageVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; import { IDynamicVariable } from '../../../../common/attachments/chatVariables.js'; import { IChatWidgetService } from '../../../chat.js'; import { getDynamicVariablesForWidget } from '../../../attachments/chatVariables.js'; @@ -386,6 +387,7 @@ function createCustomPasteEdit(model: ITextModel, context: IChatRequestVariableE const label = context.length === 1 ? context[0].name : localize('pastedAttachment.multiple', '{0} and {1} more', context[0].name, context.length - 1); + const announceImageAttachment = context.length === 1 && isImageVariableEntry(context[0]); const customEdit = { resource: model.uri, @@ -403,6 +405,9 @@ function createCustomPasteEdit(model: ITextModel, context: IChatRequestVariableE throw new Error('No widget found for redo'); } widget.attachmentModel.addContext(...context); + if (announceImageAttachment) { + alert(localize('chat.pastedImageAttached', 'Attached image')); + } }, metadata: { needsConfirmation: false, From e842b429d2742ccdf0ad16363e79708230ac8237 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:15:59 -0800 Subject: [PATCH 315/448] Fix browser positioning issues (#299842) --- .../electron-browser/browserEditor.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index a9983e9e468..a3af6057701 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -49,6 +49,7 @@ import { logBrowserOpen } from '../../../../platform/browserView/common/browserV import { URI } from '../../../../base/common/uri.js'; import { ChatConfiguration } from '../../chat/common/constants.js'; import { Event } from '../../../../base/common/event.js'; +import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; export const CONTEXT_BROWSER_CAN_GO_BACK = new RawContextKey('browserCanGoBack', false, localize('browser.canGoBack', "Whether the browser can go back")); export const CONTEXT_BROWSER_CAN_GO_FORWARD = new RawContextKey('browserCanGoForward', false, localize('browser.canGoForward', "Whether the browser can go forward")); @@ -273,7 +274,8 @@ export class BrowserEditor extends EditorPane { @IEditorService private readonly editorService: IEditorService, @IBrowserElementsService private readonly browserElementsService: IBrowserElementsService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILayoutService private readonly layoutService: ILayoutService ) { super(BrowserEditor.ID, group, telemetryService, themeService, storageService); } @@ -378,12 +380,6 @@ export class BrowserEditor extends EditorPane { hasFocus: this._model?.focused ?? false, window: this._model?.focused ? this.window : undefined }))); - - // Automatically call layoutBrowserContainer() when the browser container changes size. - // Be careful to use `ResizeObserver` from the target window to avoid cross-window issues. - const resizeObserver = new this.window.ResizeObserver(() => this.layoutBrowserContainer()); - resizeObserver.observe(this._browserContainer); - this._register(toDisposable(() => resizeObserver.disconnect())); } override async setInput(input: BrowserEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -514,12 +510,11 @@ export class BrowserEditor extends EditorPane { if (targetWindowId === this.window.vscodeWindowId) { // Update CSS variable for size calculations this._browserContainerWrapper.style.setProperty('--zoom-factor', String(getZoomFactor(this.window))); - this.layoutBrowserContainer(); } })); this.updateErrorDisplay(); - this.layoutBrowserContainer(); + this.layout(); this.updateVisibility(); this.doScreenshot(); @@ -1168,21 +1163,28 @@ export class BrowserEditor extends EditorPane { } } - override layout(dimension: Dimension, _position?: IDomPosition): void { + override layout(dimension?: Dimension, _position?: IDomPosition): void { // Layout find widget if it exists - this._findWidget.rawValue?.layout(dimension.width); + if (dimension && this._findWidget.rawValue) { + this._findWidget.rawValue.layout(dimension.width); + } + + const whenContainerStylesLoaded = this.layoutService.whenContainerStylesLoaded(this.window); + if (whenContainerStylesLoaded) { + // In floating windows, we need to ensure that the + // container is ready for us to compute certain + // layout related properties. + whenContainerStylesLoaded.then(() => this.layoutBrowserContainer()); + } else { + this.layoutBrowserContainer(); + } } /** - * This should be called whenever .browser-container changes in size, or when - * there could be any elements, such as the command palette, overlapping with it. - * - * Note that we don't call layoutBrowserContainer() from layout() but instead rely on using a ResizeObserver and on - * making direct calls to it. This is because we have seen cases where the getBoundingClientRect() values of - * the .browser-container element are not correct during layout() calls, especially during "Move into New Window" - * and "Copy into New Window" operations into a different monitor. + * Recompute the layout of the browser container and update the model with the new bounds. + * This should generally only be called via layout() to ensure that the container is ready and all necessary styles are loaded. */ - layoutBrowserContainer(): void { + private layoutBrowserContainer(): void { if (this._model) { this.checkOverlays(); From 0747aea6921df56f3ef517f98c4cbac0550506ec Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Mar 2026 13:16:49 -0800 Subject: [PATCH 316/448] chat: fix stale pending divider headers from persisting (#299868) When templates are reused for different tree items, the DOM content from pending dividers was not being cleaned up. This caused old 'Steering' or 'Queued' divider headers to persist visually even after they were no longer in the list. The fix checks if the previous element in a template was a pending divider, and if so, clears the templateData.value node when the template is reused for a new element. - Adds a check in clearRenderedParts() to clear templateData.value when the previous element was a pending divider - Ensures stale divider headers don't remain visible after pending requests are processed and removed from the queue Fixes https://github.com/microsoft/vscode/issues/299853 (Commit message generated by Copilot) --- .../workbench/contrib/chat/browser/widget/chatListRenderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index ef9919d8f84..9e51d5a6f5c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -657,6 +657,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Fri, 6 Mar 2026 13:23:22 -0800 Subject: [PATCH 317/448] sessions: 'update from VS Code' (#299359) * refactor(AccountWidget): simplify update button logic and styling * feat(AccountWidget): enhance update button logic for embedded app scenarios * fix(AccountWidget): add missing line for clarity in onClick method * embedded app update hint * fix(AccountWidget): remove background image from update button when disabled * Update src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(AccountWidget): refine styles for disabled update button * Update src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(AccountWidget): embedded app update flow with dialog, close, and open VS Code - Show outlined 'Update Available' button when updates are disabled due to embedded app - On click, confirm dialog explains Sessions will close and VS Code will open - Opens VS Code via productService.urlProtocol with windowId=_blank (new empty window) - Closes Sessions window after launching VS Code - Uses secondary outlined button style (border, no fill) for hint state - Inject IOpenerService, IDialogService, INativeHostService - Remove simulation TODOs, use real updateService.state * refactor(update): detect updates in embedded app via canInstall flag - Add optional canInstall field to AvailableForDownload state - Darwin: embedded app runs normal init + scheduled checks via HTTP (no Electron autoUpdater events), sets AvailableForDownload(update, false) - Win32: embedded app skips platform setup, checks via HTTP, sets AvailableForDownload(update, false) when update found - Sessions UI: check canInstall === false for hint button + dialog - Remove DisablementReason.EmbeddedApp (no longer needed) - Non-embedded --sessions mode uses standard update flow unchanged * fix: update AccountWidget fixture with new constructor args * fix: use IHostService instead of INativeHostService for browser layer compliance --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/platform/update/common/update.ts | 5 +-- .../electron-main/updateService.darwin.ts | 24 +++++++---- .../electron-main/updateService.win32.ts | 13 +++++- .../browser/account.contribution.ts | 40 +++++++++++++++++++ .../browser/media/accountWidget.css | 11 +++++ .../test/browser/accountWidget.fixture.ts | 8 +++- 6 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index cbeb3a60888..b5c2b121c64 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -62,14 +62,13 @@ export const enum DisablementReason { MissingConfiguration, InvalidConfiguration, RunningAsAdmin, - EmbeddedApp, } export type Uninitialized = { type: StateType.Uninitialized }; export type Disabled = { type: StateType.Disabled; reason: DisablementReason }; export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean }; -export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate }; +export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate; canInstall?: boolean }; export type Downloading = { type: StateType.Downloading; update?: IUpdate; explicit: boolean; overwrite: boolean; downloadedBytes?: number; totalBytes?: number; startTime?: number }; export type Downloaded = { type: StateType.Downloaded; update: IUpdate; explicit: boolean; overwrite: boolean }; export type Updating = { type: StateType.Updating; update: IUpdate; currentProgress?: number; maxProgress?: number }; @@ -83,7 +82,7 @@ export const State = { Disabled: (reason: DisablementReason): Disabled => ({ type: StateType.Disabled, reason }), Idle: (updateType: UpdateType, error?: string): Idle => ({ type: StateType.Idle, updateType, error }), CheckingForUpdates: (explicit: boolean): CheckingForUpdates => ({ type: StateType.CheckingForUpdates, explicit }), - AvailableForDownload: (update: IUpdate): AvailableForDownload => ({ type: StateType.AvailableForDownload, update }), + AvailableForDownload: (update: IUpdate, canInstall?: boolean): AvailableForDownload => ({ type: StateType.AvailableForDownload, update, canInstall }), Downloading: (update: IUpdate | undefined, explicit: boolean, overwrite: boolean, downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading => ({ type: StateType.Downloading, update, explicit, overwrite, downloadedBytes, totalBytes, startTime }), Downloaded: (update: IUpdate, explicit: boolean, overwrite: boolean): Downloaded => ({ type: StateType.Downloaded, update, explicit, overwrite }), Updating: (update: IUpdate, currentProgress?: number, maxProgress?: number): Updating => ({ type: StateType.Updating, update, currentProgress, maxProgress }), diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 842c6766924..317ae6408bf 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -16,7 +16,7 @@ import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; import { asJson, IRequestService } from '../../request/common/request.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; -import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; +import { AvailableForDownload, IUpdate, State, StateType, UpdateType } from '../common/update.js'; import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; import { INodeProcess } from '../../../base/common/platform.js'; @@ -68,13 +68,15 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } protected override async initialize(): Promise { + await super.initialize(); + + // In the embedded app we still want to detect available updates via HTTP, + // but we must not wire up Electron's autoUpdater (which auto-downloads). if ((process as INodeProcess).isEmbeddedApp) { - this.setState(State.Disabled(DisablementReason.EmbeddedApp)); - this.logService.info('update#ctor - updates are disabled from embedded app'); + this.logService.info('update#ctor - embedded app: checking for updates without auto-download'); return; } - await super.initialize(); this.onRawError(this.onError, this, this.disposables); this.onRawCheckingForUpdate(this.onCheckingForUpdate, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); @@ -135,6 +137,13 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } + // In the embedded app, always check without triggering Electron's auto-download. + if ((process as INodeProcess).isEmbeddedApp) { + this.logService.info('update#doCheckForUpdates - embedded app: checking for update without auto-download'); + this.checkForUpdateNoDownload(url, /* canInstall */ false); + return; + } + // When connection is metered and this is not an explicit check, avoid electron call as to not to trigger auto-download. if (!explicit && this.meteredConnectionService.isConnectionMetered) { this.logService.info('update#doCheckForUpdates - checking for update without auto-download because connection is metered'); @@ -148,9 +157,10 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau /** * Manually check the update feed URL without triggering Electron's auto-download. - * Used when connection is metered to show update availability without downloading. + * Used when connection is metered or in the embedded app. + * @param canInstall When false, signals that the update cannot be installed from this app. */ - private async checkForUpdateNoDownload(url: string): Promise { + private async checkForUpdateNoDownload(url: string, canInstall?: boolean): Promise { const headers = getUpdateRequestHeaders(this.productService.version); this.logService.trace('update#checkForUpdateNoDownload - checking update server', { url, headers }); @@ -165,7 +175,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.setState(State.Idle(UpdateType.Archive)); } else { this.logService.trace('update#checkForUpdateNoDownload - update available', { version: update.version, productVersion: update.productVersion }); - this.setState(State.AvailableForDownload(update)); + this.setState(State.AvailableForDownload(update, canInstall)); } } catch (err) { this.logService.error('update#checkForUpdateNoDownload - failed to check for update', err); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 25535f21252..7933d7f675b 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -99,9 +99,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async initialize(): Promise { + // In the embedded app, skip win32-specific setup (cache paths, telemetry) + // but still run the base initialization to detect available updates. if ((process as INodeProcess).isEmbeddedApp) { - this.setState(State.Disabled(DisablementReason.EmbeddedApp)); - this.logService.info('update#ctor - updates are disabled from embedded app'); + this.logService.info('update#ctor - embedded app: checking for updates without auto-download'); + await super.initialize(); return; } @@ -227,6 +229,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } + // In the embedded app, signal that an update exists but can't be installed here. + if ((process as INodeProcess).isEmbeddedApp) { + this.logService.info('update#doCheckForUpdates - embedded app: update available, skipping download'); + this.setState(State.AvailableForDownload(update, /* canInstall */ false)); + return Promise.resolve(null); + } + // When connection is metered and this is not an explicit check, // show update is available but don't start downloading if (!explicit && this.meteredConnectionService.isConnectionMetered) { diff --git a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts index 75afc565594..c4d28d53385 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts +++ b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts @@ -26,6 +26,10 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IHostService } from '../../../../workbench/services/host/browser/host.js'; +import { URI } from '../../../../base/common/uri.js'; import { UpdateHoverWidget } from './updateHoverWidget.js'; // --- Account Menu Items --- // @@ -101,6 +105,9 @@ export class AccountWidget extends ActionViewItem { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHoverService private readonly hoverService: IHoverService, @IProductService private readonly productService: IProductService, + @IOpenerService private readonly openerService: IOpenerService, + @IDialogService private readonly dialogService: IDialogService, + @IHostService private readonly hostService: IHostService, ) { super(undefined, action, { ...options, icon: false, label: false }); this.updateHoverWidget = new UpdateHoverWidget(this.updateService, this.productService, this.hoverService); @@ -188,6 +195,19 @@ export class AccountWidget extends ActionViewItem { } const state = this.updateService.state; + + // In the embedded app, updates are detected but cannot be installed directly. + // Show a hint button to update via VS Code only when an update is actually available. + if (state.type === StateType.AvailableForDownload && state.canInstall === false) { + this.updateButton.element.classList.remove('hidden'); + this.updateButton.element.classList.remove('account-widget-update-button-ready'); + this.updateButton.element.classList.add('account-widget-update-button-hint'); + this.updateButton.enabled = true; + this.updateButton.label = localize('updateAvailable', "Update Available"); + this.updateButton.element.title = localize('updateInVSCodeHover', "Updates are managed by VS Code. Click to open VS Code."); + return; + } + if (this.shouldHideUpdateButton(state.type)) { this.clearUpdateButtonStyling(); this.updateButton.element.classList.add('hidden'); @@ -239,9 +259,29 @@ export class AccountWidget extends ActionViewItem { } private async update(): Promise { + const state = this.updateService.state; + if (state.type === StateType.AvailableForDownload && state.canInstall === false) { + const { confirmed } = await this.dialogService.confirm({ + message: localize('updateFromVSCode.title', "Update from VS Code"), + detail: localize('updateFromVSCode.detail', "This will close the Sessions app and open VS Code so you can install the update.\n\nLaunch Sessions again after the update is complete."), + primaryButton: localize('updateFromVSCode.open', "Close and Open VS Code"), + }); + if (confirmed) { + await this.openVSCode(); + await this.hostService.close(); + } + return; + } await this.updateService.quitAndInstall(); } + private async openVSCode(): Promise { + await this.openerService.open(URI.from({ + scheme: this.productService.urlProtocol, + query: 'windowId=_blank', + }), { openExternal: true }); + } + override onClick(): void { // Handled by custom click handlers diff --git a/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css b/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css index aeff16819c7..3e852ea53ba 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css +++ b/src/vs/sessions/contrib/accountMenu/browser/media/accountWidget.css @@ -67,6 +67,17 @@ color: var(--vscode-button-foreground) !important; } +/* Boxed hint style for embedded app update indicator — outlined, no fill */ +.account-widget-update .account-widget-update-button.account-widget-update-button-hint { + background-color: transparent !important; + color: var(--vscode-button-foreground) !important; + border: 1px solid var(--vscode-button-background) !important; +} + +.account-widget-update .account-widget-update-button.account-widget-update-button-hint:hover { + background-color: color-mix(in srgb, var(--vscode-button-background) 20%, transparent) !important; +} + .monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update .account-widget-update-button.account-widget-update-button-ready:hover { background-color: var(--vscode-button-background) !important; } diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts index 26c7d3a822d..143b89aad16 100644 --- a/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts +++ b/src/vs/sessions/contrib/accountMenu/test/browser/accountWidget.fixture.ts @@ -13,6 +13,9 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IUpdateService, State, UpdateType } from '../../../../../platform/update/common/update.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IHostService } from '../../../../../workbench/services/host/browser/host.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; import { AccountWidget } from '../../browser/account.contribution.js'; @@ -83,7 +86,10 @@ function renderAccountWidget(ctx: ComponentFixtureContext, state: State, account const contextKeyService = instantiationService.get(IContextKeyService); const hoverService = instantiationService.get(IHoverService); const productService = instantiationService.get(IProductService); - const widget = new AccountWidget(action, {}, mockAccountService, mockUpdateService, contextMenuService, menuService, contextKeyService, hoverService, productService); + const openerService = instantiationService.get(IOpenerService); + const dialogService = instantiationService.get(IDialogService); + const hostService = instantiationService.get(IHostService); + const widget = new AccountWidget(action, {}, mockAccountService, mockUpdateService, contextMenuService, menuService, contextKeyService, hoverService, productService, openerService, dialogService, hostService); ctx.disposableStore.add(widget); widget.render(ctx.container); } From f3680f6a81e375c4224cc3490fcae10b17552ac0 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 6 Mar 2026 16:38:40 -0500 Subject: [PATCH 318/448] Support rendering reserved output separately (#299867) * Support rendering reserved output separately * Fix some of the progress bar logic * Better handling for reserve --- .../api/browser/mainThreadChatAgents2.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatAgents2.ts | 1 + .../viewPane/chatContextUsageDetails.ts | 42 +++++++++--- .../viewPane/chatContextUsageWidget.ts | 34 ++++++---- .../media/chatContextUsageDetails.css | 64 +++++++++++++++++++ .../chat/common/chatService/chatService.ts | 1 + ...ode.proposed.chatParticipantAdditions.d.ts | 6 ++ 8 files changed, 130 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 9459b611a98..b851f3fe206 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -411,6 +411,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA kind: 'usage', promptTokens: progress.promptTokens, completionTokens: progress.completionTokens, + outputBuffer: progress.outputBuffer, promptTokenDetails: progress.promptTokenDetails }); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index be954021047..1f3304a5d75 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2528,6 +2528,7 @@ export interface IChatUsageDto { kind: 'usage'; promptTokens: number; completionTokens: number; + outputBuffer?: number; promptTokenDetails?: readonly { category: string; label: string; percentageOfPrompt: number }[]; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 5edee47784f..d91ae3ce3f0 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -440,6 +440,7 @@ export class ChatAgentResponseStream { kind: 'usage', promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, + outputBuffer: usage.outputBuffer, promptTokenDetails: usage.promptTokenDetails }; _report(dto); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 9c63561bb60..47fe2a59f2c 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -22,8 +22,10 @@ export interface IChatContextUsagePromptTokenDetail { export interface IChatContextUsageData { usedTokens: number; + completionTokens: number; totalContextWindow: number; percentage: number; + outputBufferPercentage?: number; promptTokenDetails?: readonly IChatContextUsagePromptTokenDetail[]; } @@ -39,6 +41,8 @@ export class ChatContextUsageDetails extends Disposable { private readonly percentageLabel: HTMLElement; private readonly tokenCountLabel: HTMLElement; private readonly progressFill: HTMLElement; + private readonly outputBufferFill: HTMLElement; + private readonly outputBufferLegend: HTMLElement; private readonly tokenDetailsContainer: HTMLElement; private readonly warningMessage: HTMLElement; private readonly actionsSection: HTMLElement; @@ -67,6 +71,14 @@ export class ChatContextUsageDetails extends Disposable { // Progress bar const progressBar = this.quotaItem.appendChild($('.quota-bar')); this.progressFill = progressBar.appendChild($('.quota-bit')); + this.outputBufferFill = progressBar.appendChild($('.quota-bit.output-buffer')); + + // Output buffer legend (shown only when outputBuffer is provided) + this.outputBufferLegend = this.quotaItem.appendChild($('.output-buffer-legend')); + this.outputBufferLegend.appendChild($('.output-buffer-swatch')); + const legendLabel = this.outputBufferLegend.appendChild($('span')); + legendLabel.textContent = localize('outputReserved', "Reserved for response"); + this.outputBufferLegend.style.display = 'none'; // Token details container (for category breakdown) this.tokenDetailsContainer = this.domNode.appendChild($('.token-details-container')); @@ -98,25 +110,39 @@ export class ChatContextUsageDetails extends Disposable { } update(data: IChatContextUsageData): void { - const { percentage, usedTokens, totalContextWindow, promptTokenDetails } = data; + const { percentage, usedTokens, totalContextWindow, outputBufferPercentage, promptTokenDetails } = data; - // Update token count and percentage + // Update token count and percentage — reflects actual usage only this.tokenCountLabel.textContent = localize( 'tokenCount', "{0} / {1} tokens", this.formatTokenCount(usedTokens, 1), this.formatTokenCount(totalContextWindow, 0) ); - this.percentageLabel.textContent = localize('quotaDisplay', "{0}%", percentage.toFixed(0)); + this.percentageLabel.textContent = localize('quotaDisplay', "{0}%", Math.min(100, percentage).toFixed(0)); - // Update progress bar - this.progressFill.style.width = `${Math.min(100, percentage)}%`; + // Progress bar: actual usage fill + remaining reserved output fill + const usageBarWidth = Math.max(0, Math.min(100, percentage)); + this.progressFill.style.width = `${usageBarWidth}%`; - // Update color classes based on usage level on the quota item + if (outputBufferPercentage !== undefined && outputBufferPercentage > 0) { + // Clamp so the reserve never overflows the bar + this.outputBufferFill.style.width = `${Math.max(0, Math.min(100 - usageBarWidth, outputBufferPercentage))}%`; + this.outputBufferFill.style.display = ''; + this.outputBufferLegend.style.display = ''; + } else { + this.outputBufferFill.style.width = '0'; + this.outputBufferFill.style.display = 'none'; + this.outputBufferLegend.style.display = 'none'; + } + + // Color classes based on total spoken-for percentage + // (actual usage + remaining reserve) + const effectivePercentage = percentage + (outputBufferPercentage ?? 0); this.quotaItem.classList.remove('warning', 'error'); - if (percentage >= 90) { + if (effectivePercentage >= 90) { this.quotaItem.classList.add('error'); - } else if (percentage >= 75) { + } else if (effectivePercentage >= 75) { this.quotaItem.classList.add('warning'); } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index e051fbcfb3f..0bd56a8fe13 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -274,32 +274,42 @@ export class ChatContextUsageWidget extends Disposable { } const promptTokens = usage.promptTokens; + const completionTokens = usage.completionTokens; const promptTokenDetails = usage.promptTokenDetails; + const outputBuffer = usage.outputBuffer; const totalContextWindow = maxInputTokens + maxOutputTokens; - const usedTokens = promptTokens + maxOutputTokens; - const percentage = Math.min(100, (usedTokens / totalContextWindow) * 100); + const usedTokens = promptTokens + completionTokens; + const percentage = (usedTokens / totalContextWindow) * 100; - this.render(percentage, usedTokens, totalContextWindow, promptTokenDetails); + // Remaining reserve = whatever the model reserved minus what completions + // have already consumed. Once completions exceed the reserve, it drops to 0. + const outputBufferPercentage = outputBuffer !== undefined + ? (Math.max(0, outputBuffer - completionTokens) / totalContextWindow) * 100 + : undefined; + + this.render(percentage, completionTokens, usedTokens, totalContextWindow, outputBufferPercentage, promptTokenDetails); this.show(); } - private render(percentage: number, usedTokens: number, totalContextWindow: number, promptTokenDetails?: readonly { category: string; label: string; percentageOfPrompt: number }[]): void { + private render(percentage: number, completionTokens: number, usedTokens: number, totalContextWindow: number, outputBufferPercentage: number | undefined, promptTokenDetails?: readonly { category: string; label: string; percentageOfPrompt: number }[]): void { // Store current data for use in details popup - this.currentData = { usedTokens, totalContextWindow, percentage, promptTokenDetails }; + this.currentData = { usedTokens, completionTokens, totalContextWindow, percentage, outputBufferPercentage, promptTokenDetails }; - // Update pie chart progress - this.progressIndicator.setProgress(percentage); + // Pie chart shows actual usage + remaining reserve so the user can see + // how much of the context window is spoken for. + this.progressIndicator.setProgress(percentage + (outputBufferPercentage ?? 0)); - // Update percentage label and aria-label - const roundedPercentage = Math.round(percentage); + // Update percentage label and aria-label (clamp display to 100) + const roundedPercentage = Math.min(100, Math.round(percentage)); this.percentageLabel.textContent = `${roundedPercentage}%`; this.domNode.setAttribute('aria-label', localize('contextUsagePercentageLabel', "Context window usage: {0}%", roundedPercentage)); - // Update color based on usage level + // Color based on total spoken-for percentage (usage + remaining reserve) + const effectivePercentage = percentage + (outputBufferPercentage ?? 0); this.domNode.classList.remove('warning', 'error'); - if (percentage >= 90) { + if (effectivePercentage >= 90) { this.domNode.classList.add('error'); - } else if (percentage >= 75) { + } else if (effectivePercentage >= 75) { this.domNode.classList.add('warning'); } } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css index 21dd9bcb7d4..53344c162f3 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css @@ -52,6 +52,7 @@ border-radius: 4px; border: 1px solid var(--vscode-gauge-border); margin: 4px 0; + display: flex; } .chat-context-usage-details .quota-indicator .quota-bar .quota-bit { @@ -61,6 +62,45 @@ transition: width 0.3s ease; } +.chat-context-usage-details .quota-indicator .quota-bar .quota-bit.output-buffer { + background: repeating-linear-gradient( + -45deg, + var(--vscode-gauge-foreground), + var(--vscode-gauge-foreground) 2px, + transparent 2px, + transparent 4px + ); + border-radius: 0 4px 4px 0; +} + +.chat-context-usage-details .quota-indicator .quota-bar .quota-bit:not(.output-buffer):has(+ .quota-bit.output-buffer:not([style*="display: none"])) { + border-radius: 4px 0 0 4px; +} + +/* Output buffer legend */ +.chat-context-usage-details .quota-indicator .output-buffer-legend { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 11px; + color: var(--vscode-descriptionForeground); +} + +.chat-context-usage-details .quota-indicator .output-buffer-legend .output-buffer-swatch { + width: 12px; + height: 8px; + border-radius: 2px; + background: repeating-linear-gradient( + -45deg, + var(--vscode-gauge-foreground), + var(--vscode-gauge-foreground) 2px, + transparent 2px, + transparent 4px + ); + flex-shrink: 0; +} + .chat-context-usage-details .quota-indicator.warning .quota-bar { background-color: var(--vscode-gauge-warningBackground); } @@ -69,6 +109,16 @@ background-color: var(--vscode-gauge-warningForeground); } +.chat-context-usage-details .quota-indicator.warning .quota-bar .quota-bit.output-buffer { + background: repeating-linear-gradient( + -45deg, + var(--vscode-gauge-warningForeground), + var(--vscode-gauge-warningForeground) 2px, + transparent 2px, + transparent 4px + ); +} + .chat-context-usage-details .quota-indicator.error .quota-bar { background-color: var(--vscode-gauge-errorBackground); } @@ -77,6 +127,16 @@ background-color: var(--vscode-gauge-errorForeground); } +.chat-context-usage-details .quota-indicator.error .quota-bar .quota-bit.output-buffer { + background: repeating-linear-gradient( + -45deg, + var(--vscode-gauge-errorForeground), + var(--vscode-gauge-errorForeground) 2px, + transparent 2px, + transparent 4px + ); +} + /* Description / warning text — matching ChatStatusDashboard */ .chat-context-usage-details div.description { font-size: 11px; @@ -100,6 +160,10 @@ font-weight: 600; } +.chat-context-usage-details .token-category:first-child .token-category-header { + margin-top: 8px; +} + .chat-context-usage-details .token-detail-item { display: flex; justify-content: space-between; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 4d1fd79be9f..b33bde89eb8 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -151,6 +151,7 @@ export interface IChatUsagePromptTokenDetail { export interface IChatUsage { promptTokens: number; completionTokens: number; + outputBuffer?: number; promptTokenDetails?: readonly IChatUsagePromptTokenDetail[]; kind: 'usage'; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 9143c72c08d..286b87dd85c 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -839,6 +839,12 @@ declare module 'vscode' { */ readonly completionTokens: number; + /** + * The number of tokens reserved for the response. + * This is rendered specially in the UI to indicate that these tokens aren't used but are reserved. + */ + readonly outputBuffer?: number; + /** * Optional breakdown of prompt token usage by category and label. * If the percentages do not sum to 100%, the remaining will be shown as "Uncategorized". From 8fb2a54f4561281fdffa5e8ed09397c3f3690e00 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Mar 2026 17:37:10 -0500 Subject: [PATCH 319/448] dispose of all terminals for session on dispose or archive (#299816) --- .../agentSessions/agentSessionsService.ts | 5 + .../browser/tools/runInTerminalTool.ts | 104 ++++++++--- .../runInTerminalTool.test.ts | 172 ++++++++++++++++-- 3 files changed, 243 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts index efdfc49fa93..3bce41c7d02 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -13,6 +14,7 @@ export interface IAgentSessionsService { readonly _serviceBrand: undefined; readonly model: IAgentSessionsModel; + readonly onDidChangeSessionArchivedState: Event; getSession(resource: URI): IAgentSession | undefined; } @@ -20,11 +22,14 @@ export interface IAgentSessionsService { export class AgentSessionsService extends Disposable implements IAgentSessionsService { declare readonly _serviceBrand: undefined; + private readonly _onDidChangeSessionArchivedState = this._register(new Emitter()); + readonly onDidChangeSessionArchivedState = this._onDidChangeSessionArchivedState.event; private _model: IAgentSessionsModel | undefined; get model(): IAgentSessionsModel { if (!this._model) { this._model = this._register(this.instantiationService.createInstance(AgentSessionsModel)); + this._register(this._model.onDidChangeSessionArchivedState(session => this._onDidChangeSessionArchivedState.fire(session))); this._model.resolve(undefined /* all providers */); } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index fd8fcd1f37a..2947937e8e5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -10,7 +10,7 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Event } from '../../../../../../base/common/event.js'; import { MarkdownString, type IMarkdownString } from '../../../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { basename, posix, win32 } from '../../../../../../base/common/path.js'; import { OperatingSystem, OS } from '../../../../../../base/common/platform.js'; @@ -69,6 +69,7 @@ import { TerminalChatCommandId } from '../../../chat/browser/terminalChat.js'; import { clamp } from '../../../../../../base/common/numbers.js'; import { IOutputAnalyzer } from './outputAnalyzer.js'; import { SandboxOutputAnalyzer } from './sandboxOutputAnalyzer.js'; +import { IAgentSessionsService } from '../../../../chat/browser/agentSessions/agentSessionsService.js'; // #region Tool data @@ -322,8 +323,11 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { private readonly _commandLineAnalyzers: ICommandLineAnalyzer[]; private readonly _commandLinePresenters: ICommandLinePresenter[]; private readonly _outputAnalyzers: IOutputAnalyzer[]; + private readonly _archivedSessionListener = this._register(new MutableDisposable()); protected readonly _sessionTerminalAssociations = new ResourceMap(); + protected readonly _sessionTerminalInstances = new ResourceMap>(); + private readonly _terminalsBeingDisposedBySessionCleanup = new Set(); // Immutable window state protected readonly _osBackend: Promise; @@ -373,6 +377,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { @ITerminalService private readonly _terminalService: ITerminalService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, ) { super(); @@ -417,11 +422,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { // Restore terminal associations from storage this._restoreTerminalAssociations(); this._register(this._terminalService.onDidDisposeInstance(e => { - for (const [sessionResource, toolTerminal] of this._sessionTerminalAssociations.entries()) { - if (e === toolTerminal.instance) { - this._sessionTerminalAssociations.delete(sessionResource); - } - } + this._removeTerminalAssociations(e); })); // Listen for chat session disposal to clean up associated terminals @@ -430,6 +431,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._cleanupSessionTerminals(resource); } })); + } async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise { @@ -1108,7 +1110,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance); this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionResource, toolTerminal.instance); this._registerInputListener(toolTerminal); - this._sessionTerminalAssociations.set(chatSessionResource, toolTerminal); + this._addSessionTerminalAssociation(chatSessionResource, toolTerminal); if (token.isCancellationRequested) { toolTerminal.instance.dispose(); throw new CancellationError(); @@ -1149,7 +1151,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { shellIntegrationQuality: association.shellIntegrationQuality, isBackground: association.isBackground }; - this._sessionTerminalAssociations.set(chatSessionResource, toolTerminal); + this._addSessionTerminalAssociation(chatSessionResource, toolTerminal); this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionResource, instance); // Listen for terminal disposal to clean up storage @@ -1220,23 +1222,83 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } private _cleanupSessionTerminals(chatSessionResource: URI): void { + const sessionTerminals = this._sessionTerminalInstances.get(chatSessionResource); const toolTerminal = this._sessionTerminalAssociations.get(chatSessionResource); - if (toolTerminal) { - this._logService.debug(`RunInTerminalTool: Cleaning up terminal for disposed chat session ${chatSessionResource}`); + const terminalsToDispose = sessionTerminals ?? (toolTerminal ? new Set([toolTerminal.instance]) : undefined); + if (!terminalsToDispose || terminalsToDispose.size === 0) { + return; + } - this._sessionTerminalAssociations.delete(chatSessionResource); - toolTerminal.instance.dispose(); + this._logService.debug(`RunInTerminalTool: Cleaning up ${terminalsToDispose.size} terminal(s) for ended chat session ${chatSessionResource}`); - // Clean up any active executions associated with this session - const terminalToRemove: string[] = []; - for (const [termId, execution] of RunInTerminalTool._activeExecutions.entries()) { - if (execution.instance === toolTerminal.instance) { - execution.dispose(); - terminalToRemove.push(termId); - } + this._sessionTerminalAssociations.delete(chatSessionResource); + this._sessionTerminalInstances.delete(chatSessionResource); + + for (const terminal of terminalsToDispose) { + // Skip redundant map walks in onDidDispose since this session has already been removed. + this._terminalsBeingDisposedBySessionCleanup.add(terminal); + terminal.dispose(); + } + + // Clean up any active executions associated with this session + const terminalToRemove: string[] = []; + for (const [termId, execution] of RunInTerminalTool._activeExecutions.entries()) { + if (terminalsToDispose.has(execution.instance)) { + execution.dispose(); + terminalToRemove.push(termId); } - for (const termId of terminalToRemove) { - RunInTerminalTool._activeExecutions.delete(termId); + } + for (const termId of terminalToRemove) { + RunInTerminalTool._activeExecutions.delete(termId); + } + } + + private _addSessionTerminalAssociation(chatSessionResource: URI, toolTerminal: IToolTerminal): void { + this._ensureArchivedSessionListener(); + + let sessionTerminals = this._sessionTerminalInstances.get(chatSessionResource); + if (!sessionTerminals) { + sessionTerminals = new Set(); + this._sessionTerminalInstances.set(chatSessionResource, sessionTerminals); + } + sessionTerminals.add(toolTerminal.instance); + + if (!toolTerminal.isBackground) { + this._sessionTerminalAssociations.set(chatSessionResource, toolTerminal); + } + } + + private _ensureArchivedSessionListener(): void { + if (this._archivedSessionListener.value) { + return; + } + + // Archiving a session does not fire onDidDisposeSession, but we still need to dispose + // any terminals associated with the archived session to avoid process accumulation. + this._archivedSessionListener.value = this._agentSessionsService.onDidChangeSessionArchivedState(session => { + if (session.isArchived()) { + this._cleanupSessionTerminals(session.resource); + } + }); + } + + private _removeTerminalAssociations(terminal: ITerminalInstance): void { + if (this._terminalsBeingDisposedBySessionCleanup.delete(terminal)) { + return; + } + + for (const [sessionResource, toolTerminal] of this._sessionTerminalAssociations.entries()) { + if (terminal === toolTerminal.instance) { + this._sessionTerminalAssociations.delete(sessionResource); + } + } + + for (const [sessionResource, sessionTerminals] of this._sessionTerminalInstances.entries()) { + if (!sessionTerminals.delete(terminal)) { + continue; + } + if (sessionTerminals.size === 0) { + this._sessionTerminalInstances.delete(sessionResource); } } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 48ff9c22554..43c58ddd915 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -41,11 +41,14 @@ import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js'; import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; import { TerminalChatService } from '../../../chat/browser/terminalChatService.js'; import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { IAgentSessionsService } from '../../../../chat/browser/agentSessions/agentSessionsService.js'; +import { IAgentSession } from '../../../../chat/browser/agentSessions/agentSessionsModel.js'; class TestRunInTerminalTool extends RunInTerminalTool { protected override _osBackend: Promise = Promise.resolve(OperatingSystem.Windows); get sessionTerminalAssociations() { return this._sessionTerminalAssociations; } + get sessionTerminalInstances() { return this._sessionTerminalInstances; } get profileFetcher() { return this._profileFetcher; } setBackendOs(os: OperatingSystem) { @@ -63,6 +66,7 @@ suite('RunInTerminalTool', () => { let workspaceContextService: TestContextService; let terminalServiceDisposeEmitter: Emitter; let chatServiceDisposeEmitter: Emitter<{ sessionResource: URI[]; reason: 'cleared' }>; + let chatSessionArchivedEmitter: Emitter; let runInTerminalTool: TestRunInTerminalTool; @@ -79,6 +83,7 @@ suite('RunInTerminalTool', () => { setConfig(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, 'outsideWorkspace'); terminalServiceDisposeEmitter = new Emitter(); chatServiceDisposeEmitter = new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>(); + chatSessionArchivedEmitter = new Emitter(); instantiationService = workbenchInstantiationService({ configurationService: () => configurationService, @@ -89,6 +94,12 @@ suite('RunInTerminalTool', () => { onDidDisposeSession: chatServiceDisposeEmitter.event, getSession: () => undefined, }); + instantiationService.stub(IAgentSessionsService, { + onDidChangeSessionArchivedState: chatSessionArchivedEmitter.event, + model: { + onDidChangeSessionArchivedState: chatSessionArchivedEmitter.event, + } as IAgentSessionsService['model'] + }); instantiationService.stub(ITerminalChatService, store.add(instantiationService.createInstance(TerminalChatService))); instantiationService.stub(IWorkspaceContextService, workspaceContextService); instantiationService.stub(IHistoryService, { @@ -505,8 +516,7 @@ suite('RunInTerminalTool', () => { // Verify that auto-approve information is included ok(result?.toolSpecificData, 'Expected toolSpecificData to be defined'); - // eslint-disable-next-line local/code-no-any-casts - const terminalData = result!.toolSpecificData as any; + const terminalData = result!.toolSpecificData as IChatTerminalToolInvocationData; ok(terminalData.autoApproveInfo, 'Expected autoApproveInfo to be defined for auto-approved background command'); ok(terminalData.autoApproveInfo.value, 'Expected autoApproveInfo to have a value'); ok(terminalData.autoApproveInfo.value.includes('npm'), 'Expected autoApproveInfo to mention the approved rule'); @@ -1140,13 +1150,149 @@ suite('RunInTerminalTool', () => { }); suite('chat session disposal cleanup', () => { + const createMockTerminal = (processId: number): ITerminalInstance => ({ + dispose: () => { /* Mock dispose */ }, + processId + } as unknown as ITerminalInstance); + + test('should restore all terminals into the session terminal map and dispose them when archived', () => { + const sessionId = 'test-session-restored-archive'; + const sessionResource = LocalChatSessionUri.forSession(sessionId); + + let terminal1Disposed = false; + let terminal2Disposed = false; + const terminal1DisposedEmitter = new Emitter(); + const terminal2DisposedEmitter = new Emitter(); + const mockTerminal1 = { + dispose: () => { + terminal1Disposed = true; + terminal1DisposedEmitter.fire(); + }, + onDisposed: terminal1DisposedEmitter.event, + processId: 55555, + } as unknown as ITerminalInstance; + const mockTerminal2 = { + dispose: () => { + terminal2Disposed = true; + terminal2DisposedEmitter.fire(); + }, + onDisposed: terminal2DisposedEmitter.event, + processId: 66666, + } as unknown as ITerminalInstance; + + storageService.store('chat.terminalSessions', JSON.stringify({ + [mockTerminal1.processId!]: { + sessionId, + id: 'restored-1', + shellIntegrationQuality: ShellIntegrationQuality.None, + isBackground: true, + }, + [mockTerminal2.processId!]: { + sessionId, + id: 'restored-2', + shellIntegrationQuality: ShellIntegrationQuality.None, + isBackground: false, + } + }), StorageScope.WORKSPACE, StorageTarget.USER); + + instantiationService.stub(ITerminalService, { + onDidDisposeInstance: terminalServiceDisposeEmitter.event, + instances: [mockTerminal1, mockTerminal2], + setNextCommandId: async () => { } + }); + + const restoredRunInTerminalTool = store.add(instantiationService.createInstance(TestRunInTerminalTool)); + const restoredSessionTerminals = restoredRunInTerminalTool.sessionTerminalInstances.get(sessionResource); + strictEqual(restoredSessionTerminals?.size, 2, 'Both restored terminals should be tracked for the session'); + + chatSessionArchivedEmitter.fire({ + resource: sessionResource, + isArchived: () => true, + } as unknown as IAgentSession); + + strictEqual(terminal1Disposed, true, 'Restored background terminal should have been disposed'); + strictEqual(terminal2Disposed, true, 'Restored foreground terminal should have been disposed'); + ok(!restoredRunInTerminalTool.sessionTerminalAssociations.has(sessionResource), 'Foreground terminal association should be removed after archive'); + ok(!restoredRunInTerminalTool.sessionTerminalInstances.has(sessionResource), 'All restored terminals for the session should be removed after archive'); + }); + + test('should dispose all terminals associated with a single chat session when archived', () => { + const sessionId = 'test-session-archive'; + const sessionResource = LocalChatSessionUri.forSession(sessionId); + const mockTerminal1 = { dispose: () => { /* Mock dispose */ }, processId: 33333 } as unknown as ITerminalInstance; + const mockTerminal2 = { dispose: () => { /* Mock dispose */ }, processId: 44444 } as unknown as ITerminalInstance; + + let terminal1Disposed = false; + let terminal2Disposed = false; + mockTerminal1.dispose = () => { terminal1Disposed = true; }; + mockTerminal2.dispose = () => { terminal2Disposed = true; }; + + runInTerminalTool.sessionTerminalAssociations.set(sessionResource, { + instance: mockTerminal2, + shellIntegrationQuality: ShellIntegrationQuality.None + }); + runInTerminalTool.sessionTerminalInstances.set(sessionResource, new Set([mockTerminal1, mockTerminal2])); + + // Initialize lazy archive listener before firing the archive event. + const ensureArchivedSessionListener = (runInTerminalTool as unknown as Record void>)['_ensureArchivedSessionListener']; + ensureArchivedSessionListener.call(runInTerminalTool); + + chatSessionArchivedEmitter.fire({ + resource: sessionResource, + isArchived: () => true, + } as unknown as IAgentSession); + + strictEqual(terminal1Disposed, true, 'Terminal 1 should have been disposed'); + strictEqual(terminal2Disposed, true, 'Terminal 2 should have been disposed'); + ok(!runInTerminalTool.sessionTerminalAssociations.has(sessionResource), 'Terminal association should be removed after archive'); + ok(!runInTerminalTool.sessionTerminalInstances.has(sessionResource), 'All tracked terminals for the session should be removed after archive'); + }); + + test('should not access agent sessions model when initializing archive listener', () => { + let modelAccessed = false; + instantiationService.stub(IAgentSessionsService, { + onDidChangeSessionArchivedState: chatSessionArchivedEmitter.event, + get model() { + modelAccessed = true; + throw new Error('model should not be accessed when wiring archive listener'); + }, + } as unknown as IAgentSessionsService); + + const noModelAccessRunInTerminalTool = store.add(instantiationService.createInstance(TestRunInTerminalTool)); + const ensureArchivedSessionListener = (noModelAccessRunInTerminalTool as unknown as Record void>)['_ensureArchivedSessionListener']; + ensureArchivedSessionListener.call(noModelAccessRunInTerminalTool); + + strictEqual(modelAccessed, false, 'Agent sessions model should not be accessed when initializing archive listener'); + }); + + test('should dispose all terminals associated with a single chat session', () => { + const sessionId = 'test-session-multiple-terminals'; + const mockTerminal1 = createMockTerminal(11111); + const mockTerminal2 = createMockTerminal(22222); + + let terminal1Disposed = false; + let terminal2Disposed = false; + mockTerminal1.dispose = () => { terminal1Disposed = true; }; + mockTerminal2.dispose = () => { terminal2Disposed = true; }; + + const sessionResource = LocalChatSessionUri.forSession(sessionId); + runInTerminalTool.sessionTerminalAssociations.set(sessionResource, { + instance: mockTerminal2, + shellIntegrationQuality: ShellIntegrationQuality.None + }); + runInTerminalTool.sessionTerminalInstances.set(sessionResource, new Set([mockTerminal1, mockTerminal2])); + + chatServiceDisposeEmitter.fire({ sessionResource: [sessionResource], reason: 'cleared' }); + + strictEqual(terminal1Disposed, true, 'Terminal 1 should have been disposed'); + strictEqual(terminal2Disposed, true, 'Terminal 2 should have been disposed'); + ok(!runInTerminalTool.sessionTerminalAssociations.has(sessionResource), 'Terminal association should be removed after disposal'); + ok(!runInTerminalTool.sessionTerminalInstances.has(sessionResource), 'All tracked terminals for the session should be removed after disposal'); + }); + test('should dispose associated terminals when chat session is disposed', () => { const sessionId = 'test-session-123'; - // eslint-disable-next-line local/code-no-any-casts - const mockTerminal: ITerminalInstance = { - dispose: () => { /* Mock dispose */ }, - processId: 12345 - } as any; + const mockTerminal = createMockTerminal(12345); let terminalDisposed = false; mockTerminal.dispose = () => { terminalDisposed = true; }; @@ -1167,16 +1313,8 @@ suite('RunInTerminalTool', () => { test('should not affect other sessions when one session is disposed', () => { const sessionId1 = 'test-session-1'; const sessionId2 = 'test-session-2'; - // eslint-disable-next-line local/code-no-any-casts - const mockTerminal1: ITerminalInstance = { - dispose: () => { /* Mock dispose */ }, - processId: 12345 - } as any; - // eslint-disable-next-line local/code-no-any-casts - const mockTerminal2: ITerminalInstance = { - dispose: () => { /* Mock dispose */ }, - processId: 67890 - } as any; + const mockTerminal1 = createMockTerminal(12345); + const mockTerminal2 = createMockTerminal(67890); let terminal1Disposed = false; let terminal2Disposed = false; From 3c7c3102080666d91e2db20344e64b084d4d7221 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Mar 2026 14:50:43 -0800 Subject: [PATCH 320/448] Cherry pick cd11faec7b031b928bc5ec37f350d623ffb28713 (#299875) --- src/vs/workbench/contrib/mcp/common/mcpRegistry.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 20500e68f87..d9ff9dad84a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -26,6 +26,7 @@ import { observableConfigValue } from '../../../../platform/observable/common/pl import { IQuickInputButton, IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; @@ -87,6 +88,8 @@ export class McpRegistry extends Disposable implements IMcpRegistry { @ILabelService private readonly _labelService: ILabelService, @ILogService private readonly _logService: ILogService, @IMcpSandboxService private readonly _mcpSandboxService: IMcpSandboxService, + @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, ) { super(); this._mcpAccessValue = observableConfigValue(mcpAccessConfig, McpAccessValue.All, configurationService); @@ -215,6 +218,14 @@ export class McpRegistry extends Disposable implements IMcpRegistry { autoTrustChanges = false, errorOnUserInteraction = false, }: IMcpResolveConnectionOptions) { + if (collection.scope === StorageScope.WORKSPACE && !this._workspaceTrustManagementService.isWorkspaceTrusted()) { + if (errorOnUserInteraction) { + throw new UserInteractionRequiredError('workspaceTrust'); + } else if (!await this._workspaceTrustRequestService.requestWorkspaceTrust({ message: localize('runTrust', "This MCP server definition is defined in your workspace files.") })) { + return false; + } + } + if (collection.trustBehavior === McpServerTrust.Kind.Trusted) { this._logService.trace(`MCP server ${definition.id} is trusted, no trust prompt needed`); return true; From 9fc3d42d22bce4b6d57feb442e417c74aaa856d0 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Fri, 6 Mar 2026 14:52:29 -0800 Subject: [PATCH 321/448] Pass internalOrg to buildUpdateFeedUrl when checking latest version (#299883) --- src/vs/platform/update/electron-main/abstractUpdateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 3e5956faa22..06a42fe3485 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -317,7 +317,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return undefined; } - const url = this.buildUpdateFeedUrl(this.quality, commit ?? this.productService.commit!); + const url = this.buildUpdateFeedUrl(this.quality, commit ?? this.productService.commit!, { internalOrg: this.getInternalOrg() }); if (!url) { return undefined; From 57479c0e8adadf63699e58f55c21251e793caaf2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Mar 2026 14:52:44 -0800 Subject: [PATCH 322/448] mcp: fix concurrent request response collection race (#299628) * mcp: fix concurrent request response collection race - JsonRpcProtocol.handleMessage now returns JsonRpcMessage[] containing responses generated by incoming requests, rather than delegating response collection to callers via side-channel state - McpGatewaySession simplified by removing _pendingResponses and _isCollectingPostResponses fields, which were susceptible to racing under concurrent HTTP POSTs. Now directly uses handleMessage's return value for the response body - _send callback still invoked for all messages (backward compatible with McpServerRequestHandler and SSE notification broadcast) - Updated tests to assert on handleMessage return values Fixes #297780 (Commit message generated by Copilot) * mcp: address review comments on jsonRpcProtocol changes - Adds JSDoc to handleMessage clarifying what is returned (only responses for incoming requests), ordering guarantees for batch inputs, and that responses are still emitted via _send callback to avoid double-sending - Tightens _handleRequest return type from Promise to Promise, enforcing that only valid responses are returned. Introduces JsonRpcResponse type alias for better type safety - Expands error handling tests to assert that returned replies match what is emitted via _send for both JsonRpcError and generic error paths Fixes #297780 (Commit message generated by Copilot) --- src/vs/base/common/jsonRpcProtocol.ts | 58 ++++++++++++++----- .../base/test/common/jsonRpcProtocol.test.ts | 42 +++++++++----- src/vs/platform/mcp/node/mcpGatewaySession.ts | 19 +----- 3 files changed, 71 insertions(+), 48 deletions(-) diff --git a/src/vs/base/common/jsonRpcProtocol.ts b/src/vs/base/common/jsonRpcProtocol.ts index 67c4ed4fc4d..35d7144ba82 100644 --- a/src/vs/base/common/jsonRpcProtocol.ts +++ b/src/vs/base/common/jsonRpcProtocol.ts @@ -43,6 +43,7 @@ export interface IJsonRpcErrorResponse { } export type JsonRpcMessage = IJsonRpcRequest | IJsonRpcNotification | IJsonRpcSuccessResponse | IJsonRpcErrorResponse; +export type JsonRpcResponse = IJsonRpcSuccessResponse | IJsonRpcErrorResponse; interface IPendingRequest { promise: DeferredPromise; @@ -122,15 +123,31 @@ export class JsonRpcProtocol extends Disposable { }) as Promise; } - public async handleMessage(message: JsonRpcMessage | JsonRpcMessage[]): Promise { + /** + * Handles one or more incoming JSON-RPC messages. + * + * Returns an array of JSON-RPC response objects generated for any incoming + * requests in the message(s). Notifications and responses to our own + * outgoing requests do not produce return values. For batch inputs, the + * returned responses are in the same order as the corresponding requests. + * + * Note: responses are also emitted via the `_send` callback, so callers + * that rely on the return value should not re-send them. + */ + public async handleMessage(message: JsonRpcMessage | JsonRpcMessage[]): Promise { if (Array.isArray(message)) { + const replies: JsonRpcResponse[] = []; for (const single of message) { - await this._handleMessage(single); + const reply = await this._handleMessage(single); + if (reply) { + replies.push(reply); + } } - return; + return replies; } - await this._handleMessage(message); + const reply = await this._handleMessage(message); + return reply ? [reply] : []; } public cancelPendingRequest(id: JsonRpcId): void { @@ -152,22 +169,25 @@ export class JsonRpcProtocol extends Disposable { } } - private async _handleMessage(message: JsonRpcMessage): Promise { + private async _handleMessage(message: JsonRpcMessage): Promise { if (isJsonRpcResponse(message)) { if (hasKey(message, { result: true })) { this._handleResult(message); } else { this._handleError(message); } + return undefined; } if (isJsonRpcRequest(message)) { - await this._handleRequest(message); + return this._handleRequest(message); } if (isJsonRpcNotification(message)) { this._handlers.handleNotification?.(message); } + + return undefined; } private _handleResult(response: IJsonRpcSuccessResponse): void { @@ -192,17 +212,18 @@ export class JsonRpcProtocol extends Disposable { } } - private async _handleRequest(request: IJsonRpcRequest): Promise { + private async _handleRequest(request: IJsonRpcRequest): Promise { if (!this._handlers.handleRequest) { - this._send({ + const response: IJsonRpcErrorResponse = { jsonrpc: '2.0', id: request.id, error: { code: JsonRpcProtocol.MethodNotFound, message: `Method not found: ${request.method}`, } - }); - return; + }; + this._send(response); + return response; } const cts = new CancellationTokenSource(); @@ -211,14 +232,17 @@ export class JsonRpcProtocol extends Disposable { try { const resultOrThenable = this._handlers.handleRequest(request, cts.token); const result = isThenable(resultOrThenable) ? await resultOrThenable : resultOrThenable; - this._send({ + const response: IJsonRpcSuccessResponse = { jsonrpc: '2.0', id: request.id, result, - }); + }; + this._send(response); + return response; } catch (error) { + let response: IJsonRpcErrorResponse; if (error instanceof JsonRpcError) { - this._send({ + response = { jsonrpc: '2.0', id: request.id, error: { @@ -226,17 +250,19 @@ export class JsonRpcProtocol extends Disposable { message: error.message, data: error.data, } - }); + }; } else { - this._send({ + response = { jsonrpc: '2.0', id: request.id, error: { code: JsonRpcProtocol.InternalError, message: error instanceof Error ? error.message : 'Internal error', } - }); + }; } + this._send(response); + return response; } finally { cts.dispose(true); } diff --git a/src/vs/base/test/common/jsonRpcProtocol.test.ts b/src/vs/base/test/common/jsonRpcProtocol.test.ts index 4a167d2cc8a..9a000e35f48 100644 --- a/src/vs/base/test/common/jsonRpcProtocol.test.ts +++ b/src/vs/base/test/common/jsonRpcProtocol.test.ts @@ -39,7 +39,7 @@ suite('JsonRpcProtocol', () => { const requestPromise = protocol.sendRequest({ method: 'echo', params: { value: 'ok' } }); const outgoingRequest = sentMessages[0] as IJsonRpcRequest; - await protocol.handleMessage({ + const replies = await protocol.handleMessage({ jsonrpc: '2.0', id: outgoingRequest.id, result: 'done' @@ -47,6 +47,7 @@ suite('JsonRpcProtocol', () => { const result = await requestPromise; assert.strictEqual(result, 'done'); + assert.deepStrictEqual(replies, []); }); test('sendRequest rejects on error response', async () => { @@ -107,20 +108,22 @@ suite('JsonRpcProtocol', () => { test('handleRequest responds with method not found without handler', async () => { const { protocol, sentMessages } = createProtocol(); - await protocol.handleMessage({ + const replies = await protocol.handleMessage({ jsonrpc: '2.0', id: 7, method: 'unknown' }); - assert.deepStrictEqual(sentMessages, [{ + const expected = [{ jsonrpc: '2.0', id: 7, error: { code: -32601, message: 'Method not found: unknown' } - }]); + }]; + assert.deepStrictEqual(sentMessages, expected); + assert.deepStrictEqual(replies, expected); }); test('handleRequest responds with result and passes cancellation token', async () => { @@ -134,7 +137,7 @@ suite('JsonRpcProtocol', () => { } }); - await protocol.handleMessage({ + const replies = await protocol.handleMessage({ jsonrpc: '2.0', id: 9, method: 'compute' @@ -142,27 +145,29 @@ suite('JsonRpcProtocol', () => { assert.ok(receivedToken); assert.strictEqual(wasCanceledDuringHandler, false); - assert.deepStrictEqual(sentMessages, [{ + const expected = [{ jsonrpc: '2.0', id: 9, result: 'compute:ok' - }]); + }]; + assert.deepStrictEqual(sentMessages, expected); + assert.deepStrictEqual(replies, expected); }); - test('handleRequest serializes JsonRpcError', async () => { + test('handleRequest serializes JsonRpcError and returns it', async () => { const { protocol, sentMessages } = createProtocol({ handleRequest: () => { throw new JsonRpcError(88, 'bad request', { detail: true }); } }); - await protocol.handleMessage({ + const replies = await protocol.handleMessage({ jsonrpc: '2.0', id: 'a', method: 'boom' }); - assert.deepStrictEqual(sentMessages, [{ + const expected = [{ jsonrpc: '2.0', id: 'a', error: { @@ -170,30 +175,34 @@ suite('JsonRpcProtocol', () => { message: 'bad request', data: { detail: true } } - }]); + }]; + assert.deepStrictEqual(sentMessages, expected); + assert.deepStrictEqual(replies, expected); }); - test('handleRequest maps unknown errors to internal error', async () => { + test('handleRequest maps unknown errors to internal error and returns it', async () => { const { protocol, sentMessages } = createProtocol({ handleRequest: () => { throw new Error('unexpected'); } }); - await protocol.handleMessage({ + const replies = await protocol.handleMessage({ jsonrpc: '2.0', id: 'b', method: 'explode' }); - assert.deepStrictEqual(sentMessages, [{ + const expected = [{ jsonrpc: '2.0', id: 'b', error: { code: -32603, message: 'unexpected' } - }]); + }]; + assert.deepStrictEqual(sentMessages, expected); + assert.deepStrictEqual(replies, expected); }); test('handleMessage processes batch sequentially', async () => { @@ -225,8 +234,9 @@ suite('JsonRpcProtocol', () => { assert.deepStrictEqual(sequence, ['request:start']); gate.complete(); - await handlingPromise; + const replies = await handlingPromise; assert.deepStrictEqual(sequence, ['request:start', 'request:end', 'notification']); + assert.deepStrictEqual(replies, [{ jsonrpc: '2.0', id: 1, result: true }]); }); }); diff --git a/src/vs/platform/mcp/node/mcpGatewaySession.ts b/src/vs/platform/mcp/node/mcpGatewaySession.ts index 579b0184495..20f6d23dc71 100644 --- a/src/vs/platform/mcp/node/mcpGatewaySession.ts +++ b/src/vs/platform/mcp/node/mcpGatewaySession.ts @@ -6,7 +6,7 @@ import type * as http from 'http'; import { IJsonRpcNotification, IJsonRpcRequest, - isJsonRpcNotification, isJsonRpcResponse, JsonRpcError, JsonRpcMessage, JsonRpcProtocol + isJsonRpcNotification, isJsonRpcResponse, JsonRpcError, JsonRpcMessage, JsonRpcProtocol, JsonRpcResponse } from '../../../base/common/jsonRpcProtocol.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { hasKey } from '../../../base/common/types.js'; @@ -79,8 +79,6 @@ function encodeResourceUrisInContent(content: MCP.ContentBlock[], serverIndex: n export class McpGatewaySession extends Disposable { private readonly _rpc: JsonRpcProtocol; private readonly _sseClients = new Set(); - private readonly _pendingResponses: JsonRpcMessage[] = []; - private _isCollectingPostResponses = false; private _lastEventId = 0; private _isInitialized = false; @@ -136,16 +134,8 @@ export class McpGatewaySession extends Disposable { }); } - public async handleIncoming(message: JsonRpcMessage | JsonRpcMessage[]): Promise { - this._pendingResponses.length = 0; - this._isCollectingPostResponses = true; - try { - await this._rpc.handleMessage(message); - return [...this._pendingResponses]; - } finally { - this._isCollectingPostResponses = false; - this._pendingResponses.length = 0; - } + public async handleIncoming(message: JsonRpcMessage | JsonRpcMessage[]): Promise { + return this._rpc.handleMessage(message); } public override dispose(): void { @@ -162,9 +152,6 @@ export class McpGatewaySession extends Disposable { private _handleOutgoingMessage(message: JsonRpcMessage): void { if (isJsonRpcResponse(message)) { - if (this._isCollectingPostResponses) { - this._pendingResponses.push(message); - } this._logService.debug(`[McpGateway][session ${this.id}] --> response: ${JSON.stringify(message)}`); return; } From 19b032d2ff68faf05f34599076d30b97d534d940 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Mar 2026 15:24:05 -0800 Subject: [PATCH 323/448] chat: toggle queue/steer keybindings based on context (#299885) * chat: toggle queue/steer keybindings based on context - Splits EditingRequestType.QueueOrSteer into Queue and Steer to track which type of pending message is being edited - Keybindings now respect the chat.requestQueuing.defaultAction setting: when steer is default, Enter=Steer and Alt+Enter=Queue; when queue is default, the bindings swap - When editing a queued or steer message, Enter always submits with the same type, regardless of the config setting. This ensures pressing Enter to save an edit keeps the message in its original queue category - Updates chatWidget to set the specific editing type based on the pending message's kind - Simplifies keybinding logic with effectiveDefault conditions that account for both config and editing context Fixes #297454 (Commit message generated by Copilot) * pr comments --- .../browser/actions/chatExecuteActions.ts | 3 +- .../chat/browser/actions/chatQueueActions.ts | 47 +++++++++++++++++-- .../contrib/chat/browser/widget/chatWidget.ts | 6 ++- .../chat/common/actions/chatContextKeys.ts | 3 +- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index b1d898bbcb1..907a1145b99 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -744,7 +744,8 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { constructor() { const notInProgressOrEditing = ContextKeyExpr.and( ContextKeyExpr.or(whenNotInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), - ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.QueueOrSteer) + ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Queue), + ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Steer) ); const menuCondition = ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index 0ad56d59669..3eac2679afa 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -13,15 +13,34 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js'; +import { ChatConfiguration } from '../../common/constants.js'; import { isRequestVM } from '../../common/model/chatViewModel.js'; import { IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; +const editingQueue = ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Queue); +const editingSteer = ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Steer); +const editingQueueOrSteer = ContextKeyExpr.or(editingQueue, editingSteer)!; + const queuingActionsPresent = ContextKeyExpr.and( - ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)), + ContextKeyExpr.or(ChatContextKeys.requestInProgress, editingQueueOrSteer), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent), ); +const steerIsDefault = ContextKeyExpr.equals(`config.${ChatConfiguration.RequestQueueingDefaultAction}`, 'steer'); +const queueIsDefault = steerIsDefault.negate(); + +// The effective default respects the editing context: when editing a queued/steer +// message, the default matches that message type regardless of the config setting. +const effectiveDefaultIsQueue = ContextKeyExpr.or( + ContextKeyExpr.and(queueIsDefault, editingQueueOrSteer.negate()), + editingQueue +); +const effectiveDefaultIsSteer = ContextKeyExpr.or( + ContextKeyExpr.and(steerIsDefault, editingQueueOrSteer.negate()), + editingSteer +); + export interface IChatRemovePendingRequestContext { sessionResource: URI; pendingRequestId: string; @@ -52,14 +71,23 @@ export class ChatQueueMessageAction extends Action2 { queuingActionsPresent, ChatContextKeys.inputHasText, ), - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ChatContextKeys.inChatInput, queuingActionsPresent, + effectiveDefaultIsSteer, ), primary: KeyMod.Alt | KeyCode.Enter, weight: KeybindingWeight.EditorContrib + 1 - }, + }, { + when: ContextKeyExpr.and( + ChatContextKeys.inChatInput, + queuingActionsPresent, + effectiveDefaultIsQueue, + ), + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + 1 + }], }); } @@ -94,14 +122,23 @@ export class ChatSteerWithMessageAction extends Action2 { queuingActionsPresent, ChatContextKeys.inputHasText, ), - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ChatContextKeys.inChatInput, queuingActionsPresent, + effectiveDefaultIsSteer, ), primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib + 1 - }, + }, { + when: ContextKeyExpr.and( + ChatContextKeys.inChatInput, + queuingActionsPresent, + effectiveDefaultIsQueue, + ), + primary: KeyMod.Alt | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + 1 + }], }); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index a26b7b4efb6..81716d9c338 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1542,7 +1542,11 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.currentlyEditing.bindTo(item.contextKeyService).set(true); } - const isEditingSentRequest = currentElement.pendingKind === undefined ? ChatContextKeys.EditingRequestType.Sent : ChatContextKeys.EditingRequestType.QueueOrSteer; + const isEditingSentRequest = currentElement.pendingKind === undefined + ? ChatContextKeys.EditingRequestType.Sent + : currentElement.pendingKind === ChatRequestQueueKind.Queued + ? ChatContextKeys.EditingRequestType.Queue + : ChatContextKeys.EditingRequestType.Steer; const isInput = this.configurationService.getValue('chat.editRequests') === 'input'; this.inputPart?.setEditing(!!this.viewModel?.editing && isInput, isEditingSentRequest); diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 33f7a4a9f7a..7b5018b7f44 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -23,7 +23,8 @@ export namespace ChatContextKeys { export const enum EditingRequestType { Sent = 's', - QueueOrSteer = 'qs', + Queue = 'q', + Steer = 'st', } export const editingRequestType = new RawContextKey('chatEditingSentRequest', undefined, { type: 'string', description: localize('chatEditingSentRequest', "The type of the current editing request.") }); From 175e346f3fb99de026ba7f9bb6542669ec309f76 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Mar 2026 15:38:18 -0800 Subject: [PATCH 324/448] fix: remove chat-pending-dragging class on re-render to fix opacity issue (#299886) * fix: remove chat-pending-dragging class on re-render to fix opacity issue Fixes #297473 The 'chat-pending-dragging' class (which sets opacity: 0.4) was not being removed when elements were re-rendered. This caused messages to randomly appear with lower opacity if they had been dragged before the list was updated. The fix adds 'chat-pending-dragging' to the classList.remove() call that clears pending-related classes during re-render. * fix: move class cleanup before pending divider rendering Addresses review feedback: the chat-pending-dragging class was only being removed on the normal render path. If a recycled template was rendered as a pending divider, it would return early before the cleanup code ran, leaving the class and opacity stuck at 0.4. This moves the class cleanup to run before the isPendingDividerVM check, ensuring it's always applied regardless of element type. --- .../chat/browser/widget/chatListRenderer.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 9e51d5a6f5c..727179577cf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -685,6 +685,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Sat, 7 Mar 2026 01:37:29 +0100 Subject: [PATCH 325/448] Make variable resolvers based on environment including launch config env (#299752) --- src/vs/server/node/remoteTerminalChannel.ts | 2 +- .../contrib/terminal/browser/terminalProcessManager.ts | 7 ++++++- .../terminal/test/browser/terminalProcessManager.test.ts | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index a455eba4267..ab6f98f0256 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -226,7 +226,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< const activeWorkspaceFolder = args.activeWorkspaceFolder ? reviveWorkspaceFolder(args.activeWorkspaceFolder) : undefined; const activeFileResource = args.activeFileResource ? URI.revive(uriTransformer.transformIncoming(args.activeFileResource)) : undefined; const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables, this._extensionManagementService); - const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, process.env, customVariableResolver); + const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, baseEnv, customVariableResolver); // Get the initial cwd const initialCwd = await terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 2cc3d43b763..1733e24d83e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -261,7 +261,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this.backend = backend; // Create variable resolver - const variableResolver = terminalEnvironment.createVariableResolver(this._cwdWorkspaceFolder, await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority), this._configurationResolverService); + // Start with the full base environment so that all standard variables (e.g. PATH) are + // available, then overlay the shell environment on top so that launch configuration + // variables and shell-profile modifications take precedence. + const envForResolver = { ...await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority) }; + terminalEnvironment.mergeEnvironments(envForResolver, await backend.getShellEnvironment()); + const variableResolver = terminalEnvironment.createVariableResolver(this._cwdWorkspaceFolder, envForResolver, this._configurationResolverService); // resolvedUserHome is needed here as remote resolvers can launch local terminals before // they're connected to the remote. diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index 70ed6a4426e..f029f371a90 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -69,7 +69,8 @@ class TestTerminalInstanceService implements Partial { options: any, shouldPersist: boolean ) => new TestTerminalChildProcess(shouldPersist), - getLatency: () => Promise.resolve([]) + getLatency: () => Promise.resolve([]), + getShellEnvironment: () => Promise.resolve({}) } as unknown as ITerminalBackend; } } From 966c59037a27a15f15cfd0a46d6ea73efa1329c5 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 6 Mar 2026 17:06:24 -0800 Subject: [PATCH 326/448] Add sessionId to telemetry (#299895) * Add sessionId to telemetry * CCR comments --- .../chat/common/chatService/chatService.ts | 6 +++++ .../common/chatService/chatServiceImpl.ts | 24 +++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index b33bde89eb8..bc6f5672bba 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1490,6 +1490,7 @@ export type ChatStopCancellationNoopEvent = { pendingRequests: number; sessionScheme?: string; lastRequestId?: string; + sessionId?: string; }; export type ChatStopCancellationNoopClassification = { @@ -1499,6 +1500,7 @@ export type ChatStopCancellationNoopClassification = { pendingRequests: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of queued pending requests at no-op time when known.'; isMeasurement: true }; sessionScheme?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The URI scheme of the session resource (e.g. vscodeLocalChatSession vs remote).' }; lastRequestId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the last request in the session, for correlating with tool invocations.' }; + sessionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat session ID.' }; owner: 'roblourens'; comment: 'Tracks possible no-op stop cancellation paths.'; }; @@ -1508,11 +1510,15 @@ export const ChatPendingRequestChangeEventName = 'chat.pendingRequestChange'; export type ChatPendingRequestChangeEvent = { action: 'add' | 'remove' | 'notCancelable'; source: string; + requestId?: string; + sessionId?: string; }; export type ChatPendingRequestChangeClassification = { action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a pending request was added or removed.' }; source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The method that triggered the pending request change.' }; + requestId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The request ID associated with the pending request change.' }; + sessionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat session ID.' }; owner: 'roblourens'; comment: 'Tracks pending request lifecycle changes in the chat service.'; }; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index a1e1d22d80c..ed16f7aa579 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -46,7 +46,7 @@ import { IChatSessionsService } from '../chatSessionsService.js'; import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js'; import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; import { IChatTransferService } from '../model/chatTransferService.js'; -import { LocalChatSessionUri } from '../model/chatUri.js'; +import { chatSessionResourceToId, LocalChatSessionUri } from '../model/chatUri.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../languageModels.js'; @@ -681,7 +681,7 @@ export class ChatService extends Disposable implements IChatService { if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) { const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); this._pendingRequests.set(model.sessionResource, initialCancellationRequest); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', sessionId: chatSessionResourceToId(model.sessionResource) }); const cancellationListener = disposables.add(new MutableDisposable()); const createCancellationListener = (token: CancellationToken) => { @@ -691,7 +691,7 @@ export class ChatService extends Disposable implements IChatService { // User cancelled the interruption const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); this._pendingRequests.set(model.sessionResource, newCancellationRequest); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', sessionId: chatSessionResourceToId(model.sessionResource) }); cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token); } }); @@ -721,7 +721,7 @@ export class ChatService extends Disposable implements IChatService { } })); } else { - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'notCancelable', source: 'remoteSession' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'notCancelable', source: 'remoteSession', sessionId: chatSessionResourceToId(model.sessionResource) }); if (lastRequest && model.editingSession) { // wait for timeline to load so that a 'changes' part is added when the response completes await chatEditingSessionIsReady(model.editingSession); @@ -1108,6 +1108,9 @@ export class ChatService extends Disposable implements IChatService { } })); pendingRequest.requestId ??= requestProps.requestId; + if (pendingRequest.requestId) { + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequestId', requestId: pendingRequest.requestId, sessionId: chatSessionResourceToId(sessionResource) }); + } } completeResponseCreated(); @@ -1229,11 +1232,11 @@ export class ChatService extends Disposable implements IChatService { // Note- requestId is not known at this point, assigned later const cancellableRequest = this.instantiationService.createInstance(CancellableRequest, source, undefined); this._pendingRequests.set(model.sessionResource, cancellableRequest); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequest' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequest', sessionId: chatSessionResourceToId(model.sessionResource) }); rawResponsePromise.finally(() => { if (this._pendingRequests.get(model.sessionResource) === cancellableRequest) { this._pendingRequests.deleteAndDispose(model.sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'sendRequestComplete' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'sendRequestComplete', requestId: cancellableRequest.requestId, sessionId: chatSessionResourceToId(model.sessionResource) }); } // Process the next pending request from the queue if any if (shouldProcessPending) { @@ -1434,7 +1437,7 @@ export class ChatService extends Disposable implements IChatService { if (pendingRequest?.requestId === requestId) { pendingRequest.cancel(); this._pendingRequests.deleteAndDispose(sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'removeRequest' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'removeRequest', requestId, sessionId: chatSessionResourceToId(model.sessionResource) }); } model.removeRequest(requestId); @@ -1457,8 +1460,8 @@ export class ChatService extends Disposable implements IChatService { if (cts) { cts.requestId = request.id; this._pendingRequests.set(target.sessionResource, cts); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'adoptRequest' }); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'adoptRequest' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'adoptRequest', requestId: request.id, sessionId: chatSessionResourceToId(oldOwner.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'adoptRequest', requestId: request.id, sessionId: chatSessionResourceToId(target.sessionResource) }); } } } @@ -1505,6 +1508,7 @@ export class ChatService extends Disposable implements IChatService { pendingRequests: pendingRequestsCount, sessionScheme: sessionResource.scheme, lastRequestId: lastRequest?.id, + sessionId: chatSessionResourceToId(sessionResource), }); this.info('cancelCurrentRequestForSession', `No pending request was found for session ${sessionResource}. requestInProgress=${requestInProgress ?? 'unknown'}, pendingRequests=${pendingRequestsCount}`); return; @@ -1512,7 +1516,7 @@ export class ChatService extends Disposable implements IChatService { pendingRequest.cancel(); this._pendingRequests.deleteAndDispose(sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: source ?? 'cancelRequest' }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: source ?? 'cancelRequest', requestId: pendingRequest.requestId, sessionId: chatSessionResourceToId(sessionResource) }); } setYieldRequested(sessionResource: URI): void { From 3154150df5204a1c70a3f6afa9da97ed01c7eb46 Mon Sep 17 00:00:00 2001 From: Robo Date: Sat, 7 Mar 2026 10:26:01 +0900 Subject: [PATCH 327/448] chore: bump electron@39.8.0 (#299669) * chore: bump electron@39.8.0 * chore: bump distro --- .npmrc | 4 +- build/checksums/electron.txt | 150 ++++++++++++++++----------------- build/gulpfile.vscode.win32.ts | 2 +- cgmanifest.json | 6 +- package-lock.json | 8 +- package.json | 4 +- 6 files changed, 87 insertions(+), 87 deletions(-) diff --git a/.npmrc b/.npmrc index b07eade64d5..a275846ab5c 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="39.6.0" -ms_build_id="13330601" +target="39.8.0" +ms_build_id="13470701" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 3df57a48a97..5d8343f42a9 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -1a1bb622d9788793310458b7bf9eedcea8347da9556dd1d7661b757c15ebfdd5 *chromedriver-v39.6.0-darwin-arm64.zip -c84565c127adeca567ca69e85bbd8f387fff1f83c09e69f6f851528f5602dc4e *chromedriver-v39.6.0-darwin-x64.zip -f50df11f99a2e3df84560d5331608cd0a9d7a147a1490f25edfd8a95531918a2 *chromedriver-v39.6.0-linux-arm64.zip -a571fd25e33f3b3bded91506732a688319d93eb652e959bb19a09cd3f67f9e5f *chromedriver-v39.6.0-linux-armv7l.zip -2a50751190bbfe07984f7d8cbf2f12c257a4c132a36922a78c4e320169b8f498 *chromedriver-v39.6.0-linux-x64.zip -cf6034c20b727c48a6f44bb87b1ec89fd4189f56200a32cd39cedaab3f19e007 *chromedriver-v39.6.0-mas-arm64.zip -d2107db701c41fa5f3aaa04c279275ac4dcffde4542c032c806939acd8c6cd6c *chromedriver-v39.6.0-mas-x64.zip -1593ed5550fa11c549fd4ff5baea5cb7806548bff15b79340343ac24a86d6de3 *chromedriver-v39.6.0-win32-arm64.zip -deee89cbeed935a57551294fbc59f6a346b76769e27dd78a59a35a82ae3037d9 *chromedriver-v39.6.0-win32-ia32.zip -f88a23ebc246ed2a506d6d172eb9ffbb4c9d285103285a735e359268fcd08895 *chromedriver-v39.6.0-win32-x64.zip -2e1ec8568f4fda21dc4bb7231cdb0427fa31bb03c4bc39f8aa36659894f2d23e *electron-api.json -03e743428685b44beeab9aa51bad7437387dc2ce299b94745ed8fb0923dd9a07 *electron-v39.6.0-darwin-arm64-dsym-snapshot.zip -723d64530286ebd58539bc29deb65e9334ae8450a714b075d369013b4bbfdce0 *electron-v39.6.0-darwin-arm64-dsym.zip -8f529fbbed8c386f3485614fa059ea9408ebe17d3f0c793269ea52ef3efdf8df *electron-v39.6.0-darwin-arm64-symbols.zip -dace1f9e5c49f4f63f32341f8b0fb7f16b8cf07ce5fcb17abcc0b33782966b8c *electron-v39.6.0-darwin-arm64.zip -e2425514469c4382be374e676edff6779ef98ca1c679b1500337fa58aa863e98 *electron-v39.6.0-darwin-x64-dsym-snapshot.zip -877e72afd7d8695e8a4420a74765d45c30fad30606d3dbab07a0e88fe600e3f6 *electron-v39.6.0-darwin-x64-dsym.zip -ae958c150c6fe76fc7989a28ddb6104851f15d2e24bd32fe60f51e308954a816 *electron-v39.6.0-darwin-x64-symbols.zip -bed88dac3ac28249a020397d83f3f61871c7eaea2099d5bf6b1e92878cb14f19 *electron-v39.6.0-darwin-x64.zip -a86e9470d6084611f38849c9f9b3311584393fa81b55d0bbf7e284a649b729cf *electron-v39.6.0-linux-arm64-debug.zip -e7d7aec3873a6d2f2c9fe406a27a8668910f8b4fdf55a36b5302d9db3ec390db *electron-v39.6.0-linux-arm64-symbols.zip -d6ded47a49046eb031800cf70f2b5d763ccac11dac64e70a874c62aaa115ccba *electron-v39.6.0-linux-arm64.zip -2bf6a75c9f3c2400698c325e48c9b6444d108e4d76544fb130d04605002ae084 *electron-v39.6.0-linux-armv7l-debug.zip -421d02c8a063602b22e4f16a2614fe6cc13e07f9d4ead309fe40aeac296fe951 *electron-v39.6.0-linux-armv7l-symbols.zip -ee34896d1317f1572ed4f3ed8eb1719f599f250d442fc6afb6ec40091c4f4cdc *electron-v39.6.0-linux-armv7l.zip -233f55caae4514144310928248a96bd3a3ce7ac6dc1ff99e7531737a579793b1 *electron-v39.6.0-linux-x64-debug.zip -eca69e741b00ce141b9c2e6e63c1f77cd834a85aa095385f032fdb58d3154fff *electron-v39.6.0-linux-x64-symbols.zip -94bf4bee48f3c657edffd4556abbe62556ca8225cbb4528d62eb858233a3c34b *electron-v39.6.0-linux-x64.zip -6dfebeb760627df74c65ff8da7088fb77e0ae222cab5590fea4cdd37c060ea06 *electron-v39.6.0-mas-arm64-dsym-snapshot.zip -b327d41507546799451a684b6061caed10f1c16ee39a7e686aac71187f8b7afe *electron-v39.6.0-mas-arm64-dsym.zip -02a56a9c3c3522ebc653f03ad88be9a2f46594c730a767a28e7322ddb7a789b7 *electron-v39.6.0-mas-arm64-symbols.zip -2fe93cd39521371bb5722c358feebadc5e79d79628b07a79a00a9d918e261de4 *electron-v39.6.0-mas-arm64.zip -f25ddc8a9b2b699d6d9e54fdf66220514e387ae36e45efeb4d8217b1462503f6 *electron-v39.6.0-mas-x64-dsym-snapshot.zip -6732026b6a3728bea928af0c5928bf82d565eebeb3f5dc5b6991639d27e7c457 *electron-v39.6.0-mas-x64-dsym.zip -5260dabf5b0fc369e0f69d3286fbcce9d67bc65e3364e17f7bb13dd49e320422 *electron-v39.6.0-mas-x64-symbols.zip -905f7cf95270afa92972b6c9242fc50c0afd65ffd475a81ded6033588f27a613 *electron-v39.6.0-mas-x64.zip -9204c9844e89f5ca0b32a8347cf9141d8dcb66671906e299afa06004f464d9b0 *electron-v39.6.0-win32-arm64-pdb.zip -6778c54d8cf7a0d305e4334501c3b877daf4737197187120ac18064f4e093b23 *electron-v39.6.0-win32-arm64-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-arm64-toolchain-profile.zip -22b96aca4cf8f7823b98e3b20b6131e521e0100c5cd03ab76f106eefbd0399cf *electron-v39.6.0-win32-arm64.zip -f5b69c8c1c9349a1f3b4309fb3fa1cf6326953e0807d2063fc27ba9f1400232e *electron-v39.6.0-win32-ia32-pdb.zip -1d6e103869acdeb0330b26ee08089667e0b5afc506efcd7021ba761ed8b786b5 *electron-v39.6.0-win32-ia32-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-ia32-toolchain-profile.zip -2b30e5bc923fff1443e2a4d1971cb9b26f61bd6a454cfbb991042457bab4d623 *electron-v39.6.0-win32-ia32.zip -5f93924c317206a2a4800628854e44e68662a9c40b3457c9e72690d6fff884d3 *electron-v39.6.0-win32-x64-pdb.zip -eab07439f0a21210cd560c1169c04ea5e23c6fe0ab65bd60cffce2b9f69fd36e *electron-v39.6.0-win32-x64-symbols.zip -efec460f92ff99a9d5970dd7a67fd0be5272989cfacc9389dec954c706b23f7d *electron-v39.6.0-win32-x64-toolchain-profile.zip -e8eee36be3bb85ba6fd8fcd26cf3a264bc946ac0717762c64e168896695c8e34 *electron-v39.6.0-win32-x64.zip -2e84c606e40c7bab5530e4c83bbf3a24c28143b0a768dafa5ecf78b18d889297 *electron.d.ts -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.0-darwin-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.0-darwin-x64.zip -52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.6.0-linux-arm64.zip -622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.6.0-linux-armv7l.zip -ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.6.0-linux-x64.zip -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.6.0-mas-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.6.0-mas-x64.zip -2a358c2dbeeb259c0b6a18057b52ffb0109de69112086cb2ce02f3a79bd70cee *ffmpeg-v39.6.0-win32-arm64.zip -4555510880a7b8dff5d5d0520f641665c62494689782adbed67fa0e24b45ae67 *ffmpeg-v39.6.0-win32-ia32.zip -091ab3c97d5a1cda1e04c6bd263a2c07ea63ed7ec3fd06600af6d7e23bbbbe15 *ffmpeg-v39.6.0-win32-x64.zip -650fb5fbc7e6cc27e5caeb016f72aba756469772bbfdfb3ec0b229f973d8ad46 *hunspell_dictionaries.zip -669ef1bf8ed0f6378e67f4f8bc23d2907d7cc1db7369dbdf468e164f4ef49365 *libcxx-objects-v39.6.0-linux-arm64.zip -996d81ad796524246144e15e22ffef75faff055a102c49021d70b03f039c3541 *libcxx-objects-v39.6.0-linux-armv7l.zip -1ffb610613c11169640fa76e4790137034a0deb3b48e2aef51a01c9b96b7700a *libcxx-objects-v39.6.0-linux-x64.zip -6dd8db57473992367c7914b50d06cae3a1b713cc09ceebecfcd4107df333e759 *libcxx_headers.zip -e5c18f813cc64a7d3b0404ee9adeb9cbb49e7ee5e1054b62c71fa7d1a448ad1b *libcxxabi_headers.zip -7f58d6e1d8c75b990f7d2259de8d0896414d0f2cff2f0fe4e5c7f8037d8fe879 *mksnapshot-v39.6.0-darwin-arm64.zip -be1178e4aa1f4910ba2b8f35b5655e12182657b9e32d509b47f0b2db033f0ac5 *mksnapshot-v39.6.0-darwin-x64.zip -5e36a594067fea08bb3d7bcd60873c3e240ebcee2208bcebfbc9f77d3075cc0d *mksnapshot-v39.6.0-linux-arm64-x64.zip -2db9196d2af0148ebb7b6f1f597f46a535b7af482f95739bd1ced78e1ebf39e7 *mksnapshot-v39.6.0-linux-armv7l-x64.zip -cd673e0a908fc950e0b4246e2b099018a8ee879d12a62973a01cb7de522f5bcf *mksnapshot-v39.6.0-linux-x64.zip -0749d8735a1fd8c666862cd7020b81317c45203d01319c9be089d1e750cb2c15 *mksnapshot-v39.6.0-mas-arm64.zip -81ae98e064485f8c6c69cd6c875ee72666c0cc801a8549620d382c2d0cea3b5c *mksnapshot-v39.6.0-mas-x64.zip -2e44f75df797922e7c8bad61a1b41fed14b070a54257a6a751892b2b8b9dfe29 *mksnapshot-v39.6.0-win32-arm64-x64.zip -fb5d73a8bf4b8db80f61b7073aa8458b5c46cce5c2a4b23591e851c6fcbd0144 *mksnapshot-v39.6.0-win32-ia32.zip -118ae88dbcd6b260cfa370e46ccfb0ab00af5efbf59495aaeea56a2831f604b2 *mksnapshot-v39.6.0-win32-x64.zip +d70954386008ad2c65d9849bb89955ab3c7dd08763256ae0d91d8604e8894d64 *chromedriver-v39.8.0-darwin-arm64.zip +2f6b654337133c13440aafdaf9e8b15f5ebb244e7d49f20977f03438e9bb8adb *chromedriver-v39.8.0-darwin-x64.zip +ef8681bb6b6af42cdf0e14c9ce188f035e01620781308c06cd3c6b922aaea2e6 *chromedriver-v39.8.0-linux-arm64.zip +c03fea6ac2b743d771407dc5f58809f44d2a885b1830b847957823cac2e7b222 *chromedriver-v39.8.0-linux-armv7l.zip +4bb7c6d9b3a7bfdd89edd0db98e63599ebf6dacdb888d5985bbb73f6153acc0c *chromedriver-v39.8.0-linux-x64.zip +aad1f6f970b5636d637c1c242766fbaa5bebe2707a605a38aadc7b40724b3d11 *chromedriver-v39.8.0-mas-arm64.zip +e89ebebe3a135d3ce40168152a0aabfd055b9fa6b118262a6df18405fd2ea433 *chromedriver-v39.8.0-mas-x64.zip +232e1a0460f6a59056499cccfff3265bf92eae22f20f02f2419e5e49552aaed7 *chromedriver-v39.8.0-win32-arm64.zip +ab92f46cc55da7c719175b50203c734781828389b8b3a1a535204bf0dc7d1296 *chromedriver-v39.8.0-win32-ia32.zip +a40eb521063e4ea6791ed4005815fa8ac259c1febc850246a83a47ce120121ce *chromedriver-v39.8.0-win32-x64.zip +d6a33b4c3c0de845ea23d1e2614c6c6d3bbe35b771bb63ae521c4db11373b021 *electron-api.json +5425323fdb23167870075e944ec6cf3ae383fbe45ad141d08b1d9689030ccd05 *electron-v39.8.0-darwin-arm64-dsym-snapshot.zip +aa32ab00ee58d8827cd53ca561b8c26b7cb7e2ad8cb0801acdda117ee728388e *electron-v39.8.0-darwin-arm64-dsym.zip +f94e589804a3394a4735543b888927be873f8f402899d0debe32a9dc570d6285 *electron-v39.8.0-darwin-arm64-symbols.zip +681d82c2ec6677ff0bf12f5bb1808b5a51dcbf10894bd0298641015119a3e04d *electron-v39.8.0-darwin-arm64.zip +a95e83b5cde762a37e64229e5669b0c19b95aac148689d96ca344535109eb983 *electron-v39.8.0-darwin-x64-dsym-snapshot.zip +8c989d8ca835ecdd93d49d9627f5548272c0ed03e263392b21ed287960b29e41 *electron-v39.8.0-darwin-x64-dsym.zip +b4b6fda9c5b9063a104318645aa29ef4738dd099da2b722e3e9b6dde5e098418 *electron-v39.8.0-darwin-x64-symbols.zip +ec53f2ba79498410323bb96a19ce98741bf28666cc9d83e07d11dadcc5506f38 *electron-v39.8.0-darwin-x64.zip +9141e64f9d4ea7f0e6a43ae364c8232a0dac79ecec44de2d4a0e5d688fbb742c *electron-v39.8.0-linux-arm64-debug.zip +5fac949d5331abaff0643dbcda7cc187e548cd4bf9d198c1ffc361383bfaa79f *electron-v39.8.0-linux-arm64-symbols.zip +c9db883fa671237fbc16256cf89aba55b9fcfbd9825fec32a6d57724a6446fe1 *electron-v39.8.0-linux-arm64.zip +b26ac10e84f6b7d338c13a38547aa66b5e9afbe2f1355b183ebc2ff8f428cfa9 *electron-v39.8.0-linux-armv7l-debug.zip +16c47c008a8783f6c8d6387fe01ea15425161befbf4211e4667bbdd6bb806ef0 *electron-v39.8.0-linux-armv7l-symbols.zip +b1b37fd450a5081a876c2b00b6ca007d454747a7d1d8f04feb16119d6ace94c6 *electron-v39.8.0-linux-armv7l.zip +1e8039cdf60b27785771c9e3f3c4c39fad37602bb0e6b75a30f83c57fdbef069 *electron-v39.8.0-linux-x64-debug.zip +ff9ca169c6e79649dd4c5a49a82a8d4b1761b62fbe14c15c61bf534381a9f653 *electron-v39.8.0-linux-x64-symbols.zip +854076cc4c63d6d6c320df1ca3f4bd7084ef9f9bb47c7b75d80feb2c2ed920b4 *electron-v39.8.0-linux-x64.zip +91bc313cbd009435552d8d5efff5d6ed0ff15465743c2629dac1cfe99ac34e4d *electron-v39.8.0-mas-arm64-dsym-snapshot.zip +974f10f80ec6c65f8d9f2ac1ccd8c4395bb34e24e2b09dc0ff80bd351099692e *electron-v39.8.0-mas-arm64-dsym.zip +b3878bc9198cff324b7c829ce2fbea7a4ee505f2f99b0bb3c11ac5e60651be59 *electron-v39.8.0-mas-arm64-symbols.zip +48dac99c757a850b0db7b38c1b95e08270f690a7ea1b58872e45308c2f7c8c93 *electron-v39.8.0-mas-arm64.zip +1a6e4df1092f89ed46833938d6dd1b3036640037bd09f0630a369ae386a7c872 *electron-v39.8.0-mas-x64-dsym-snapshot.zip +81425eb867527341af64c00726bd462957fec4d5f073922df891d830addbc5bc *electron-v39.8.0-mas-x64-dsym.zip +748ce154e894a27b117b46354cc288dc9442fade844c637b59fe1c1f3f7c625d *electron-v39.8.0-mas-x64-symbols.zip +91f8f7d4eb1a42ac4fa0eaa93034c8e6155ccb50718f9f55541ce2be4a4ed6d0 *electron-v39.8.0-mas-x64.zip +b775b7584afb84e52b0a770e1e63a2f17384b66eeebe845e0c5c82beacaf7e93 *electron-v39.8.0-win32-arm64-pdb.zip +ac62373d11ed682b4fcdae27de2bd72ebf7d46d3b569f5fcf242de01786d0948 *electron-v39.8.0-win32-arm64-symbols.zip +b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.0-win32-arm64-toolchain-profile.zip +08b79fa5deabbcace447f1e15eb99b3b117b42a84b71ad5b0f52d2da68a34192 *electron-v39.8.0-win32-arm64.zip +f4fb798d76a0c2f80717ef1607571537dbbb07f1cc5f177048bcfd17046c2255 *electron-v39.8.0-win32-ia32-pdb.zip +37c1d2988793604294724b648589fca6459472021189abab1550d5e1eecff1a7 *electron-v39.8.0-win32-ia32-symbols.zip +b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.0-win32-ia32-toolchain-profile.zip +59b70a12abedb550795614bc74c5803787e824da3529a631fdb5c2b5aad00196 *electron-v39.8.0-win32-ia32.zip +0357c6fb0d7198c45cba0e8c939473ea1d971e1efe801bc84e2c559141b368e7 *electron-v39.8.0-win32-x64-pdb.zip +8e6f4e8516d15aecde5244beac315067c13513c7074383086523eef2638a5e8d *electron-v39.8.0-win32-x64-symbols.zip +b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.0-win32-x64-toolchain-profile.zip +9edc111b22aee1a0efb5103d6d3b48645af57b48214eeb48f75f9edfc3e271d6 *electron-v39.8.0-win32-x64.zip +b6eca0e05fcff2464382278dff52367f6f21eb1a580dd8a0a954fc16397ab085 *electron.d.ts +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.8.0-darwin-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.8.0-darwin-x64.zip +52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.8.0-linux-arm64.zip +622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.8.0-linux-armv7l.zip +ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.8.0-linux-x64.zip +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.8.0-mas-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.8.0-mas-x64.zip +3ba7c7507181e0d4836f70f3d8800b4e9ba379e1086e9e89fda7ff9b3b9ad2cb *ffmpeg-v39.8.0-win32-arm64.zip +f37e7d51b8403e2ed8ca192bc6ae759cf63d80010e747b15eeb7120b575578b2 *ffmpeg-v39.8.0-win32-ia32.zip +b252e232438010f9683e8fd10c3bf0631df78e42a6ae11d6cb7aa7e6ac11185f *ffmpeg-v39.8.0-win32-x64.zip +365735192f58a7f7660100227ec348ba3df604415ff5264b54d93cb6cf5f6f6f *hunspell_dictionaries.zip +6384ee31daa39de4dd4bd3aa225cdb14cdddb7f463a2c1663b38a79e122a13e2 *libcxx-objects-v39.8.0-linux-arm64.zip +9748b3272e52a8274fe651def2d6ae2dad7a3771b520dd105f46f4020ba9d63b *libcxx-objects-v39.8.0-linux-armv7l.zip +74d47a155ecc6c2054418c7c3e0540f32b983ebdc65e8b4ea5d3e257d29b3f4f *libcxx-objects-v39.8.0-linux-x64.zip +c0755fbb84011664bd36459fc6e06a603078dccd3b7b260f6ed6ad1d409f79f7 *libcxx_headers.zip +3ea41e9bd56e8f52ab8562c1406ba9416abe3993640935e981cbbd77c0f2654b *libcxxabi_headers.zip +befcd6067f35d911a6a87b927e79dc531cb7bea39e85f86a65e9ab82ef0cece1 *mksnapshot-v39.8.0-darwin-arm64.zip +f0e692655298ffed60630c3e6490ced69e9d8726e85bcaecfa34485f3a991469 *mksnapshot-v39.8.0-darwin-x64.zip +d5d0901cd1eafdf921d2a0d1565829cf60f454a71ce74fa60db98780fd8a1a96 *mksnapshot-v39.8.0-linux-arm64-x64.zip +1bc0a3294d258a59846aa5c5359cd8b0f43831ebd7c3e1dde9a6cfaa39d845bf *mksnapshot-v39.8.0-linux-armv7l-x64.zip +4e414dbe75f460cb34508608db984aa6f4d274f333fa327a3d631da4a516da8f *mksnapshot-v39.8.0-linux-x64.zip +c51c86e3a11ad75fb4f7559798f6d64ec7def19583c96ce08de7ee5796568841 *mksnapshot-v39.8.0-mas-arm64.zip +6544d1e93adea1e9a694f9b9f539d96f84df647d9c9319b29d4fc88751ff9075 *mksnapshot-v39.8.0-mas-x64.zip +372b4685c53f19ccc72c33d78c1283d9389c72f42cd48224439fe4f89199caa0 *mksnapshot-v39.8.0-win32-arm64-x64.zip +199e9244f4522a4a02aece09a6a33887b24d7ec837640d39c930170e4b3caa57 *mksnapshot-v39.8.0-win32-ia32.zip +970e979e7a8b70f300f7854cb571756d9049bc42b44a6153a9ce3a18e1a83243 *mksnapshot-v39.8.0-win32-x64.zip diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 1f525cff35a..0f81323c98d 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -113,7 +113,7 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { Quality: quality }; - const isInsiderOrExploration = false; + const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; const embedded = isInsiderOrExploration ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded : undefined; diff --git a/cgmanifest.json b/cgmanifest.json index 21554434500..1b1e1711ccf 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "a229dbf7a56336b847b34dfff1bac79afc311eee", - "tag": "39.6.0" + "commitHash": "69c8cbf259da0f84e9c1db04958516a68f7170aa", + "tag": "39.8.0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "39.6.0" + "version": "39.8.0" }, { "component": { diff --git a/package-lock.json b/package-lock.json index 5fc1351c51f..34cb015609d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "39.6.0", + "electron": "39.8.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -7879,9 +7879,9 @@ "dev": true }, "node_modules/electron": { - "version": "39.6.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.6.0.tgz", - "integrity": "sha512-KQK3sJ6JCyymY3HQxV0N/bVBQwKQETRW0N/+OYcrL9H6tZhpmTSaZY3qSxcruWrPIuouvoiP3Vk/JKUpw05ZIw==", + "version": "39.8.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.0.tgz", + "integrity": "sha512-K+f3YelSyh9Q4LgUXuIhLB4kq73LJrqnIbe8ih9vpWi+iSdPebj0w7FRYwILCMDoyBQMFC9LicYHuIPmZzdKlg==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 3f5bcc61b9c..a6fdb61d42a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.112.0", - "distro": "cd72f8f27b485d65c99f5020caa895a5ac5692eb", + "distro": "af92bdba4fde517633b51b6706479a636c8d541a", "author": { "name": "Microsoft Corporation" }, @@ -174,7 +174,7 @@ "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "39.6.0", + "electron": "39.8.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", From 8c3784f0ec6e5080820c4948f20680d83b3c4b47 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 6 Mar 2026 18:16:53 -0800 Subject: [PATCH 328/448] Port chat input overflow fix to main (#299901) The part of #299863 that wasn't in main --- .../workbench/contrib/chat/browser/widget/media/chat.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index fbb9c530fbb..cfbf53ea6d8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -820,6 +820,14 @@ have to be updated for changes to the rules above, or to support more deeply nes position: relative; } +/* Prevent contents from covering border corners. Not applied in compact mode + because overflow:hidden creates a BFC that causes a ResizeObserver ↔ layout + feedback loop when toolbars share width with the editor. */ +.interactive-session .interactive-input-part:not(.compact) .chat-input-container { + /* Prevent contents from covering border corner */ + overflow: hidden; +} + /* Context usage widget container - positioned in the secondary toolbar below input */ .interactive-session .chat-input-toolbars .chat-context-usage-container, .interactive-session .chat-secondary-toolbar .chat-context-usage-container { From dee49a91907ccd0616bac9b14433eba3221ee968 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 6 Mar 2026 18:36:00 -0800 Subject: [PATCH 329/448] Force show scrollbar (#299902) * Force show scrollbar * fix --- .../chat/browser/actions/chatActions.ts | 1 + .../chatSetup/chatSetupContributions.ts | 1 + .../browser/widget/input/chatInputPart.ts | 41 ++++++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 98fb8e824f9..a6f6a8952dc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -368,6 +368,7 @@ abstract class OpenChatGlobalAction extends Action2 { if (opts?.query) { if (opts.isPartialQuery) { + chatWidget.input.showScrollbarUntilAccept(); chatWidget.setInput(opts.query); } else { if (!chatWidget.viewModel) { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts index 9c49e9e9f59..4a7157945cc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts @@ -250,6 +250,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr if (options?.inputValue) { const chatWidget = await widgetService.revealWidget(); + chatWidget?.input.showScrollbarUntilAccept(); chatWidget?.setInput(options.inputValue); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 5ed966e748a..85f1d2dc4b3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -35,7 +35,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IEditorConstructionOptions } from '../../../../../../editor/browser/config/editorConfiguration.js'; import { EditorExtensionsRegistry } from '../../../../../../editor/browser/editorExtensions.js'; import { CodeEditorWidget } from '../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js'; +import { EditorOptions, IEditorOptions, IEditorScrollbarOptions } from '../../../../../../editor/common/config/editorOptions.js'; import { IDimension } from '../../../../../../editor/common/core/2d/dimension.js'; import { IPosition } from '../../../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../../../editor/common/core/range.js'; @@ -330,6 +330,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; + private _forceVisibleScrollbarUntilAccept = false; // Reference to the input model for syncing input state private _inputModel: IInputModel | undefined; @@ -1460,6 +1461,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.history.append(this._getFilteredEntry(userQuery)); } + this.resetScrollbarVisibilityAfterAccept(); + if (this._chatSessionIsEmpty) { this._chatSessionIsEmpty = false; this._emptyInputState.set(undefined, undefined); @@ -3203,6 +3206,42 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Fallback for unknown contexts return { location: ChatWidgetLocation.Editor, isMaximized: false }; } + + private getDefaultScrollbarOptions(): IEditorScrollbarOptions { + const scrollbar = this._inputEditor.getRawOptions().scrollbar ?? {}; + return this.options.renderStyle === 'compact' + ? { ...scrollbar, vertical: 'hidden' } + : { ...scrollbar, vertical: 'auto', verticalScrollbarSize: 7 }; + } + + private getVisibleScrollbarOptions(): IEditorScrollbarOptions { + const scrollbar = this._inputEditor.getRawOptions().scrollbar ?? {}; + return this.options.renderStyle === 'compact' + ? { ...scrollbar, vertical: 'hidden' } + : { ...scrollbar, vertical: 'visible', verticalScrollbarSize: 7 }; + } + + private updateInputEditorScrollbarOptions(): void { + this._inputEditor.updateOptions({ + scrollbar: this._forceVisibleScrollbarUntilAccept + ? this.getVisibleScrollbarOptions() + : this.getDefaultScrollbarOptions() + }); + } + + showScrollbarUntilAccept(): void { + this._forceVisibleScrollbarUntilAccept = true; + this.updateInputEditorScrollbarOptions(); + } + + private resetScrollbarVisibilityAfterAccept(): void { + if (!this._forceVisibleScrollbarUntilAccept) { + return; + } + + this._forceVisibleScrollbarUntilAccept = false; + this.updateInputEditorScrollbarOptions(); + } } From 848654f005a406b23baabf3f6818e0db880e3c45 Mon Sep 17 00:00:00 2001 From: Robo Date: Sat, 7 Mar 2026 15:50:34 +0900 Subject: [PATCH 330/448] fix: devtools entrypoint for connecting to extension host (#299905) --- .../extensions/electron-browser/localProcessExtensionHost.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index af93d8558de..93fac9a4889 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -268,7 +268,7 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+):(\d+)\/([^\s]+)/); if (inspectorUrlMatch) { const [, host, port, auth] = inspectorUrlMatch; - const devtoolsUrl = `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${host}:${port}/${auth}`; + const devtoolsUrl = `devtools://devtools/bundled/js_app.html?v8only=true&ws=${host}:${port}/${auth}`; if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { console.debug(`%c[Extension Host] %cdebugger inspector at ${devtoolsUrl}`, 'color: blue', 'color:'); } From c0ebf3e550d22af123594e474bd069c05260732a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:38:09 +0100 Subject: [PATCH 331/448] Sessions - rename changes and git folders (#299936) --- .../browser/changesView.contribution.ts | 0 .../contrib/{changesView => changes}/browser/changesView.ts | 0 .../{changesView => changes}/browser/changesViewActions.ts | 0 .../{changesView => changes}/browser/media/changesView.css | 0 .../browser/media/changesViewActions.css | 0 .../{changesView => changes}/browser/toggleChangesView.ts | 0 .../contrib/{changesView => changes}/common/changes.ts | 0 .../browser/git.contribution.ts} | 0 src/vs/sessions/sessions.desktop.main.ts | 4 ++-- 9 files changed, 2 insertions(+), 2 deletions(-) rename src/vs/sessions/contrib/{changesView => changes}/browser/changesView.contribution.ts (100%) rename src/vs/sessions/contrib/{changesView => changes}/browser/changesView.ts (100%) rename src/vs/sessions/contrib/{changesView => changes}/browser/changesViewActions.ts (100%) rename src/vs/sessions/contrib/{changesView => changes}/browser/media/changesView.css (100%) rename src/vs/sessions/contrib/{changesView => changes}/browser/media/changesViewActions.css (100%) rename src/vs/sessions/contrib/{changesView => changes}/browser/toggleChangesView.ts (100%) rename src/vs/sessions/contrib/{changesView => changes}/common/changes.ts (100%) rename src/vs/sessions/contrib/{gitSync/browser/gitSync.contribution.ts => git/browser/git.contribution.ts} (100%) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.contribution.ts b/src/vs/sessions/contrib/changes/browser/changesView.contribution.ts similarity index 100% rename from src/vs/sessions/contrib/changesView/browser/changesView.contribution.ts rename to src/vs/sessions/contrib/changes/browser/changesView.contribution.ts diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts similarity index 100% rename from src/vs/sessions/contrib/changesView/browser/changesView.ts rename to src/vs/sessions/contrib/changes/browser/changesView.ts diff --git a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts similarity index 100% rename from src/vs/sessions/contrib/changesView/browser/changesViewActions.ts rename to src/vs/sessions/contrib/changes/browser/changesViewActions.ts diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesView.css b/src/vs/sessions/contrib/changes/browser/media/changesView.css similarity index 100% rename from src/vs/sessions/contrib/changesView/browser/media/changesView.css rename to src/vs/sessions/contrib/changes/browser/media/changesView.css diff --git a/src/vs/sessions/contrib/changesView/browser/media/changesViewActions.css b/src/vs/sessions/contrib/changes/browser/media/changesViewActions.css similarity index 100% rename from src/vs/sessions/contrib/changesView/browser/media/changesViewActions.css rename to src/vs/sessions/contrib/changes/browser/media/changesViewActions.css diff --git a/src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts b/src/vs/sessions/contrib/changes/browser/toggleChangesView.ts similarity index 100% rename from src/vs/sessions/contrib/changesView/browser/toggleChangesView.ts rename to src/vs/sessions/contrib/changes/browser/toggleChangesView.ts diff --git a/src/vs/sessions/contrib/changesView/common/changes.ts b/src/vs/sessions/contrib/changes/common/changes.ts similarity index 100% rename from src/vs/sessions/contrib/changesView/common/changes.ts rename to src/vs/sessions/contrib/changes/common/changes.ts diff --git a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts b/src/vs/sessions/contrib/git/browser/git.contribution.ts similarity index 100% rename from src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts rename to src/vs/sessions/contrib/git/browser/git.contribution.ts diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 6d98af657ae..7e1174067c5 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -205,10 +205,10 @@ import './contrib/chat/browser/chat.contribution.js'; import './contrib/chat/browser/customizationsDebugLog.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; -import './contrib/changesView/browser/changesView.contribution.js'; +import './contrib/changes/browser/changesView.contribution.js'; import './contrib/codeReview/browser/codeReview.contributions.js'; import './contrib/files/browser/files.contribution.js'; -import './contrib/gitSync/browser/gitSync.contribution.js'; +import './contrib/git/browser/git.contribution.js'; import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; From 8dd5e19942ade9963cf9a7b6d79478fbba5550c6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 7 Mar 2026 17:51:58 +0100 Subject: [PATCH 332/448] sessions - fix chat input padding when space is narrow (#299927) * style - add padding to chat item containers * style - add box-sizing to chat input container * Address feedback on chat input padding for narrow spaces (#299930) * Initial plan * style - add box-sizing: border-box to item containers to align with input Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --- src/vs/sessions/browser/media/style.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 4454c30fb8c..c4c659e4af9 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -54,11 +54,17 @@ .agent-sessions-workbench .interactive-session .interactive-item-container { max-width: 950px; margin: 0 auto; + padding-left: 12px; + padding-right: 12px; + box-sizing: border-box; } .agent-sessions-workbench .interactive-session > .chat-suggest-next-widget { max-width: 950px; margin: 0 auto; + padding-left: 12px; + padding-right: 12px; + box-sizing: border-box; } /* ---- Chat Input ---- */ @@ -73,5 +79,6 @@ margin: 0 auto !important; display: inherit !important; /* Align with changes view */ - padding: 4px 0 6px 0 !important; + padding: 4px 12px 6px 12px !important; + box-sizing: border-box; } From f698061d0d57fe2390bfb1c610d38f33820df1c3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 7 Mar 2026 17:52:37 +0100 Subject: [PATCH 333/448] Allow to resize modal editor (fix #293915) (#299969) * Allow to resize modal editor (fix #293915) * Update src/vs/platform/editor/common/editor.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/browser/parts/editor/modalEditorPart.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/platform/editor/common/editor.ts | 10 + .../browser/parts/editor/editorParts.ts | 22 +- .../parts/editor/media/modalEditorPart.css | 13 +- .../browser/parts/editor/modalEditorPart.ts | 286 ++++++++++++++++-- .../editor/common/editorGroupsService.ts | 10 + .../parts/editor/modalEditorResize.test.ts | 230 ++++++++++++++ 6 files changed, 540 insertions(+), 31 deletions(-) create mode 100644 src/vs/workbench/test/browser/parts/editor/modalEditorResize.test.ts diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index f1477768f39..d0751cb4bc9 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -334,6 +334,16 @@ export interface IModalEditorPartOptions { */ readonly maximized?: boolean; + /** + * Size of the modal editor part unless it is maximized. + */ + readonly size?: { readonly width: number; readonly height: number }; + + /** + * Position of the modal editor part unless it is maximized. + */ + readonly position?: { readonly left: number; readonly top: number }; + /** * The navigation context for navigating between items * within this modal editor. Pass `undefined` to clear. diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index eee5f12fe08..786d0bd239f 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -22,7 +22,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { getActiveElement, isAncestor, isHTMLElement } from '../../../../base/browser/dom.js'; +import { getActiveElement, IDimension, isAncestor, isHTMLElement } from '../../../../base/browser/dom.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { DeepPartial } from '../../../../base/common/types.js'; @@ -159,6 +159,8 @@ export class EditorParts extends MultiWindowParts { @@ -169,21 +171,27 @@ export class EditorParts extends MultiWindowParts { + this.modalEditorMaximized = part.maximized; + this.modalEditorSize = part.size; + this.modalEditorPosition = part.position; + this.modalPartInstantiationService = undefined; this.modalEditorPart = undefined; })); - // Track maximized state in memory - disposables.add(part.onDidChangeMaximized(maximized => { - this.modalEditorMaximized = maximized; - })); - // Events this._onDidAddGroup.fire(part.activeGroup); diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index 3b95d6bf310..b5b6e0efd74 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -12,17 +12,20 @@ left: 0; /* z-index for modal editors: above titlebar (2500) but below quick input (2550) and dialogs (2575) */ z-index: 2540; - display: flex; - justify-content: center; - align-items: center; /* Never allow content to escape above the title bar */ overflow: hidden; background: rgba(0, 0, 0, 0.3); + .modal-editor-resizable { + position: absolute; + } + .modal-editor-shadow { box-shadow: var(--vscode-shadow-xl); border-radius: 8px; overflow: hidden; + width: 100%; + height: 100%; } } @@ -30,8 +33,8 @@ .monaco-modal-editor-block .modal-editor-part { display: flex; flex-direction: column; - min-width: 400px; - min-height: 300px; + width: 100%; + height: 100%; background-color: var(--vscode-editor-background); border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); border-radius: 8px; diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 4f164d14403..9d282dcd1a0 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import './media/modalEditorPart.css'; -import { $, addDisposableListener, append, EventHelper, EventType, hide, isHTMLElement, setVisibility, show } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, Dimension, EventHelper, EventType, hide, IDimension, isHTMLElement, setVisibility, show } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { prepareActions } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -35,6 +36,15 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, MOVE_MODAL_EDITOR_TO_WINDOW_COMMAND_ID, NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js'; import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; +const MODAL_MIN_WIDTH = 400; +const MODAL_MIN_HEIGHT = 300; +const MODAL_MAX_DEFAULT_WIDTH = 1400; +const MODAL_MAX_DEFAULT_HEIGHT = 900; +const MODAL_BORDER_SIZE = 2; // 1px border on each side +const MODAL_HEADER_HEIGHT = 33; // 32px header + 1px border bottom +const MODAL_SNAP_THRESHOLD = 20; +const MODAL_MAXIMIZED_PADDING = 16; + const defaultModalEditorAllowableCommands = new Set([ // Application @@ -153,7 +163,14 @@ export class ModalEditorPart { } })); - const shadowElement = modalElement.appendChild($('.modal-editor-shadow')); + // Resizable wrapper + const resizableElement = new ResizableHTMLElement(); + disposables.add(toDisposable(() => resizableElement.dispose())); + resizableElement.domNode.classList.add('modal-editor-resizable'); + resizableElement.minSize = new Dimension(MODAL_MIN_WIDTH, MODAL_MIN_HEIGHT); + modalElement.appendChild(resizableElement.domNode); + + const shadowElement = resizableElement.domNode.appendChild($('.modal-editor-shadow')); // Editor part container const titleId = 'modal-editor-title'; @@ -289,50 +306,228 @@ export class ModalEditorPart { disposables.add(addDisposableListener(headerElement, EventType.DBLCLICK, e => { EventHelper.stop(e); - editorPart.toggleMaximized(); + editorPart.handleHeaderDoubleClick(); + })); + + // Handle drag on header to move the modal + const dragDisposables = disposables.add(new DisposableStore()); + let didDrag = false; + disposables.add(addDisposableListener(headerElement, EventType.MOUSE_DOWN, e => { + if (editorPart.maximized) { + return; // no drag when maximized + } + + if (e.button !== 0) { + return; // only left button + } + + // Ignore if target is a button or action + const target = e.target as HTMLElement; + if (target.closest('.monaco-button') || target.closest('.action-item')) { + return; + } + + // Prevent text selection during drag + e.preventDefault(); + + dragDisposables.clear(); + + const startX = e.clientX; + const startY = e.clientY; + const startLeft = parseFloat(resizableElement.domNode.style.left) || 0; + const startTop = parseFloat(resizableElement.domNode.style.top) || 0; + didDrag = false; + + const onMouseMove = (moveEvent: MouseEvent) => { + didDrag = true; + EventHelper.stop(moveEvent, true); + + const containerDimension = this.layoutService.mainContainerDimension; + const titleBarOffset = this.layoutService.mainContainerOffset.top; + const dialogWidth = resizableElement.size.width; + const dialogHeight = resizableElement.size.height; + const availableHeight = Math.max(containerDimension.height - titleBarOffset, 0); + + // Clamp to window bounds + const minLeft = 0; + const minTop = titleBarOffset; + const maxLeft = Math.max(minLeft, containerDimension.width - dialogWidth); + const maxTop = Math.max(minTop, titleBarOffset + availableHeight - dialogHeight); + + let newLeft = Math.max(minLeft, Math.min(maxLeft, startLeft + (moveEvent.clientX - startX))); + let newTop = Math.max(minTop, Math.min(maxTop, startTop + (moveEvent.clientY - startY))); + + // Snap to center position when close + const centerLeft = (containerDimension.width - dialogWidth) / 2; + const centerTop = titleBarOffset + (availableHeight - dialogHeight) / 2; + + if (Math.abs(newLeft - centerLeft) < MODAL_SNAP_THRESHOLD && Math.abs(newTop - centerTop) < MODAL_SNAP_THRESHOLD) { + newLeft = centerLeft; + newTop = centerTop; + } + + resizableElement.domNode.style.left = `${newLeft}px`; + resizableElement.domNode.style.top = `${newTop}px`; + }; + + const onMouseUp = (upEvent: MouseEvent) => { + EventHelper.stop(upEvent, true); + dragDisposables.clear(); + + if (didDrag) { + const currentLeft = parseFloat(resizableElement.domNode.style.left) || 0; + const currentTop = parseFloat(resizableElement.domNode.style.top) || 0; + + // Check if snapped to center — if so, clear custom position + const containerDimension = this.layoutService.mainContainerDimension; + const titleBarOffset = this.layoutService.mainContainerOffset.top; + const availableHeight = Math.max(containerDimension.height - titleBarOffset, 0); + const centerLeft = (containerDimension.width - resizableElement.size.width) / 2; + const centerTop = titleBarOffset + (availableHeight - resizableElement.size.height) / 2; + + if (Math.abs(currentLeft - centerLeft) < 1 && Math.abs(currentTop - centerTop) < 1) { + editorPart.position = undefined; + } else { + editorPart.position = { left: currentLeft, top: currentTop }; + } + } + }; + + dragDisposables.add(addDisposableListener(mainWindow, EventType.MOUSE_MOVE, onMouseMove, true)); + dragDisposables.add(addDisposableListener(mainWindow, EventType.MOUSE_UP, onMouseUp, true)); })); // Focus active editor when clicking into the title area with no other click target disposables.add(addDisposableListener(headerElement, EventType.CLICK, e => { + const wasDrag = didDrag; + didDrag = false; + if (wasDrag) { + return; // skip focus after drag + } + EventHelper.stop(e); editorPart.activeGroup.focus(); })); - // Layout the modal editor part - const layoutModal = () => { + // Handle resize from sashes + let isResizing = false; + let resizeStartLeft = 0; + let resizeStartTop = 0; + let resizeStartSize = Dimension.None; + + disposables.add(resizableElement.onDidWillResize(() => { + isResizing = true; + resizeStartLeft = parseFloat(resizableElement.domNode.style.left) || 0; + resizeStartTop = parseFloat(resizableElement.domNode.style.top) || 0; + resizeStartSize = new Dimension(resizableElement.size.width, resizableElement.size.height); + })); + + disposables.add(resizableElement.onDidResize(e => { + const deltaWidth = e.dimension.width - resizeStartSize.width; + const deltaHeight = e.dimension.height - resizeStartSize.height; + + // Adjust position to keep the opposite edge fixed + if (e.west) { + resizableElement.domNode.style.left = `${resizeStartLeft - deltaWidth}px`; + } + if (e.north) { + resizableElement.domNode.style.top = `${resizeStartTop - deltaHeight}px`; + } + + // Update editor part layout during resize + editorPart.layout(e.dimension.width - MODAL_BORDER_SIZE, e.dimension.height - MODAL_BORDER_SIZE - MODAL_HEADER_HEIGHT, 0, 0); + + if (e.done) { + isResizing = false; + + // Check if size matches the default (from sash double-click reset) + const defaultSize = getDefaultSize(); + if (e.dimension.width === defaultSize.width && e.dimension.height === defaultSize.height) { + editorPart.size = undefined; + editorPart.position = undefined; + layoutModal(); + } else { + editorPart.size = new Dimension(e.dimension.width, e.dimension.height); + editorPart.position = { + left: parseFloat(resizableElement.domNode.style.left) || 0, + top: parseFloat(resizableElement.domNode.style.top) || 0, + }; + } + } + })); + + // Compute default (non-custom, non-maximized) modal size + const getDefaultSize = (): Dimension => { const containerDimension = this.layoutService.mainContainerDimension; const titleBarOffset = this.layoutService.mainContainerOffset.top; const availableHeight = Math.max(containerDimension.height - titleBarOffset, 0); + const targetWidth = containerDimension.width * 0.8; + const targetHeight = availableHeight * 0.8; + const width = Math.min(targetWidth, MODAL_MAX_DEFAULT_WIDTH, containerDimension.width); + const height = Math.min(targetHeight, MODAL_MAX_DEFAULT_HEIGHT, availableHeight); + + return new Dimension(width, height); + }; + + // Layout the modal editor part + const layoutModal = () => { + if (isResizing) { + return; // skip layout during interactive resize + } + + const containerDimension = this.layoutService.mainContainerDimension; + const titleBarOffset = this.layoutService.mainContainerOffset.top; + const availableHeight = Math.max(containerDimension.height - titleBarOffset, 0); + + const defaultSize = getDefaultSize(); let width: number; let height: number; if (editorPart.maximized) { - const horizontalPadding = 16; - const verticalPadding = Math.max(titleBarOffset /* keep away from title bar to prevent clipping issues with WCO */, 16); - width = Math.max(containerDimension.width - horizontalPadding, 0); + const verticalPadding = Math.max(titleBarOffset /* keep away from title bar to prevent clipping issues with WCO */, MODAL_MAXIMIZED_PADDING); + width = Math.max(containerDimension.width - MODAL_MAXIMIZED_PADDING, 0); height = Math.max(availableHeight - verticalPadding, 0); + } else if (editorPart.size) { + width = Math.min(editorPart.size.width, containerDimension.width); + height = Math.min(editorPart.size.height, availableHeight); } else { - const maxWidth = 1400; - const maxHeight = 900; - const targetWidth = containerDimension.width * 0.8; - const targetHeight = availableHeight * 0.8; - width = Math.min(targetWidth, maxWidth, containerDimension.width); - height = Math.min(targetHeight, maxHeight, availableHeight); + width = defaultSize.width; + height = defaultSize.height; } height = Math.min(height, availableHeight); // Ensure the modal never exceeds available height (below the title bar) - editorPartContainer.style.width = `${width}px`; - editorPartContainer.style.height = `${height}px`; + // Update resizable element size and constraints + resizableElement.maxSize = new Dimension(containerDimension.width, availableHeight); + resizableElement.preferredSize = defaultSize; + resizableElement.layout(height, width); - const borderSize = 2; // Account for 1px border on all sides and modal header height - const headerHeight = 32 + 1 /* border bottom */; - editorPart.layout(width - borderSize, height - borderSize - headerHeight, 0, 0); + // Enable/disable sashes based on maximized state + const canResize = !editorPart.maximized; + resizableElement.enableSashes(canResize, canResize, canResize, canResize); + + // Position: use custom position if available (clamped to bounds), otherwise center + if (!editorPart.maximized && editorPart.position) { + const clampedLeft = Math.max(0, Math.min(editorPart.position.left, containerDimension.width - width)); + const clampedTop = Math.max(titleBarOffset, Math.min(editorPart.position.top, titleBarOffset + availableHeight - height)); + resizableElement.domNode.style.left = `${clampedLeft}px`; + resizableElement.domNode.style.top = `${clampedTop}px`; + } else { + const left = (containerDimension.width - width) / 2; + const top = editorPart.maximized + ? (containerDimension.height - height) / 2 // center in full window to stay close to title bar + : titleBarOffset + (availableHeight - height) / 2; + resizableElement.domNode.style.left = `${left}px`; + resizableElement.domNode.style.top = `${top}px`; + } + + editorPart.layout(width - MODAL_BORDER_SIZE, height - MODAL_BORDER_SIZE - MODAL_HEADER_HEIGHT, 0, 0); }; disposables.add(Event.runAndSubscribe(this.layoutService.onDidLayoutMainContainer, layoutModal)); disposables.add(editorPart.onDidChangeMaximized(() => layoutModal())); + disposables.add(editorPart.onDidRequestLayout(() => layoutModal())); // Dim window controls to match the modal overlay this.hostService.setWindowDimmed(mainWindow, true); @@ -349,6 +544,11 @@ export class ModalEditorPart { } } +interface IPosition { + left: number; + top: number; +} + class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { private static COUNTER = 1; @@ -359,12 +559,26 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { private readonly _onDidChangeMaximized = this._register(new Emitter()); readonly onDidChangeMaximized = this._onDidChangeMaximized.event; + private readonly _onDidRequestLayout = this._register(new Emitter()); + readonly onDidRequestLayout = this._onDidRequestLayout.event; + private readonly _onDidChangeNavigation = this._register(new Emitter()); readonly onDidChangeNavigation = this._onDidChangeNavigation.event; private _maximized: boolean; get maximized(): boolean { return this._maximized; } + private _size: IDimension | undefined; + get size(): IDimension | undefined { return this._size; } + set size(value: IDimension | undefined) { this._size = value; } + + private _position: IPosition | undefined; + get position(): IPosition | undefined { return this._position; } + set position(value: IPosition | undefined) { this._position = value; } + + private savedSize: IDimension | undefined; + private savedPosition: IPosition | undefined; + private _navigation: IModalEditorNavigation | undefined; get navigation(): IModalEditorNavigation | undefined { return this._navigation; } @@ -389,8 +603,17 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { super(editorPartsView, `workbench.parts.modalEditor.${id}`, localize('modalEditorPart', "Modal Editor Area"), windowId, instantiationService, themeService, configurationService, storageService, layoutService, hostService, contextKeyService); this._maximized = options?.maximized ?? false; + this._size = options?.size; + this._position = options?.position; this._navigation = options?.navigation; + // When restoring a maximized state with custom layout, + // initialize saved state so un-maximize can restore it + if (this._maximized) { + this.savedSize = this._size; + this.savedPosition = this._position; + } + this.enforceModalPartOptions(); } @@ -426,9 +649,34 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { toggleMaximized(): void { this._maximized = !this._maximized; + if (this._maximized) { + this.savedSize = this._size; + this.savedPosition = this._position; + } else { + this._size = this.savedSize; + this._position = this.savedPosition; + this.savedSize = undefined; + this.savedPosition = undefined; + } + this._onDidChangeMaximized.fire(this._maximized); } + handleHeaderDoubleClick(): void { + if (this._maximized) { + // Clear saved state so that toggleMaximized restores to default + this.savedSize = undefined; + this.savedPosition = undefined; + this.toggleMaximized(); + } else if (this._size) { + this._size = undefined; + this._position = undefined; + this._onDidRequestLayout.fire(); + } else { + this.toggleMaximized(); // maximize + } + } + protected override handleContextKeys(): void { const isModalEditorPartContext = EditorPartModalContext.bindTo(this.scopedContextKeyService); isModalEditorPartContext.set(true); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 9fcff97208a..0e25a7e3803 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -543,6 +543,16 @@ export interface IModalEditorPart extends IEditorPart { */ toggleMaximized(): void; + /** + * Size set by the user via resizing, if any. + */ + readonly size: IDimension | undefined; + + /** + * Position set by the user via dragging, if any. + */ + readonly position: { left: number; top: number } | undefined; + /** * The current navigation context, if any. */ diff --git a/src/vs/workbench/test/browser/parts/editor/modalEditorResize.test.ts b/src/vs/workbench/test/browser/parts/editor/modalEditorResize.test.ts new file mode 100644 index 00000000000..8b17d85b0e0 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/modalEditorResize.test.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; + +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; + +interface ISize { + readonly width: number; + readonly height: number; +} + +/** + * Simple test harness that mimics the ModalEditorPartImpl resize behavior + * without requiring the full editor part infrastructure. + */ +class TestModalEditorResizeHost extends Disposable { + + private readonly _onDidChangeMaximized = this._register(new Emitter()); + readonly onDidChangeMaximized = this._onDidChangeMaximized.event; + + private readonly _onDidRequestLayout = this._register(new Emitter()); + readonly onDidRequestLayout = this._onDidRequestLayout.event; + + private _maximized = false; + get maximized(): boolean { return this._maximized; } + + private _size: ISize | undefined; + get size(): ISize | undefined { return this._size; } + set size(value: ISize | undefined) { this._size = value; } + + private _position: { left: number; top: number } | undefined; + get position(): { left: number; top: number } | undefined { return this._position; } + set position(value: { left: number; top: number } | undefined) { this._position = value; } + + private savedSize: ISize | undefined; + private savedPosition: { left: number; top: number } | undefined; + + toggleMaximized(): void { + this._maximized = !this._maximized; + + if (this._maximized) { + this.savedSize = this._size; + this.savedPosition = this._position; + } else { + this._size = this.savedSize; + this._position = this.savedPosition; + this.savedSize = undefined; + this.savedPosition = undefined; + } + + this._onDidChangeMaximized.fire(this._maximized); + } + + handleHeaderDoubleClick(): void { + if (this._maximized) { + this.savedSize = undefined; + this.savedPosition = undefined; + this.toggleMaximized(); // un-maximize to default + } else if (this._size) { + this._size = undefined; + this._position = undefined; + this._onDidRequestLayout.fire(); + } else { + this.toggleMaximized(); // maximize + } + } + +} + +suite('Modal Editor Resize', () => { + + const disposables = new DisposableStore(); + + teardown(() => disposables.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('double-click from default size maximizes', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + const events: boolean[] = []; + disposables.add(host.onDidChangeMaximized(v => events.push(v))); + + host.handleHeaderDoubleClick(); + + assert.deepStrictEqual( + { maximized: host.maximized, size: host.size, events }, + { maximized: true, size: undefined, events: [true] } + ); + }); + + test('double-click from maximized restores default', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + host.handleHeaderDoubleClick(); // maximize + + const events: boolean[] = []; + disposables.add(host.onDidChangeMaximized(v => events.push(v))); + + host.handleHeaderDoubleClick(); // restore + + assert.deepStrictEqual( + { maximized: host.maximized, size: host.size, events }, + { maximized: false, size: undefined, events: [false] } + ); + }); + + test('double-click from custom size restores default without firing maximized event', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + host.size = { width: 800, height: 600 }; + + const maximizedEvents: boolean[] = []; + let layoutRequested = false; + disposables.add(host.onDidChangeMaximized(v => maximizedEvents.push(v))); + disposables.add(host.onDidRequestLayout(() => { layoutRequested = true; })); + + host.handleHeaderDoubleClick(); + + assert.deepStrictEqual( + { maximized: host.maximized, size: host.size, maximizedEvents, layoutRequested }, + { maximized: false, size: undefined, maximizedEvents: [], layoutRequested: true } + ); + }); + + test('double-click cycle: custom → default → maximized → default', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + const events: boolean[] = []; + disposables.add(host.onDidChangeMaximized(v => events.push(v))); + + // Start with custom size + host.size = { width: 800, height: 600 }; + + // First double-click: custom → default (fires layout, not maximized) + host.handleHeaderDoubleClick(); + assert.strictEqual(host.maximized, false); + assert.strictEqual(host.size, undefined); + + // Second double-click: default → maximized + host.handleHeaderDoubleClick(); + assert.strictEqual(host.maximized, true); + assert.strictEqual(host.size, undefined); + + // Third double-click: maximized → default + host.handleHeaderDoubleClick(); + assert.strictEqual(host.maximized, false); + assert.strictEqual(host.size, undefined); + + assert.deepStrictEqual(events, [true, false]); + }); + + test('toggleMaximized preserves custom state through maximize/un-maximize cycle', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + host.size = { width: 800, height: 600 }; + host.position = { left: 100, top: 50 }; + + host.toggleMaximized(); + assert.strictEqual(host.maximized, true); + + host.toggleMaximized(); + assert.deepStrictEqual( + { maximized: host.maximized, size: host.size, position: host.position }, + { maximized: false, size: { width: 800, height: 600 }, position: { left: 100, top: 50 } } + ); + }); + + test('double-click from maximized clears saved custom state', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + // Set custom size then maximize via toggleMaximized (saves state) + host.size = { width: 800, height: 600 }; + host.toggleMaximized(); + assert.strictEqual(host.maximized, true); + + // Double-click to un-maximize: should go to default, not restore custom + host.handleHeaderDoubleClick(); + assert.deepStrictEqual( + { maximized: host.maximized, size: host.size }, + { maximized: false, size: undefined } + ); + }); + + test('double-click clears custom position along with size', () => { + const host = disposables.add(new TestModalEditorResizeHost()); + + host.size = { width: 800, height: 600 }; + host.position = { left: 100, top: 50 }; + + host.handleHeaderDoubleClick(); + + assert.deepStrictEqual( + { size: host.size, position: host.position, maximized: host.maximized }, + { size: undefined, position: undefined, maximized: false } + ); + }); + + test('session persistence: state can be saved and restored across instances', () => { + const host1 = disposables.add(new TestModalEditorResizeHost()); + + host1.size = { width: 900, height: 700 }; + host1.position = { left: 200, top: 100 }; + + // Simulate saving state on close + const savedState = { + size: host1.size, + position: host1.position, + maximized: host1.maximized, + }; + + // Simulate restoring state on new modal + const host2 = disposables.add(new TestModalEditorResizeHost()); + host2.size = savedState.size; + host2.position = savedState.position; + if (savedState.maximized) { + host2.toggleMaximized(); + } + + assert.deepStrictEqual( + { size: host2.size, position: host2.position, maximized: host2.maximized }, + { size: { width: 900, height: 700 }, position: { left: 200, top: 100 }, maximized: false } + ); + }); +}); From 5734afd12be4ca96be20d4efd1fa5fb46b6b6d78 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 7 Mar 2026 10:55:25 -0800 Subject: [PATCH 334/448] Fix build (#299987) sessionId -> chatSessionId --- .../chat/common/chatService/chatService.ts | 8 +++---- .../common/chatService/chatServiceImpl.ts | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index bc6f5672bba..661a408290a 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1490,7 +1490,7 @@ export type ChatStopCancellationNoopEvent = { pendingRequests: number; sessionScheme?: string; lastRequestId?: string; - sessionId?: string; + chatSessionId?: string; }; export type ChatStopCancellationNoopClassification = { @@ -1500,7 +1500,7 @@ export type ChatStopCancellationNoopClassification = { pendingRequests: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of queued pending requests at no-op time when known.'; isMeasurement: true }; sessionScheme?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The URI scheme of the session resource (e.g. vscodeLocalChatSession vs remote).' }; lastRequestId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the last request in the session, for correlating with tool invocations.' }; - sessionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat session ID.' }; + chatSessionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat session ID.' }; owner: 'roblourens'; comment: 'Tracks possible no-op stop cancellation paths.'; }; @@ -1511,14 +1511,14 @@ export type ChatPendingRequestChangeEvent = { action: 'add' | 'remove' | 'notCancelable'; source: string; requestId?: string; - sessionId?: string; + chatSessionId?: string; }; export type ChatPendingRequestChangeClassification = { action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a pending request was added or removed.' }; source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The method that triggered the pending request change.' }; requestId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The request ID associated with the pending request change.' }; - sessionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat session ID.' }; + chatSessionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat session ID.' }; owner: 'roblourens'; comment: 'Tracks pending request lifecycle changes in the chat service.'; }; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index ed16f7aa579..854edefa586 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -681,7 +681,7 @@ export class ChatService extends Disposable implements IChatService { if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) { const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); this._pendingRequests.set(model.sessionResource, initialCancellationRequest); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', sessionId: chatSessionResourceToId(model.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', chatSessionId: chatSessionResourceToId(model.sessionResource) }); const cancellationListener = disposables.add(new MutableDisposable()); const createCancellationListener = (token: CancellationToken) => { @@ -691,7 +691,7 @@ export class ChatService extends Disposable implements IChatService { // User cancelled the interruption const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); this._pendingRequests.set(model.sessionResource, newCancellationRequest); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', sessionId: chatSessionResourceToId(model.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', chatSessionId: chatSessionResourceToId(model.sessionResource) }); cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token); } }); @@ -721,7 +721,7 @@ export class ChatService extends Disposable implements IChatService { } })); } else { - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'notCancelable', source: 'remoteSession', sessionId: chatSessionResourceToId(model.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'notCancelable', source: 'remoteSession', chatSessionId: chatSessionResourceToId(model.sessionResource) }); if (lastRequest && model.editingSession) { // wait for timeline to load so that a 'changes' part is added when the response completes await chatEditingSessionIsReady(model.editingSession); @@ -1109,7 +1109,7 @@ export class ChatService extends Disposable implements IChatService { })); pendingRequest.requestId ??= requestProps.requestId; if (pendingRequest.requestId) { - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequestId', requestId: pendingRequest.requestId, sessionId: chatSessionResourceToId(sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequestId', requestId: pendingRequest.requestId, chatSessionId: chatSessionResourceToId(sessionResource) }); } } completeResponseCreated(); @@ -1232,11 +1232,11 @@ export class ChatService extends Disposable implements IChatService { // Note- requestId is not known at this point, assigned later const cancellableRequest = this.instantiationService.createInstance(CancellableRequest, source, undefined); this._pendingRequests.set(model.sessionResource, cancellableRequest); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequest', sessionId: chatSessionResourceToId(model.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequest', chatSessionId: chatSessionResourceToId(model.sessionResource) }); rawResponsePromise.finally(() => { if (this._pendingRequests.get(model.sessionResource) === cancellableRequest) { this._pendingRequests.deleteAndDispose(model.sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'sendRequestComplete', requestId: cancellableRequest.requestId, sessionId: chatSessionResourceToId(model.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'sendRequestComplete', requestId: cancellableRequest.requestId, chatSessionId: chatSessionResourceToId(model.sessionResource) }); } // Process the next pending request from the queue if any if (shouldProcessPending) { @@ -1437,7 +1437,7 @@ export class ChatService extends Disposable implements IChatService { if (pendingRequest?.requestId === requestId) { pendingRequest.cancel(); this._pendingRequests.deleteAndDispose(sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'removeRequest', requestId, sessionId: chatSessionResourceToId(model.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'removeRequest', requestId, chatSessionId: chatSessionResourceToId(model.sessionResource) }); } model.removeRequest(requestId); @@ -1460,8 +1460,8 @@ export class ChatService extends Disposable implements IChatService { if (cts) { cts.requestId = request.id; this._pendingRequests.set(target.sessionResource, cts); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'adoptRequest', requestId: request.id, sessionId: chatSessionResourceToId(oldOwner.sessionResource) }); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'adoptRequest', requestId: request.id, sessionId: chatSessionResourceToId(target.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: 'adoptRequest', requestId: request.id, chatSessionId: chatSessionResourceToId(oldOwner.sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'adoptRequest', requestId: request.id, chatSessionId: chatSessionResourceToId(target.sessionResource) }); } } } @@ -1508,7 +1508,7 @@ export class ChatService extends Disposable implements IChatService { pendingRequests: pendingRequestsCount, sessionScheme: sessionResource.scheme, lastRequestId: lastRequest?.id, - sessionId: chatSessionResourceToId(sessionResource), + chatSessionId: chatSessionResourceToId(sessionResource), }); this.info('cancelCurrentRequestForSession', `No pending request was found for session ${sessionResource}. requestInProgress=${requestInProgress ?? 'unknown'}, pendingRequests=${pendingRequestsCount}`); return; @@ -1516,7 +1516,7 @@ export class ChatService extends Disposable implements IChatService { pendingRequest.cancel(); this._pendingRequests.deleteAndDispose(sessionResource); - this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: source ?? 'cancelRequest', requestId: pendingRequest.requestId, sessionId: chatSessionResourceToId(sessionResource) }); + this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: source ?? 'cancelRequest', requestId: pendingRequest.requestId, chatSessionId: chatSessionResourceToId(sessionResource) }); } setYieldRequested(sessionResource: URI): void { From 010781f467a9be0f5ac549b6ea23d149210fa2d2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 7 Mar 2026 12:44:53 -0800 Subject: [PATCH 335/448] Wait for chat cancellation to complete before proceeding (#300001) * Wait for chat cancellation to complete before proceeding This is an inherent race condition in the architecture, working on changing it, in the meantime, work around this... * Fix test --- .../browser/actions/chatExecuteActions.ts | 6 +- .../chat/browser/actions/chatQueueActions.ts | 4 +- .../contrib/chat/browser/widget/chatWidget.ts | 4 +- .../chat/common/chatService/chatService.ts | 2 +- .../common/chatService/chatServiceImpl.ts | 16 ++-- .../localAgentSessionsController.test.ts | 2 +- .../common/chatService/chatService.test.ts | 76 ++++++++++++++++++- .../common/chatService/mockChatService.ts | 2 +- .../browser/inlineChatController.ts | 2 +- .../browser/inlineChatSessionServiceImpl.ts | 2 +- .../chat/browser/terminalChatWidget.ts | 2 +- 11 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 907a1145b99..14b0cb77d71 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -896,7 +896,7 @@ class SendToNewChatAction extends Action2 { // Cancel any in-progress request before clearing if (widget.viewModel) { - chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource, 'newSessionAction'); + await chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource, 'newSessionAction'); } if (widget.viewModel?.model) { @@ -956,7 +956,7 @@ export class CancelAction extends Action2 { }); } - run(accessor: ServicesAccessor, ...args: unknown[]) { + async run(accessor: ServicesAccessor, ...args: unknown[]) { const context = args[0] as IChatExecuteActionContext | undefined; const widgetService = accessor.get(IChatWidgetService); const logService = accessor.get(ILogService); @@ -975,7 +975,7 @@ export class CancelAction extends Action2 { const chatService = accessor.get(IChatService); if (widget.viewModel) { - chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource, 'cancelAction'); + await chatService.cancelCurrentRequestForSession(widget.viewModel.sessionResource, 'cancelAction'); } else { telemetryService.publicLog2(ChatStopCancellationNoopEventName, { source: 'cancelAction', diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index 3eac2679afa..ed1b2f3689a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -219,7 +219,7 @@ export class ChatSendPendingImmediatelyAction extends Action2 { }); } - override run(accessor: ServicesAccessor, ...args: unknown[]): void { + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { const chatService = accessor.get(IChatService); const widgetService = accessor.get(IChatWidgetService); const [context] = args; @@ -250,7 +250,7 @@ export class ChatSendPendingImmediatelyAction extends Action2 { ]; chatService.setPendingRequests(context.sessionResource, reordered); - chatService.cancelCurrentRequestForSession(context.sessionResource, 'queueRunNext'); + await chatService.cancelCurrentRequestForSession(context.sessionResource, 'queueRunNext'); chatService.processPendingRequests(context.sessionResource); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 81716d9c338..266efc7b245 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -2246,7 +2246,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.chatService.removePendingRequest(this.viewModel.sessionResource, editingRequestId); options.queue ??= editingPendingRequest; } else { - this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-editing'); + await this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-editing'); options.queue = undefined; } @@ -2266,7 +2266,7 @@ export class ChatWidget extends Disposable implements IChatWidget { options.queue ??= ChatRequestQueueKind.Queued; } if (model.requestNeedsInput.get() && !model.getPendingRequests().length) { - this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-needsInput'); + await this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource, 'acceptInput-needsInput'); options.queue ??= ChatRequestQueueKind.Queued; } if (requestInProgress) { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 661a408290a..51cea27eb77 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1410,7 +1410,7 @@ export interface IChatService { resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise; removeRequest(sessionResource: URI, requestId: string): Promise; - cancelCurrentRequestForSession(sessionResource: URI, source?: string): void; + cancelCurrentRequestForSession(sessionResource: URI, source?: string): Promise; /** * Sets yieldRequested on the active request for the given session. */ diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 854edefa586..e91a75cd468 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DeferredPromise } from '../../../../../base/common/async.js'; +import { DeferredPromise, raceTimeout } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; @@ -67,6 +67,7 @@ class CancellableRequest implements IDisposable { constructor( public readonly cancellationTokenSource: CancellationTokenSource, public requestId: string | undefined, + public readonly responseCompletePromise: Promise | undefined, @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService ) { } @@ -679,7 +680,7 @@ export class ChatService extends Disposable implements IChatService { } if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) { - const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); + const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined, undefined); this._pendingRequests.set(model.sessionResource, initialCancellationRequest); this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', chatSessionId: chatSessionResourceToId(model.sessionResource) }); const cancellationListener = disposables.add(new MutableDisposable()); @@ -689,7 +690,7 @@ export class ChatService extends Disposable implements IChatService { providedSession.interruptActiveResponseCallback?.().then(userConfirmedInterruption => { if (!userConfirmedInterruption) { // User cancelled the interruption - const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); + const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined, undefined); this._pendingRequests.set(model.sessionResource, newCancellationRequest); this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'remoteSession', chatSessionId: chatSessionResourceToId(model.sessionResource) }); cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token); @@ -1230,7 +1231,7 @@ export class ChatService extends Disposable implements IChatService { let shouldProcessPending = false; const rawResponsePromise = sendRequestInternal(); // Note- requestId is not known at this point, assigned later - const cancellableRequest = this.instantiationService.createInstance(CancellableRequest, source, undefined); + const cancellableRequest = this.instantiationService.createInstance(CancellableRequest, source, undefined, rawResponsePromise); this._pendingRequests.set(model.sessionResource, cancellableRequest); this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'add', source: 'sendRequest', chatSessionId: chatSessionResourceToId(model.sessionResource) }); rawResponsePromise.finally(() => { @@ -1493,7 +1494,7 @@ export class ChatService extends Disposable implements IChatService { request.response?.complete(); } - cancelCurrentRequestForSession(sessionResource: URI, source?: string): void { + async cancelCurrentRequestForSession(sessionResource: URI, source?: string): Promise { this.trace('cancelCurrentRequestForSession', `session: ${sessionResource}`); const pendingRequest = this._pendingRequests.get(sessionResource); if (!pendingRequest) { @@ -1514,9 +1515,14 @@ export class ChatService extends Disposable implements IChatService { return; } + const responseCompletePromise = pendingRequest.responseCompletePromise; pendingRequest.cancel(); this._pendingRequests.deleteAndDispose(sessionResource); this.telemetryService.publicLog2(ChatPendingRequestChangeEventName, { action: 'remove', source: source ?? 'cancelRequest', requestId: pendingRequest.requestId, chatSessionId: chatSessionResourceToId(sessionResource) }); + + if (responseCompletePromise) { + await raceTimeout(responseCompletePromise, 1000); + } } setYieldRequested(sessionResource: URI): void { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index c1c62a1246e..ae6d57e0a80 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -149,7 +149,7 @@ class MockChatService implements IChatService { throw new Error('Method not implemented.'); } - cancelCurrentRequestForSession(_sessionResource: URI, _source?: string): void { } + async cancelCurrentRequestForSession(_sessionResource: URI, _source?: string): Promise { } setYieldRequested(_sessionResource: URI): void { } diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index f0bd3984aae..3ca952c05d5 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -12,6 +12,7 @@ import { constObservable, observableValue } from '../../../../../../base/common/ import { URI } from '../../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; @@ -40,7 +41,7 @@ import { TestMcpService } from '../../../../mcp/test/common/testMcpService.js'; import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../../common/participants/chatAgents.js'; import { IChatEditingService, IChatEditingSession } from '../../../common/editing/chatEditingService.js'; import { ChatModel, IChatModel, ISerializableChatData } from '../../../common/model/chatModel.js'; -import { ChatRequestQueueKind, ChatSendResult, IChatFollowup, IChatModelReference, IChatService } from '../../../common/chatService/chatService.js'; +import { ChatRequestQueueKind, ChatSendResult, IChatFollowup, IChatModelReference, IChatService, ResponseModelState } from '../../../common/chatService/chatService.js'; import { ChatService } from '../../../common/chatService/chatServiceImpl.js'; import { ChatSlashCommandService, IChatSlashCommandService } from '../../../common/participants/chatSlashCommands.js'; import { IChatVariablesService } from '../../../common/attachments/chatVariables.js'; @@ -535,6 +536,79 @@ suite('ChatService', () => { assert.ok(invokedRequests[1].includes('steering3'), 'Combined message should include steering3'); assert.ok(invokedRequests[1].includes('\n\n'), 'Combined message should use \\n\\n as separator'); }); + test('cancelCurrentRequestForSession waits for response completion', async () => { + const requestStarted = new DeferredPromise(); + const completeRequest = new DeferredPromise(); + + const slowAgent: IChatAgentImplementation = { + async invoke(request, progress, history, token) { + requestStarted.complete(); + const listener = token.onCancellationRequested(() => { + listener.dispose(); + // Simulate some cleanup delay before completing + setTimeout(() => completeRequest.complete(), 10); + }); + await completeRequest.p; + return {}; + }, + }; + + testDisposables.add(chatAgentService.registerAgent('slowAgent', { ...getAgentData('slowAgent'), isDefault: true })); + testDisposables.add(chatAgentService.registerAgentImplementation('slowAgent', slowAgent)); + + const testService = createChatService(); + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + + const response = await testService.sendRequest(model.sessionResource, 'test request', { agentId: 'slowAgent' }); + ChatSendResult.assertSent(response); + + await requestStarted.p; + + // Cancel and await - should wait for the response to complete + await testService.cancelCurrentRequestForSession(model.sessionResource, 'test'); + + // After cancel resolves, the response model should have a result + const lastRequest = model.getRequests()[0]; + assert.ok(lastRequest.response, 'Response should exist after cancellation completes'); + assert.strictEqual(lastRequest.response.state, ResponseModelState.Cancelled, 'Response should be in Cancelled state'); + }); + + test('cancelCurrentRequestForSession returns after timeout if response does not complete', async () => { + const requestStarted = new DeferredPromise(); + const completeRequest = new DeferredPromise(); + + const hangingAgent: IChatAgentImplementation = { + async invoke(request, progress, history, token) { + requestStarted.complete(); + // Wait for external signal, ignoring cancellation to simulate a hung agent + await completeRequest.p; + return {}; + }, + }; + + testDisposables.add(chatAgentService.registerAgent('hangingAgent', { ...getAgentData('hangingAgent'), isDefault: true })); + testDisposables.add(chatAgentService.registerAgentImplementation('hangingAgent', hangingAgent)); + + const testService = createChatService(); + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + + const response = await testService.sendRequest(model.sessionResource, 'test request', { agentId: 'hangingAgent' }); + ChatSendResult.assertSent(response); + + await requestStarted.p; + + // Cancel should return after timeout even though the agent has not completed. + // Use faked timers so raceTimeout's 1s setTimeout fires instantly. + await runWithFakedTimers({ useFakeTimers: true }, async () => { + await testService.cancelCurrentRequestForSession(model.sessionResource, 'test'); + }); + + // Let the agent finish so the test cleans up properly + completeRequest.complete(); + await response.data.responseCompletePromise; + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index f10ffa492b6..4911cfe126c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -88,7 +88,7 @@ export class MockChatService implements IChatService { removeRequest(sessionResource: URI, requestId: string): Promise { throw new Error('Method not implemented.'); } - cancelCurrentRequestForSession(sessionResource: URI, source?: string): void { + async cancelCurrentRequestForSession(sessionResource: URI, source?: string): Promise { throw new Error('Method not implemented.'); } setYieldRequested(sessionResource: URI): void { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 76aa39da942..d26f9270bdd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -605,7 +605,7 @@ export class InlineChatController implements IEditorContribution { if (!session) { return; } - this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); + await this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject'); await session.editingSession.reject(); session.dispose(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 4a008a59b0f..6879c6b4391 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -86,7 +86,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const store = new DisposableStore(); store.add(toDisposable(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); + void this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession'); chatModel.editingSession?.reject(); this._sessions.delete(uri); this._onDidChangeSessions.fire(this); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 2be6c5d9e96..be4f138c4ea 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -412,7 +412,7 @@ export class TerminalChatWidget extends Disposable { if (!model?.sessionResource) { return; } - this._chatService.cancelCurrentRequestForSession(model?.sessionResource, 'terminalChat'); + void this._chatService.cancelCurrentRequestForSession(model?.sessionResource, 'terminalChat'); } async viewInChat(): Promise { From 7bd2f8d4d6ce86cd5da34cff658296b43804c76a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:03:02 +0100 Subject: [PATCH 336/448] Session - add actions to filter changes (#300000) * Initial implementation * Get changes from last turn working * Update src/vs/workbench/api/common/extHostGitExtensionService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixup the PR * Another PR fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/platform/actions/common/actions.ts | 1 + .../contrib/changes/browser/changesView.ts | 193 +++++++++++++++++- .../browser/mainThreadGitExtensionService.ts | 24 ++- .../workbench/api/common/extHost.protocol.ts | 6 + .../api/common/extHostGitExtensionService.ts | 26 ++- .../contrib/git/browser/gitService.ts | 6 +- .../contrib/git/common/gitService.ts | 7 + 7 files changed, 251 insertions(+), 12 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 497ec1a43ab..5c1cf404a4c 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -262,6 +262,7 @@ export class MenuId { static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar'); static readonly ChatEditingSessionChangesToolbar = new MenuId('ChatEditingSessionChangesToolbar'); static readonly ChatEditingSessionApplySubmenu = new MenuId('ChatEditingSessionApplySubmenu'); + static readonly ChatEditingSessionChangesVersionsSubmenu = new MenuId('ChatEditingSessionChangesVersionsSubmenu'); static readonly ChatEditingEditorContent = new MenuId('ChatEditingEditorContent'); static readonly ChatEditingEditorHunk = new MenuId('ChatEditingEditorHunk'); static readonly ChatEditingDeletedNotebookCell = new MenuId('ChatEditingDeletedNotebookCell'); diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 9cf004af32a..efd27ab3a77 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -18,10 +18,10 @@ import { basename, dirname } from '../../../../base/common/path.js'; import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; -import { localize } from '../../../../nls.js'; +import { localize, localize2 } from '../../../../nls.js'; import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { MenuId, Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -44,6 +44,9 @@ import { IResourceLabel, ResourceLabels } from '../../../../workbench/browser/la import { ViewPane, IViewPaneOptions, ViewAction } from '../../../../workbench/browser/parts/views/viewPane.js'; import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js'; import { IViewDescriptorService } from '../../../../workbench/common/views.js'; +import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; @@ -58,7 +61,7 @@ import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/b import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; -import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; +import { IGitRepository, IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; const $ = dom.$; @@ -77,6 +80,17 @@ export const enum ChangesViewMode { const changesViewModeContextKey = new RawContextKey('changesViewMode', ChangesViewMode.List); +// --- Versions Mode + +const enum ChangesVersionMode { + AllChanges = 'allChanges', + LastTurn = 'lastTurn', + Uncommitted = 'uncommitted' +} + +const changesVersionModeContextKey = new RawContextKey('changesVersionMode', ChangesVersionMode.AllChanges); +const hasUncommittedChangesContextKey = new RawContextKey('hasUncommittedChanges', false); + // --- List Item type ChangeType = 'added' | 'modified' | 'deleted'; @@ -229,11 +243,24 @@ export class ChangesViewPane extends ViewPane { this.storageService.store('changesView.viewMode', mode, StorageScope.WORKSPACE, StorageTarget.USER); } + // Version mode (all changes, last turn, uncommitted) + private readonly versionModeObs = observableValue(this, ChangesVersionMode.AllChanges); + private readonly versionModeContextKey: IContextKey; + + setVersionMode(mode: ChangesVersionMode): void { + if (this.versionModeObs.get() === mode) { + return; + } + this.versionModeObs.set(mode, undefined); + this.versionModeContextKey.set(mode); + } + // Track the active session used by this view private readonly activeSession: IObservableWithChange; private readonly activeSessionFileCountObs: IObservableWithChange; private readonly activeSessionHasChangesObs: IObservableWithChange; private readonly activeSessionRepositoryChangesObs: IObservableWithChange; + private readonly activeSessionRepositoryObs: IObservableWithChange | undefined>; get activeSessionHasChanges(): IObservable { return this.activeSessionHasChangesObs; @@ -272,6 +299,10 @@ export class ChangesViewPane extends ViewPane { this.viewModeContextKey = changesViewModeContextKey.bindTo(contextKeyService); this.viewModeContextKey.set(initialMode); + // Version mode + this.versionModeContextKey = changesVersionModeContextKey.bindTo(contextKeyService); + this.versionModeContextKey.set(ChangesVersionMode.AllChanges); + // Track active session from sessions management service this.activeSession = derivedOpts({ equalsFn: (a, b) => isEqual(a?.resource, b?.resource), @@ -290,7 +321,7 @@ export class ChangesViewPane extends ViewPane { }).recomputeInitiallyAndOnChange(this._store); // Track active session repository changes - const repositoryObs = derived(reader => { + this.activeSessionRepositoryObs = derived(reader => { const activeSessionWorktree = this.activeSession.read(reader)?.worktree; if (!activeSessionWorktree) { return undefined; @@ -300,19 +331,22 @@ export class ChangesViewPane extends ViewPane { }); this.activeSessionRepositoryChangesObs = derived(reader => { - const repository = repositoryObs.read(reader)?.read(reader); + const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader); if (!repository) { return undefined; } const state = repository.value?.state.read(reader); + const headCommit = state?.HEAD?.commit; return (state?.workingTreeChanges ?? []).map(change => { const isDeletion = change.modifiedUri === undefined; const isAddition = change.originalUri === undefined; + const fileUri = change.modifiedUri ?? change.uri; return { type: 'file', - uri: change.modifiedUri ?? change.uri, - originalUri: change.originalUri, + uri: fileUri, + originalUri: isDeletion || !headCommit ? change.originalUri + : fileUri.with({ scheme: 'git', query: JSON.stringify({ path: fileUri.fsPath, ref: headCommit }) }), state: ModifiedFileEntryState.Accepted, isDeletion, changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', @@ -558,15 +592,65 @@ export class ChangesViewPane extends ViewPane { }); }); + // Create observable for last turn changes using diffBetweenWithStats + // Reactively computes the diff between HEAD^ and HEAD. Memoize the diff observable so + // that we only recompute it when the HEAD commit id actually changes. + const headCommitObs = derived(reader => { + const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader)?.value; + return repository?.state.read(reader)?.HEAD?.commit; + }); + + const lastTurnChangesObs = derived(reader => { + const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader)?.value; + const headCommit = headCommitObs.read(reader); + if (!repository || !headCommit) { + return undefined; + } + + return observableFromPromise(repository.diffBetweenWithStats(`${headCommit}^`, headCommit)); + }); + // Combine both entry sources for display const combinedEntriesObs = derived(reader => { + const headCommit = headCommitObs.read(reader); const editEntries = editSessionEntriesObs.read(reader); const sessionFiles = sessionFilesObs.read(reader); const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? []; + const versionMode = this.versionModeObs.read(reader); + + let sourceEntries: IChangesFileItem[]; + if (versionMode === ChangesVersionMode.Uncommitted) { + sourceEntries = repositoryFiles; + } else if (versionMode === ChangesVersionMode.LastTurn) { + const lastTurn = lastTurnChangesObs.read(reader); + const diffChanges = lastTurn?.read(reader).value ?? []; + const parentRef = headCommit ? `${headCommit}^` : ''; + sourceEntries = diffChanges.map(change => { + const isDeletion = change.modifiedUri === undefined; + const isAddition = change.originalUri === undefined; + const fileUri = change.modifiedUri ?? change.uri; + const originalUri = isAddition ? change.originalUri + : headCommit ? fileUri.with({ scheme: 'git', query: JSON.stringify({ path: fileUri.fsPath, ref: parentRef }) }) + : change.originalUri; + return { + type: 'file', + uri: fileUri, + originalUri, + state: ModifiedFileEntryState.Accepted, + isDeletion, + changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', + linesAdded: change.insertions, + linesRemoved: change.deletions, + reviewCommentCount: 0, + } satisfies IChangesFileItem; + }); + } else { + sourceEntries = [...editEntries, ...sessionFiles, ...repositoryFiles]; + } const resources = new Set(); const entries: IChangesFileItem[] = []; - for (const item of [...editEntries, ...sessionFiles, ...repositoryFiles]) { + for (const item of sourceEntries) { if (!resources.has(item.uri.fsPath)) { resources.add(item.uri.fsPath); entries.push(item); @@ -634,6 +718,18 @@ export class ChangesViewPane extends ViewPane { return files > 0; })); + // Also bind to the ViewPane's scoped context key service so the ViewTitle menu can evaluate it + this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, r => { + const { files } = topLevelStats.read(r); + return files > 0; + })); + + // Track whether there are uncommitted (working tree) changes + this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, r => { + const repositoryFiles = this.activeSessionRepositoryChangesObs.read(r); + return (repositoryFiles?.length ?? 0) > 0; + })); + // Set context key for PR state from session metadata const hasOpenPullRequestKey = scopedContextKeyService.createKey('sessions.hasOpenPullRequest', false); this.renderDisposables.add(autorun(reader => { @@ -1163,3 +1259,84 @@ class SetChangesTreeViewModeAction extends ViewAction { registerAction2(SetChangesListViewModeAction); registerAction2(SetChangesTreeViewModeAction); + +// --- Versions Submenu + +MenuRegistry.appendMenuItem(MenuId.ViewTitle, { + submenu: MenuId.ChatEditingSessionChangesVersionsSubmenu, + title: localize2('versionsActions', 'Versions'), + icon: Codicon.versions, + group: 'navigation', + order: 9, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', CHANGES_VIEW_ID), IsSessionsWindowContext, ChatContextKeys.hasAgentSessionChanges), +}); + +class AllChangesAction extends Action2 { + constructor() { + super({ + id: 'chatEditing.versionsAllChanges', + title: localize2('chatEditing.versionsAllChanges', 'All Changes'), + category: CHAT_CATEGORY, + toggled: changesVersionModeContextKey.isEqualTo(ChangesVersionMode.AllChanges), + menu: [{ + id: MenuId.ChatEditingSessionChangesVersionsSubmenu, + group: '1_changes', + order: 1, + }], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const view = viewsService.getActiveViewWithId(CHANGES_VIEW_ID); + view?.setVersionMode(ChangesVersionMode.AllChanges); + } +} +registerAction2(AllChangesAction); + +class LastTurnChangesAction extends Action2 { + constructor() { + super({ + id: 'chatEditing.versionsLastTurnChanges', + title: localize2('chatEditing.versionsLastTurnChanges', "Last Turn's Changes"), + category: CHAT_CATEGORY, + toggled: changesVersionModeContextKey.isEqualTo(ChangesVersionMode.LastTurn), + menu: [{ + id: MenuId.ChatEditingSessionChangesVersionsSubmenu, + group: '1_changes', + order: 2, + }], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const view = viewsService.getActiveViewWithId(CHANGES_VIEW_ID); + view?.setVersionMode(ChangesVersionMode.LastTurn); + } +} +registerAction2(LastTurnChangesAction); + +class UncommittedChangesAction extends Action2 { + constructor() { + super({ + id: 'chatEditing.versionsUncommittedChanges', + title: localize2('chatEditing.versionsUncommittedChanges', 'Uncommitted Changes'), + category: CHAT_CATEGORY, + toggled: changesVersionModeContextKey.isEqualTo(ChangesVersionMode.Uncommitted), + precondition: hasUncommittedChangesContextKey, + menu: [{ + id: MenuId.ChatEditingSessionChangesVersionsSubmenu, + group: '2_uncommitted', + order: 1, + }], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const view = viewsService.getActiveViewWithId(CHANGES_VIEW_ID); + view?.setVersionMode(ChangesVersionMode.Uncommitted); + } +} +registerAction2(UncommittedChangesAction); diff --git a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts index ad414190cd6..40005d672df 100644 --- a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts @@ -8,9 +8,9 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../base/common/map.js'; import { URI } from '../../../base/common/uri.js'; import { GitRepository } from '../../contrib/git/browser/gitService.js'; -import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType, GitRepositoryState, GitBranch, GitChange, IGitRepository } from '../../contrib/git/common/gitService.js'; +import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType, GitRepositoryState, GitBranch, GitChange, GitDiffChange, IGitRepository } from '../../contrib/git/common/gitService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostContext, ExtHostGitExtensionShape, GitRefTypeDto, GitRepositoryStateDto, MainContext, MainThreadGitExtensionShape } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostGitExtensionShape, GitDiffChangeDto, GitRefTypeDto, GitRepositoryStateDto, MainContext, MainThreadGitExtensionShape } from '../common/extHost.protocol.js'; function toGitRefType(type: GitRefTypeDto): GitRefType { switch (type) { @@ -21,6 +21,16 @@ function toGitRefType(type: GitRefTypeDto): GitRefType { } } +function toGitDiffChange(dto: GitDiffChangeDto): GitDiffChange { + return { + uri: URI.revive(dto.uri), + originalUri: dto.originalUri ? URI.revive(dto.originalUri) : undefined, + modifiedUri: dto.modifiedUri ? URI.revive(dto.modifiedUri) : undefined, + insertions: dto.insertions, + deletions: dto.deletions, + }; +} + function toGitRepositoryState(dto: GitRepositoryStateDto | undefined): GitRepositoryState { return { HEAD: dto?.HEAD ? { @@ -144,6 +154,16 @@ export class MainThreadGitExtensionService extends Disposable implements MainThr } satisfies GitRef)); } + async diffBetweenWithStats(root: URI, ref1: string, ref2: string, path?: string): Promise { + const handle = this._repositoryHandles.get(root); + if (handle === undefined) { + return []; + } + + const result = await this._proxy.$diffBetweenWithStats(handle, ref1, ref2, path); + return result.map(toGitDiffChange); + } + async $onDidChangeRepository(handle: number): Promise { const repository = this._repositories.get(handle); if (!repository) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1f3304a5d75..eb65735e58a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3634,6 +3634,11 @@ export interface GitChangeDto { readonly modifiedUri: UriComponents | undefined; } +export interface GitDiffChangeDto extends GitChangeDto { + readonly insertions: number; + readonly deletions: number; +} + export interface GitRepositoryStateDto { readonly HEAD?: GitBranchDto; readonly mergeChanges: readonly GitChangeDto[]; @@ -3663,6 +3668,7 @@ export interface ExtHostGitExtensionShape { $openRepository(root: UriComponents): Promise<{ handle: number; rootUri: UriComponents; state: GitRepositoryStateDto } | undefined>; $getRefs(handle: number, query: GitRefQueryDto, token?: CancellationToken): Promise; $getRepositoryState(handle: number): Promise; + $diffBetweenWithStats(handle: number, ref1: string, ref2: string, path?: string): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostGitExtensionService.ts b/src/vs/workbench/api/common/extHostGitExtensionService.ts index 6c2960f0383..93f8c7ee7da 100644 --- a/src/vs/workbench/api/common/extHostGitExtensionService.ts +++ b/src/vs/workbench/api/common/extHostGitExtensionService.ts @@ -11,7 +11,7 @@ import { ExtensionIdentifier } from '../../../platform/extensions/common/extensi import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { IExtHostExtensionService } from './extHostExtensionService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; -import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js'; +import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitDiffChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js'; import { ResourceMap } from '../../../base/common/map.js'; const GIT_EXTENSION_ID = 'vscode.git'; @@ -91,12 +91,18 @@ function toGitRepositoryStateDto(state: RepositoryState): GitRepositoryStateDto }; } +interface DiffChange extends Change { + readonly insertions: number; + readonly deletions: number; +} + interface Repository { readonly rootUri: vscode.Uri; readonly state: RepositoryState; status(): Promise; getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise; + diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; } interface Change { @@ -279,6 +285,24 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi return toGitRepositoryStateDto(repository.state); } + async $diffBetweenWithStats(handle: number, ref1: string, ref2: string, path?: string): Promise { + const repository = this._repositories.get(handle); + if (!repository) { + return []; + } + + try { + const changes = await repository.diffBetweenWithStats(ref1, ref2, path); + return changes.map(c => ({ + ...toGitChangeDto(c), + insertions: c.insertions, + deletions: c.deletions, + })); + } catch { + return []; + } + } + private async _ensureGitApi(): Promise { if (this._gitApi) { return this._gitApi; diff --git a/src/vs/workbench/contrib/git/browser/gitService.ts b/src/vs/workbench/contrib/git/browser/gitService.ts index 2406d972a3b..ca34f506015 100644 --- a/src/vs/workbench/contrib/git/browser/gitService.ts +++ b/src/vs/workbench/contrib/git/browser/gitService.ts @@ -7,7 +7,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; -import { IGitService, IGitExtensionDelegate, GitRef, GitRefQuery, IGitRepository, GitRepositoryState } from '../common/gitService.js'; +import { IGitService, IGitExtensionDelegate, GitRef, GitRefQuery, IGitRepository, GitRepositoryState, GitDiffChange } from '../common/gitService.js'; import { ISettableObservable, observableValueOpts } from '../../../../base/common/observable.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { AutoOpenBarrier } from '../../../../base/common/async.js'; @@ -81,4 +81,8 @@ export class GitRepository extends Disposable implements IGitRepository { async getRefs(query: GitRefQuery, token?: CancellationToken): Promise { return this.delegate.getRefs(this.rootUri, query, token); } + + async diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise { + return this.delegate.diffBetweenWithStats(this.rootUri, ref1, ref2, path); + } } diff --git a/src/vs/workbench/contrib/git/common/gitService.ts b/src/vs/workbench/contrib/git/common/gitService.ts index 2217f2200db..5f1b6284838 100644 --- a/src/vs/workbench/contrib/git/common/gitService.ts +++ b/src/vs/workbench/contrib/git/common/gitService.ts @@ -35,6 +35,11 @@ export interface GitChange { readonly modifiedUri: URI | undefined; } +export interface GitDiffChange extends GitChange { + readonly insertions: number; + readonly deletions: number; +} + export interface GitRepositoryState { readonly HEAD?: GitBranch; readonly mergeChanges: readonly GitChange[]; @@ -62,6 +67,7 @@ export interface IGitRepository { updateState(state: GitRepositoryState): void; getRefs(query: GitRefQuery, token?: CancellationToken): Promise; + diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; } export interface IGitExtensionDelegate { @@ -69,6 +75,7 @@ export interface IGitExtensionDelegate { openRepository(uri: URI): Promise; getRefs(root: URI, query?: GitRefQuery, token?: CancellationToken): Promise; + diffBetweenWithStats(root: URI, ref1: string, ref2: string, path?: string): Promise; } export const IGitService = createDecorator('gitService'); From 1eb2581989c23047ff009b68e910e48f51d4d3d7 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 7 Mar 2026 18:26:26 -0800 Subject: [PATCH 337/448] Optimize chat anchor widget (#300009) Lazily create scoped CKS: This can be expensive and is not needed until the context menu is shown And remove the global themeService listener. We can bring it back when the chat response is correctly fully optimized. --- .../attachments/chatAttachmentWidgets.ts | 32 +++++++++++------- .../chatInlineAnchorWidget.ts | 33 +++++++++++-------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index a502dd82f1e..9a283997160 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -1430,21 +1430,27 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces e.dataTransfer?.setDragImage(widget, 0, 0); })); - // Context menu (context key service created eagerly for keybinding preconditions, - // but resource context and provider contexts are initialized lazily on first use) - const scopedContextKeyService = store.add(parentContextKeyService.createScoped(widget)); - chatAttachmentResourceContextKey.bindTo(scopedContextKeyService).set(attachment.value.uri.toString()); - setResourceContext(accessor, scopedContextKeyService, attachment.value.uri); - + // Context menu (context key service and resource contexts are initialized lazily on first context menu open) + let scopedContextKeyService: IScopedContextKeyService | undefined; let providerContexts: ReadonlyArray<[IContextKey, LanguageFeatureRegistry]> | undefined; + const ensureContextKeyService = () => { + if (!scopedContextKeyService) { + scopedContextKeyService = store.add(parentContextKeyService.createScoped(widget)); + chatAttachmentResourceContextKey.bindTo(scopedContextKeyService).set(attachment.value.uri.toString()); + setResourceContext(accessor, scopedContextKeyService, attachment.value.uri); + } + return scopedContextKeyService; + }; + const ensureProviderContexts = () => { + const cks = ensureContextKeyService(); if (!providerContexts) { providerContexts = [ - [EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider], - [EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider], - [EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider], - [EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider], + [EditorContextKeys.hasDefinitionProvider.bindTo(cks), languageFeaturesService.definitionProvider], + [EditorContextKeys.hasReferenceProvider.bindTo(cks), languageFeaturesService.referenceProvider], + [EditorContextKeys.hasImplementationProvider.bindTo(cks), languageFeaturesService.implementationProvider], + [EditorContextKeys.hasTypeDefinitionProvider.bindTo(cks), languageFeaturesService.typeDefinitionProvider], ]; } }; @@ -1466,6 +1472,8 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); dom.EventHelper.stop(domEvent, true); + const cks = ensureContextKeyService(); + try { await updateContextKeys(); } catch (e) { @@ -1473,10 +1481,10 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces } contextMenuService.showContextMenu({ - contextKeyService: scopedContextKeyService, + contextKeyService: cks, getAnchor: () => event, getActions: () => { - const menu = menuService.getMenuActions(contextMenuId, scopedContextKeyService, { arg: attachment.value }); + const menu = menuService.getMenuActions(contextMenuId, cks, { arg: attachment.value }); return getFlatContextMenuActions(menu); }, }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts index d499fda339e..1e589ede366 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts @@ -23,7 +23,7 @@ import { getFlatContextMenuActions } from '../../../../../../platform/actions/br import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; import { IResourceStat } from '../../../../../../platform/dnd/browser/dnd.js'; import { ITextResourceEditorInput } from '../../../../../../platform/editor/common/editor.js'; @@ -199,10 +199,6 @@ export class InlineAnchorWidget extends Disposable { iconEl.classList.add(...iconClasses); }; - this._register(themeService.onDidFileIconThemeChange(() => { - refreshIconClasses(); - })); - let isDirectory = false; fileService.stat(location.uri) .then(stat => { @@ -214,31 +210,42 @@ export class InlineAnchorWidget extends Disposable { }) .catch(() => { }); - // Context menu - const contextKeyService = this._register(originalContextKeyService.createScoped(element)); - chatAttachmentResourceContextKey.bindTo(contextKeyService).set(location.uri.toString()); - const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService); + // Context menu (context key service created lazily on first context menu open) + let contextKeyService: IContextKeyService | undefined; + let isFolderContext: IContextKey | undefined; let contextMenuInitialized = false; + + const ensureContextKeyService = () => { + if (!contextKeyService) { + contextKeyService = this._register(originalContextKeyService.createScoped(element)); + chatAttachmentResourceContextKey.bindTo(contextKeyService).set(location.uri.toString()); + isFolderContext = ExplorerFolderContext.bindTo(contextKeyService); + } + return contextKeyService; + }; + this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, async domEvent => { const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); dom.EventHelper.stop(domEvent, true); + const cks = ensureContextKeyService(); + if (!contextMenuInitialized) { contextMenuInitialized = true; - const resourceContextKey = new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService); + const resourceContextKey = new StaticResourceContextKey(cks, fileService, languageService, modelService); resourceContextKey.set(location.uri); } - isFolderContext.set(isDirectory); + isFolderContext!.set(isDirectory); if (this._store.isDisposed) { return; } contextMenuService.showContextMenu({ - contextKeyService, + contextKeyService: cks, getAnchor: () => event, getActions: () => { - const menu = menuService.getMenuActions(MenuId.ChatInlineResourceAnchorContext, contextKeyService, { arg: location.uri }); + const menu = menuService.getMenuActions(MenuId.ChatInlineResourceAnchorContext, cks, { arg: location.uri }); return getFlatContextMenuActions(menu); }, }); From 2c51cfd4ddd8fa5ea1d793142958eabf492fb977 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Sun, 8 Mar 2026 17:10:05 +1100 Subject: [PATCH 338/448] Avoid continuing in Copilot CLI when already in Copilot CLI (#300025) * refactor: enhance actionProvider to utilize contextKeyService for session type checks * refactor: add contextKeyService to ChatSuggestNextWidget for session type handling * refactor: simplify actionProvider by removing contextKeyService dependency --- .../widget/chatContentParts/chatSuggestNextWidget.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts index c9e3d0c4a96..8952b2339b1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts @@ -9,7 +9,9 @@ import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { IChatMode } from '../../../common/chatModes.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; import { IHandOff } from '../../../common/promptSyntax/promptFileParser.js'; @@ -36,7 +38,8 @@ export class ChatSuggestNextWidget extends Disposable { constructor( @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IChatSessionsService private readonly chatSessionsService: IChatSessionsService + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); this.domNode = this.createSuggestNextWidget(); @@ -126,11 +129,15 @@ export class ChatSuggestNextWidget extends Disposable { // Get chat session contributions to show in chevron dropdown // Filter to only first-party providers that support "continue in". // TODO: Expand later to any agent with `canDelegate` === true. + const currentSessionType = this.contextKeyService.getContextKeyValue(ChatContextKeys.chatSessionType.key); const contributions = this.chatSessionsService.getAllChatSessionContributions(); const availableContributions = contributions.filter(c => { if (!c.canDelegate) { return false; } + if (c.type === currentSessionType) { + return false; + } const provider = getAgentSessionProvider(c.type); return provider !== undefined && getAgentCanContinueIn(provider); }); From 4fb8242c5fc597352a8c58c03aa261fe354e2385 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Sat, 7 Mar 2026 23:03:35 -0800 Subject: [PATCH 339/448] Bump xterm to fix kitty keyboard protocol (#299833) --- package-lock.json | 96 ++++++++++++++++++------------------ package.json | 20 ++++---- remote/package-lock.json | 96 ++++++++++++++++++------------------ remote/package.json | 20 ++++---- remote/web/package-lock.json | 88 ++++++++++++++++----------------- remote/web/package.json | 18 +++---- 6 files changed, 169 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34cb015609d..6ff1dcbecb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,16 +30,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.168", - "@xterm/addon-image": "^0.10.0-beta.168", - "@xterm/addon-ligatures": "^0.11.0-beta.168", - "@xterm/addon-progress": "^0.3.0-beta.168", - "@xterm/addon-search": "^0.17.0-beta.168", - "@xterm/addon-serialize": "^0.15.0-beta.168", - "@xterm/addon-unicode11": "^0.10.0-beta.168", - "@xterm/addon-webgl": "^0.20.0-beta.167", - "@xterm/headless": "^6.1.0-beta.168", - "@xterm/xterm": "^6.1.0-beta.168", + "@xterm/addon-clipboard": "^0.3.0-beta.169", + "@xterm/addon-image": "^0.10.0-beta.169", + "@xterm/addon-ligatures": "^0.11.0-beta.169", + "@xterm/addon-progress": "^0.3.0-beta.169", + "@xterm/addon-search": "^0.17.0-beta.169", + "@xterm/addon-serialize": "^0.15.0-beta.169", + "@xterm/addon-unicode11": "^0.10.0-beta.169", + "@xterm/addon-webgl": "^0.20.0-beta.168", + "@xterm/headless": "^6.1.0-beta.169", + "@xterm/xterm": "^6.1.0-beta.169", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -4343,30 +4343,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", - "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", + "version": "0.3.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.169.tgz", + "integrity": "sha512-EJtjBTEAmgPCLRCwR0sF3tkOlZqswKX33Su9oeuAxjuFKsW5WvzOrGOAkMsfrc1i9zMUyzEcAKRknZyGYgr6ag==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", - "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", + "version": "0.10.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.169.tgz", + "integrity": "sha512-uNkn/31WeQ3qei98p3Z4TC/LNajsI8T/D0vXKWj45VOS5gCnvogTsagNNbdbV9ifEQsPg/bP9oPjWIpW2V0GGQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", - "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", + "version": "0.11.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.169.tgz", + "integrity": "sha512-CdInbfQsP1fFfXHiqsOl6TnpN75LV/BinLXrWfDe3u4yDFKB+1F8uFVqxSGKGoKdjZXn119CQZ+Op2WlsjIEPA==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -4376,7 +4376,7 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { @@ -4398,63 +4398,63 @@ "license": "ISC" }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", - "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", + "version": "0.3.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.169.tgz", + "integrity": "sha512-dPAEWBZ80Mwro4S98xzhyeW9o3hxJow5XkKnUxJBi28BwzmWijLS1eDdnI0jQDQkZ9T8bmvtHPCdypebG6LlgA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", - "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", + "version": "0.17.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.169.tgz", + "integrity": "sha512-omAD9odPBHT7zkM2W8mr6nIY63QNn8mu3iNlo53j+JBf1PBrwXxgOCrXkGYy4CcCX66oTtWnhqaHOQS9Eal8Dw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", - "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", + "version": "0.15.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.169.tgz", + "integrity": "sha512-PJeXwXORBsdo6VpPgdpMbiwZIhigaV9b9E/vKjUa75dy3qKGrHl/QmptDvOF1JCVjea9loaEvQ0pKHUoYJxzxA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", - "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", + "version": "0.10.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.169.tgz", + "integrity": "sha512-Nf3uCCibOGl1yN+cgzYwEf5iV1hVbBOT/qa2HUl3vJNulyOkBEa+GTQI4fi2zlPrt+/VCtbc3eW2qSxOD5I3sg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", - "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", + "version": "0.20.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.168.tgz", + "integrity": "sha512-5+WenEODiuatzM1/qKkDi9YQ6zM+N+iFM2P/jX4KjXwxsewHdzkExm/TZkbhkyhqlnZxxSeg3cKNxTnPevZhqQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.168.tgz", - "integrity": "sha512-9E8teq95/Hxv0r8WLMxfTgqnr1mDFiPpDJH71ZLWyb9TS9jAPQIoJF1h5FqOKx64NBxSQAJUJ7kr4yYbRWeE7g==", + "version": "6.1.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.169.tgz", + "integrity": "sha512-HQAy6ILa+ZdND7gG9hpII1aTpD6+KdlSL/k1DEmR6oKql3iPGRRGNIESSXymVp0G8F/XL+ZPCMHMBjrBHD7jAQ==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", - "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", + "version": "6.1.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.169.tgz", + "integrity": "sha512-8/wbTnEDRpyMh3bTC8dhb6g4FXobyAPA/1EAnuPWe+NLkiA8Fo1fjFVbEqe9TXa8KMHXEivI7Hqj1vPDt87A4w==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index a6fdb61d42a..40042c4c0df 100644 --- a/package.json +++ b/package.json @@ -100,16 +100,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.168", - "@xterm/addon-image": "^0.10.0-beta.168", - "@xterm/addon-ligatures": "^0.11.0-beta.168", - "@xterm/addon-progress": "^0.3.0-beta.168", - "@xterm/addon-search": "^0.17.0-beta.168", - "@xterm/addon-serialize": "^0.15.0-beta.168", - "@xterm/addon-unicode11": "^0.10.0-beta.168", - "@xterm/addon-webgl": "^0.20.0-beta.167", - "@xterm/headless": "^6.1.0-beta.168", - "@xterm/xterm": "^6.1.0-beta.168", + "@xterm/addon-clipboard": "^0.3.0-beta.169", + "@xterm/addon-image": "^0.10.0-beta.169", + "@xterm/addon-ligatures": "^0.11.0-beta.169", + "@xterm/addon-progress": "^0.3.0-beta.169", + "@xterm/addon-search": "^0.17.0-beta.169", + "@xterm/addon-serialize": "^0.15.0-beta.169", + "@xterm/addon-unicode11": "^0.10.0-beta.169", + "@xterm/addon-webgl": "^0.20.0-beta.168", + "@xterm/headless": "^6.1.0-beta.169", + "@xterm/xterm": "^6.1.0-beta.169", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index d936270a66b..3f11da9088f 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -22,16 +22,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.168", - "@xterm/addon-image": "^0.10.0-beta.168", - "@xterm/addon-ligatures": "^0.11.0-beta.168", - "@xterm/addon-progress": "^0.3.0-beta.168", - "@xterm/addon-search": "^0.17.0-beta.168", - "@xterm/addon-serialize": "^0.15.0-beta.168", - "@xterm/addon-unicode11": "^0.10.0-beta.168", - "@xterm/addon-webgl": "^0.20.0-beta.167", - "@xterm/headless": "^6.1.0-beta.168", - "@xterm/xterm": "^6.1.0-beta.168", + "@xterm/addon-clipboard": "^0.3.0-beta.169", + "@xterm/addon-image": "^0.10.0-beta.169", + "@xterm/addon-ligatures": "^0.11.0-beta.169", + "@xterm/addon-progress": "^0.3.0-beta.169", + "@xterm/addon-search": "^0.17.0-beta.169", + "@xterm/addon-serialize": "^0.15.0-beta.169", + "@xterm/addon-unicode11": "^0.10.0-beta.169", + "@xterm/addon-webgl": "^0.20.0-beta.168", + "@xterm/headless": "^6.1.0-beta.169", + "@xterm/xterm": "^6.1.0-beta.169", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -579,30 +579,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", - "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", + "version": "0.3.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.169.tgz", + "integrity": "sha512-EJtjBTEAmgPCLRCwR0sF3tkOlZqswKX33Su9oeuAxjuFKsW5WvzOrGOAkMsfrc1i9zMUyzEcAKRknZyGYgr6ag==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", - "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", + "version": "0.10.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.169.tgz", + "integrity": "sha512-uNkn/31WeQ3qei98p3Z4TC/LNajsI8T/D0vXKWj45VOS5gCnvogTsagNNbdbV9ifEQsPg/bP9oPjWIpW2V0GGQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", - "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", + "version": "0.11.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.169.tgz", + "integrity": "sha512-CdInbfQsP1fFfXHiqsOl6TnpN75LV/BinLXrWfDe3u4yDFKB+1F8uFVqxSGKGoKdjZXn119CQZ+Op2WlsjIEPA==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -612,67 +612,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", - "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", + "version": "0.3.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.169.tgz", + "integrity": "sha512-dPAEWBZ80Mwro4S98xzhyeW9o3hxJow5XkKnUxJBi28BwzmWijLS1eDdnI0jQDQkZ9T8bmvtHPCdypebG6LlgA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", - "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", + "version": "0.17.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.169.tgz", + "integrity": "sha512-omAD9odPBHT7zkM2W8mr6nIY63QNn8mu3iNlo53j+JBf1PBrwXxgOCrXkGYy4CcCX66oTtWnhqaHOQS9Eal8Dw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", - "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", + "version": "0.15.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.169.tgz", + "integrity": "sha512-PJeXwXORBsdo6VpPgdpMbiwZIhigaV9b9E/vKjUa75dy3qKGrHl/QmptDvOF1JCVjea9loaEvQ0pKHUoYJxzxA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", - "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", + "version": "0.10.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.169.tgz", + "integrity": "sha512-Nf3uCCibOGl1yN+cgzYwEf5iV1hVbBOT/qa2HUl3vJNulyOkBEa+GTQI4fi2zlPrt+/VCtbc3eW2qSxOD5I3sg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", - "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", + "version": "0.20.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.168.tgz", + "integrity": "sha512-5+WenEODiuatzM1/qKkDi9YQ6zM+N+iFM2P/jX4KjXwxsewHdzkExm/TZkbhkyhqlnZxxSeg3cKNxTnPevZhqQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.168.tgz", - "integrity": "sha512-9E8teq95/Hxv0r8WLMxfTgqnr1mDFiPpDJH71ZLWyb9TS9jAPQIoJF1h5FqOKx64NBxSQAJUJ7kr4yYbRWeE7g==", + "version": "6.1.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.169.tgz", + "integrity": "sha512-HQAy6ILa+ZdND7gG9hpII1aTpD6+KdlSL/k1DEmR6oKql3iPGRRGNIESSXymVp0G8F/XL+ZPCMHMBjrBHD7jAQ==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", - "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", + "version": "6.1.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.169.tgz", + "integrity": "sha512-8/wbTnEDRpyMh3bTC8dhb6g4FXobyAPA/1EAnuPWe+NLkiA8Fo1fjFVbEqe9TXa8KMHXEivI7Hqj1vPDt87A4w==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index cef1d91c5ca..bcacae3a1ab 100644 --- a/remote/package.json +++ b/remote/package.json @@ -17,16 +17,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.168", - "@xterm/addon-image": "^0.10.0-beta.168", - "@xterm/addon-ligatures": "^0.11.0-beta.168", - "@xterm/addon-progress": "^0.3.0-beta.168", - "@xterm/addon-search": "^0.17.0-beta.168", - "@xterm/addon-serialize": "^0.15.0-beta.168", - "@xterm/addon-unicode11": "^0.10.0-beta.168", - "@xterm/addon-webgl": "^0.20.0-beta.167", - "@xterm/headless": "^6.1.0-beta.168", - "@xterm/xterm": "^6.1.0-beta.168", + "@xterm/addon-clipboard": "^0.3.0-beta.169", + "@xterm/addon-image": "^0.10.0-beta.169", + "@xterm/addon-ligatures": "^0.11.0-beta.169", + "@xterm/addon-progress": "^0.3.0-beta.169", + "@xterm/addon-search": "^0.17.0-beta.169", + "@xterm/addon-serialize": "^0.15.0-beta.169", + "@xterm/addon-unicode11": "^0.10.0-beta.169", + "@xterm/addon-webgl": "^0.20.0-beta.168", + "@xterm/headless": "^6.1.0-beta.169", + "@xterm/xterm": "^6.1.0-beta.169", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 0452b9cff95..ba186896fed 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -14,15 +14,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.168", - "@xterm/addon-image": "^0.10.0-beta.168", - "@xterm/addon-ligatures": "^0.11.0-beta.168", - "@xterm/addon-progress": "^0.3.0-beta.168", - "@xterm/addon-search": "^0.17.0-beta.168", - "@xterm/addon-serialize": "^0.15.0-beta.168", - "@xterm/addon-unicode11": "^0.10.0-beta.168", - "@xterm/addon-webgl": "^0.20.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.168", + "@xterm/addon-clipboard": "^0.3.0-beta.169", + "@xterm/addon-image": "^0.10.0-beta.169", + "@xterm/addon-ligatures": "^0.11.0-beta.169", + "@xterm/addon-progress": "^0.3.0-beta.169", + "@xterm/addon-search": "^0.17.0-beta.169", + "@xterm/addon-serialize": "^0.15.0-beta.169", + "@xterm/addon-unicode11": "^0.10.0-beta.169", + "@xterm/addon-webgl": "^0.20.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.169", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", @@ -100,30 +100,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", - "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", + "version": "0.3.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.169.tgz", + "integrity": "sha512-EJtjBTEAmgPCLRCwR0sF3tkOlZqswKX33Su9oeuAxjuFKsW5WvzOrGOAkMsfrc1i9zMUyzEcAKRknZyGYgr6ag==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", - "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", + "version": "0.10.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.169.tgz", + "integrity": "sha512-uNkn/31WeQ3qei98p3Z4TC/LNajsI8T/D0vXKWj45VOS5gCnvogTsagNNbdbV9ifEQsPg/bP9oPjWIpW2V0GGQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", - "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", + "version": "0.11.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.169.tgz", + "integrity": "sha512-CdInbfQsP1fFfXHiqsOl6TnpN75LV/BinLXrWfDe3u4yDFKB+1F8uFVqxSGKGoKdjZXn119CQZ+Op2WlsjIEPA==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -133,58 +133,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", - "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", + "version": "0.3.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.169.tgz", + "integrity": "sha512-dPAEWBZ80Mwro4S98xzhyeW9o3hxJow5XkKnUxJBi28BwzmWijLS1eDdnI0jQDQkZ9T8bmvtHPCdypebG6LlgA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", - "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", + "version": "0.17.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.169.tgz", + "integrity": "sha512-omAD9odPBHT7zkM2W8mr6nIY63QNn8mu3iNlo53j+JBf1PBrwXxgOCrXkGYy4CcCX66oTtWnhqaHOQS9Eal8Dw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", - "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", + "version": "0.15.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.169.tgz", + "integrity": "sha512-PJeXwXORBsdo6VpPgdpMbiwZIhigaV9b9E/vKjUa75dy3qKGrHl/QmptDvOF1JCVjea9loaEvQ0pKHUoYJxzxA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", - "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", + "version": "0.10.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.169.tgz", + "integrity": "sha512-Nf3uCCibOGl1yN+cgzYwEf5iV1hVbBOT/qa2HUl3vJNulyOkBEa+GTQI4fi2zlPrt+/VCtbc3eW2qSxOD5I3sg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", - "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", + "version": "0.20.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.168.tgz", + "integrity": "sha512-5+WenEODiuatzM1/qKkDi9YQ6zM+N+iFM2P/jX4KjXwxsewHdzkExm/TZkbhkyhqlnZxxSeg3cKNxTnPevZhqQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.168" + "@xterm/xterm": "^6.1.0-beta.169" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", - "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", + "version": "6.1.0-beta.169", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.169.tgz", + "integrity": "sha512-8/wbTnEDRpyMh3bTC8dhb6g4FXobyAPA/1EAnuPWe+NLkiA8Fo1fjFVbEqe9TXa8KMHXEivI7Hqj1vPDt87A4w==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index d91fc919490..e7561a749a0 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -9,15 +9,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.168", - "@xterm/addon-image": "^0.10.0-beta.168", - "@xterm/addon-ligatures": "^0.11.0-beta.168", - "@xterm/addon-progress": "^0.3.0-beta.168", - "@xterm/addon-search": "^0.17.0-beta.168", - "@xterm/addon-serialize": "^0.15.0-beta.168", - "@xterm/addon-unicode11": "^0.10.0-beta.168", - "@xterm/addon-webgl": "^0.20.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.168", + "@xterm/addon-clipboard": "^0.3.0-beta.169", + "@xterm/addon-image": "^0.10.0-beta.169", + "@xterm/addon-ligatures": "^0.11.0-beta.169", + "@xterm/addon-progress": "^0.3.0-beta.169", + "@xterm/addon-search": "^0.17.0-beta.169", + "@xterm/addon-serialize": "^0.15.0-beta.169", + "@xterm/addon-unicode11": "^0.10.0-beta.169", + "@xterm/addon-webgl": "^0.20.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.169", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", From 2b1e30a017b1f787c543707aaebec140df2a6080 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 8 Mar 2026 13:38:04 +0100 Subject: [PATCH 340/448] modal - persist state (#300030) --- .../browser/parts/editor/editorParts.ts | 44 +++++++++++++++++- .../test/browser/modalEditorGroup.test.ts | 46 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 786d0bd239f..0e9f70f8c22 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -45,8 +45,15 @@ interface IEditorWorkingSetState extends IEditorWorkingSet { readonly auxiliary: IEditorPartsUIState; } +interface IModalEditorPartState { + readonly maximized: boolean; + readonly size?: { readonly width: number; readonly height: number }; + readonly position?: { readonly left: number; readonly top: number }; +} + interface IEditorPartsMemento { 'editorparts.state'?: IEditorPartsUIState; + 'editorparts.modalState'?: IModalEditorPartState; } export class EditorParts extends MultiWindowParts implements IEditorGroupsService, IEditorPartsView { @@ -75,6 +82,13 @@ export class EditorParts extends MultiWindowParts { @@ -336,8 +350,10 @@ export class EditorParts extends MultiWindowParts { @@ -662,5 +665,48 @@ suite('Modal Editor Group', () => { parts.activeModalEditorPart?.close(); }); + test('modal editor part state is remembered on close and reused on next open', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + // Create maximized modal and close it + const modalPart1 = await parts.createModalEditorPart({ maximized: true }); + modalPart1.close(); + + // Create a new modal — it should restore maximized state + const modalPart2 = await parts.createModalEditorPart(); + assert.strictEqual(modalPart2.maximized, true); + + modalPart2.close(); + }); + + test('modal editor part state restores from profile storage', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const storageService = instantiationService.get(IStorageService) as TestStorageService; + + // Pre-populate storage with modal state and clear memento cache + // so the next EditorParts instance reads fresh from storage + storageService.store('memento/workbench.editorParts', JSON.stringify({ + 'editorparts.modalState': { + maximized: true, + size: { width: 500, height: 400 }, + position: { left: 100, top: 50 } + } + }), StorageScope.PROFILE, StorageTarget.MACHINE); + Memento.clear(StorageScope.PROFILE); + + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + // Create modal — it should use state from storage + const modalPart = await parts.createModalEditorPart(); + assert.strictEqual(modalPart.maximized, true); + + modalPart.close(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); From bb18007fbba0de3bba458a9a88634502840a48ab Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 8 Mar 2026 19:18:21 +0100 Subject: [PATCH 341/448] fix retaining target mode (#300075) * fix: set isolation mode when new session is assigned * fix: update isolation mode handling in NewChatWidget and remove unused session reference * fix: remove unused session handling in BranchPicker and update NewChatWidget to set branch directly * fix: update repo handling in NewChatWidget and RepoPicker to set repository URI directly * fix: refactor repository URI handling in NewChatWidget to use a dedicated method * fix: preserve draft state in NewChatWidget to retain picker preferences during widget recreation * fix: enhance draft state preservation in NewChatWidget to conditionally store target and related properties * fix: update isolation mode handling in IsolationModePicker to use current mode as fallback --- .../contrib/chat/browser/branchPicker.ts | 13 +---- .../contrib/chat/browser/newChatViewPane.ts | 55 ++++++++++++++----- .../contrib/chat/browser/repoPicker.ts | 25 +-------- .../chat/browser/sessionTargetPicker.ts | 13 +---- 4 files changed, 45 insertions(+), 61 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/branchPicker.ts b/src/vs/sessions/contrib/chat/browser/branchPicker.ts index b5ada806bc2..0e4ff4ce967 100644 --- a/src/vs/sessions/contrib/chat/browser/branchPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/branchPicker.ts @@ -12,8 +12,6 @@ import { IActionWidgetService } from '../../../../platform/actionWidget/browser/ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; import { IGitRepository } from '../../../../workbench/contrib/git/common/gitService.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { INewSession } from './newSession.js'; - const COPILOT_WORKTREE_PATTERN = 'copilot-worktree-'; const FILTER_THRESHOLD = 10; @@ -32,7 +30,6 @@ export class BranchPicker extends Disposable { private _selectedBranch: string | undefined; private _preferredBranch: string | undefined; - private _newSession: INewSession | undefined; private _branches: string[] = []; private readonly _onDidChange = this._register(new Emitter()); @@ -62,13 +59,6 @@ export class BranchPicker extends Disposable { super(); } - /** - * Sets the new session that this picker writes to. - */ - setNewSession(session: INewSession | undefined): void { - this._newSession = session; - } - /** * Sets the git repository and loads its branches. * When undefined, the picker is shown disabled. @@ -78,7 +68,7 @@ export class BranchPicker extends Disposable { this._selectedBranch = undefined; if (!repository) { - this._newSession?.setBranch(undefined); + this._onDidChange.fire(undefined); this._setLoading(false); this._updateTriggerLabel(); return; @@ -196,7 +186,6 @@ export class BranchPicker extends Disposable { private _selectBranch(branch: string): void { if (this._selectedBranch !== branch) { this._selectedBranch = branch; - this._newSession?.setBranch(branch); this._onDidChange.fire(branch); this._updateTriggerLabel(); } diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index b033d523652..274cf54a4f5 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -219,6 +219,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { })); this._register(this._branchPicker.onDidChange((branch) => { + this._newSession.value?.setBranch(branch); this._syncIndicator.setBranch(branch); this._updateDraftState(); this._focusEditor(); @@ -234,13 +235,17 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { })); this._register(this._isolationModePicker.onDidChange((mode) => { + this._newSession.value?.setIsolationMode(mode); this._branchPicker.setVisible(mode === 'worktree'); this._syncIndicator.setVisible(mode === 'worktree'); this._updateDraftState(); this._focusEditor(); })); - this._register(this._repoPicker.onDidSelectRepo(() => { + this._register(this._repoPicker.onDidSelectRepo((repoId) => { + if (this._targetPicker.selectedTarget !== AgentSessionProviders.Background) { + this._newSession.value?.setRepoUri(this._getRepoUri(repoId)); + } this._updateDraftState(); })); @@ -364,13 +369,15 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Wire pickers to the new session and disconnect inactive ones const target = this._targetPicker.selectedTarget; if (target === AgentSessionProviders.Background) { - this._isolationModePicker.setNewSession(session); - this._branchPicker.setNewSession(session); - this._repoPicker.setNewSession(undefined); + session.setIsolationMode(this._isolationModePicker.isolationMode); + if (this._branchPicker.selectedBranch) { + session.setBranch(this._branchPicker.selectedBranch); + } } else { - this._isolationModePicker.setNewSession(undefined); - this._branchPicker.setNewSession(undefined); - this._repoPicker.setNewSession(session); + const selectedRepo = this._repoPicker.selectedRepo; + if (selectedRepo) { + session.setRepoUri(this._getRepoUri(selectedRepo)); + } } // Set the current model on the session (for local sessions) @@ -605,16 +612,20 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // For cloud targets, use the repo picker's selection const selectedRepo = this._repoPicker.selectedRepo; if (selectedRepo && selectedRepo.includes('/')) { - return URI.from({ - scheme: GITHUB_REMOTE_FILE_SCHEME, - authority: 'github', - path: `/${selectedRepo}/HEAD`, - }); + return this._getRepoUri(selectedRepo); } return undefined; } + private _getRepoUri(repoId: string): URI { + return URI.from({ + scheme: GITHUB_REMOTE_FILE_SCHEME, + authority: 'github', + path: `/${repoId}/HEAD`, + }); + } + private _createBottomToolbar(container: HTMLElement): void { const toolbar = dom.append(container, dom.$('.sessions-chat-toolbar')); @@ -1114,8 +1125,24 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } private _clearDraftState(): void { - this._draftState = undefined; - this.storageService.remove(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); + // Preserve picker preferences so they survive widget recreation + const target = this._targetPicker.selectedTarget; + const isLocal = target === AgentSessionProviders.Background; + const preserved: IDraftState = { + inputText: '', + attachments: [], + mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent }, + selectedModel: this._draftState?.selectedModel, + selections: [], + contrib: {}, + target, + isolationMode: isLocal ? this._isolationModePicker.isolationMode : undefined, + branch: isLocal ? this._branchPicker.selectedBranch : undefined, + folderUri: isLocal ? this._folderPicker.selectedFolderUri?.toString() : undefined, + repo: isLocal ? undefined : this._repoPicker.selectedRepo, + }; + this._draftState = preserved; + this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(preserved), StorageScope.WORKSPACE, StorageTarget.MACHINE); } saveState(): void { diff --git a/src/vs/sessions/contrib/chat/browser/repoPicker.ts b/src/vs/sessions/contrib/chat/browser/repoPicker.ts index bcd4506ea3a..0ac9de839be 100644 --- a/src/vs/sessions/contrib/chat/browser/repoPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/repoPicker.ts @@ -13,9 +13,6 @@ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../ import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { INewSession } from './newSession.js'; -import { URI } from '../../../../base/common/uri.js'; -import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; const OPEN_REPO_COMMAND = 'github.copilot.chat.cloudSessions.openRepository'; const STORAGE_KEY_LAST_REPO = 'agentSessions.lastPickedRepo'; @@ -42,9 +39,7 @@ export class RepoPicker extends Disposable { private _triggerElement: HTMLElement | undefined; private readonly _renderDisposables = this._register(new DisposableStore()); - private _browseGeneration = 0; - private _newSession: INewSession | undefined; private _selectedRepo: IRepoItem | undefined; private _recentlyPickedRepos: IRepoItem[] = []; @@ -76,18 +71,6 @@ export class RepoPicker extends Disposable { } catch { /* ignore */ } } - /** - * Sets the pending session that this picker writes to. - * If a repository is already selected, notifies the session. - */ - setNewSession(session: INewSession | undefined): void { - this._newSession = session; - this._browseGeneration++; - if (session && this._selectedRepo) { - this._setRepo(this._selectedRepo); - } - } - /** * Renders the repo picker trigger button into the given container. * Returns the container element. @@ -180,15 +163,13 @@ export class RepoPicker extends Disposable { this._addToRecentlyPicked(item); this.storageService.store(STORAGE_KEY_LAST_REPO, JSON.stringify(item), StorageScope.PROFILE, StorageTarget.MACHINE); this._updateTriggerLabel(); - this._setRepo(item); this._onDidSelectRepo.fire(item.id); } private async _browseForRepo(): Promise { - const generation = this._browseGeneration; try { const result: string | undefined = await this.commandService.executeCommand(OPEN_REPO_COMMAND); - if (result && generation === this._browseGeneration) { + if (result) { this._selectRepo({ id: result, name: result }); } } catch { @@ -270,8 +251,4 @@ export class RepoPicker extends Disposable { dom.append(this._triggerElement, renderIcon(Codicon.chevronDown)); } - private _setRepo(repo: IRepoItem): void { - this._newSession?.setRepoUri(URI.parse(`${GITHUB_REMOTE_FILE_SCHEME}://github/${repo.id}/HEAD`)); - } - } diff --git a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts index 88aaa11a7a0..68d175b8fbe 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts @@ -14,7 +14,6 @@ import { IActionWidgetService } from '../../../../platform/actionWidget/browser/ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { IGitRepository } from '../../../../workbench/contrib/git/common/gitService.js'; -import { INewSession } from './newSession.js'; // #region --- Session Target Picker --- @@ -138,7 +137,6 @@ export class IsolationModePicker extends Disposable { private _isolationMode: IsolationMode = 'worktree'; private _preferredIsolationMode: IsolationMode | undefined; - private _newSession: INewSession | undefined; private _repository: IGitRepository | undefined; private readonly _onDidChange = this._register(new Emitter()); @@ -158,13 +156,6 @@ export class IsolationModePicker extends Disposable { super(); } - /** - * Sets the pending session that this picker writes to. - */ - setNewSession(session: INewSession | undefined): void { - this._newSession = session; - } - /** * Sets the git repository. When undefined, worktree option is hidden * and isolation mode falls back to 'workspace'. @@ -174,8 +165,9 @@ export class IsolationModePicker extends Disposable { if (repository) { const preferred = this._preferredIsolationMode; this._preferredIsolationMode = undefined; - this._setMode(preferred ?? 'worktree'); + this._setMode(preferred ?? this._isolationMode); } else if (this._isolationMode === 'worktree') { + this._preferredIsolationMode ??= this._isolationMode; this._setMode('workspace'); } this._updateTriggerLabel(); @@ -280,7 +272,6 @@ export class IsolationModePicker extends Disposable { private _setMode(mode: IsolationMode): void { if (this._isolationMode !== mode) { this._isolationMode = mode; - this._newSession?.setIsolationMode(mode); this._onDidChange.fire(mode); this._updateTriggerLabel(); } From ef79c15d8c320b205b1be4f2d0402b114504bd2d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 8 Mar 2026 20:35:43 +0100 Subject: [PATCH 342/448] modal editor - tweaks to UI depending on setting (#300082) * modal editor - tweaks to UI depending on setting * Update src/vs/workbench/browser/parts/editor/modalEditorPart.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/parts/editor/editorPart.ts | 2 +- .../browser/parts/editor/modalEditorPart.ts | 37 +++++++----- .../test/browser/modalEditorGroup.test.ts | 58 +++++++++++++++++++ 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index c172d537754..31f83c70a11 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -168,7 +168,7 @@ export class EditorPart extends Part implements IEditorPart, readonly windowId: number, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService protected readonly configurationService: IConfigurationService, @IStorageService storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IHostService private readonly hostService: IHostService, diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 9d282dcd1a0..db862b6096c 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -103,6 +103,8 @@ const defaultModalEditorAllowableCommands = new Set([ NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, ]); +const USE_MODAL_EDITOR_SETTING = 'workbench.editor.useModal'; + export interface ICreateModalEditorPartResult { readonly part: ModalEditorPartImpl; readonly instantiationService: IInstantiationService; @@ -139,10 +141,10 @@ export class ModalEditorPart { } })); - let useModalMode = this.configurationService.getValue('workbench.editor.useModal'); + let useModalMode = this.configurationService.getValue(USE_MODAL_EDITOR_SETTING); disposables.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('workbench.editor.useModal')) { - useModalMode = this.configurationService.getValue('workbench.editor.useModal'); + if (e.affectsConfiguration(USE_MODAL_EDITOR_SETTING)) { + useModalMode = this.configurationService.getValue(USE_MODAL_EDITOR_SETTING); } })); @@ -271,6 +273,7 @@ export class ModalEditorPart { setVisibility(hasActions, editorActionsSeparator); }; disposables.add(Event.runAndSubscribe(modalEditorService.onDidActiveEditorChange, () => updateEditorActions())); + disposables.add(modalEditorService.onDidEditorsChange(() => editorPart.enforceModalPartOptions())); // Create global toolbar disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.ModalEditorTitle, { @@ -346,20 +349,19 @@ export class ModalEditorPart { const titleBarOffset = this.layoutService.mainContainerOffset.top; const dialogWidth = resizableElement.size.width; const dialogHeight = resizableElement.size.height; - const availableHeight = Math.max(containerDimension.height - titleBarOffset, 0); // Clamp to window bounds const minLeft = 0; const minTop = titleBarOffset; const maxLeft = Math.max(minLeft, containerDimension.width - dialogWidth); - const maxTop = Math.max(minTop, titleBarOffset + availableHeight - dialogHeight); + const maxTop = Math.max(minTop, containerDimension.height - dialogHeight); let newLeft = Math.max(minLeft, Math.min(maxLeft, startLeft + (moveEvent.clientX - startX))); let newTop = Math.max(minTop, Math.min(maxTop, startTop + (moveEvent.clientY - startY))); // Snap to center position when close const centerLeft = (containerDimension.width - dialogWidth) / 2; - const centerTop = titleBarOffset + (availableHeight - dialogHeight) / 2; + const centerTop = Math.max(titleBarOffset, (containerDimension.height - dialogHeight) / 2); if (Math.abs(newLeft - centerLeft) < MODAL_SNAP_THRESHOLD && Math.abs(newTop - centerTop) < MODAL_SNAP_THRESHOLD) { newLeft = centerLeft; @@ -381,9 +383,8 @@ export class ModalEditorPart { // Check if snapped to center — if so, clear custom position const containerDimension = this.layoutService.mainContainerDimension; const titleBarOffset = this.layoutService.mainContainerOffset.top; - const availableHeight = Math.max(containerDimension.height - titleBarOffset, 0); const centerLeft = (containerDimension.width - resizableElement.size.width) / 2; - const centerTop = titleBarOffset + (availableHeight - resizableElement.size.height) / 2; + const centerTop = Math.max(titleBarOffset, (containerDimension.height - resizableElement.size.height) / 2); if (Math.abs(currentLeft - centerLeft) < 1 && Math.abs(currentTop - centerTop) < 1) { editorPart.position = undefined; @@ -516,9 +517,7 @@ export class ModalEditorPart { resizableElement.domNode.style.top = `${clampedTop}px`; } else { const left = (containerDimension.width - width) / 2; - const top = editorPart.maximized - ? (containerDimension.height - height) / 2 // center in full window to stay close to title bar - : titleBarOffset + (availableHeight - height) / 2; + const top = Math.max(titleBarOffset, (containerDimension.height - height) / 2); // center in full window, but clamp to stay below the title bar resizableElement.domNode.style.left = `${left}px`; resizableElement.domNode.style.top = `${top}px`; } @@ -615,6 +614,12 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { } this.enforceModalPartOptions(); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(USE_MODAL_EDITOR_SETTING)) { + this.enforceModalPartOptions(); + } + })); } override create(parent: HTMLElement, options?: object): void { @@ -623,12 +628,16 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { super.create(parent, options); } - private enforceModalPartOptions(): void { + enforceModalPartOptions(): void { + const useModalForAll = this.configurationService.getValue(USE_MODAL_EDITOR_SETTING) === 'all'; + const editorCount = this.groups.reduce((count, group) => count + group.count, 0); + const showTabs = useModalForAll && editorCount > 1 ? 'multiple' : 'none'; + this.optionsDisposable.value = this.enforcePartOptions({ - showTabs: 'none', + showTabs, enablePreview: true, closeEmptyGroups: true, - tabActionCloseVisibility: false, + tabActionCloseVisibility: showTabs !== 'none', editorActionsLocation: 'hidden', tabHeight: 'default', wrapTabs: false, diff --git a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts index 763402f4210..7d37cde03e3 100644 --- a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts +++ b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts @@ -615,6 +615,64 @@ suite('Modal Editor Group', () => { assert.strictEqual(parts.activeModalEditorPart, undefined); }); + + test('shows tabs when multiple editors are open', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const configurationService = new TestConfigurationService(); + await configurationService.setUserConfiguration('workbench.editor.useModal', 'all'); + instantiationService.stub(IConfigurationService, configurationService); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + const editorService = disposables.add(instantiationService.createInstance(EditorService, undefined)); + instantiationService.stub(IEditorService, editorService); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + await editorService.openEditor(input1, { pinned: true }, MODAL_GROUP); + + const modalPart = parts.activeModalEditorPart!; + assert.ok(modalPart); + + // With 1 editor, tabs should be hidden + assert.strictEqual(modalPart.partOptions.showTabs, 'none'); + + // Open a second editor + const input2 = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); + await editorService.openEditor(input2, { pinned: true }, MODAL_GROUP); + + // With 2 editors, tabs should be visible + assert.strictEqual(modalPart.partOptions.showTabs, 'multiple'); + + modalPart.close(); + }); + + test('hides tabs when not in all mode even with multiple editors', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const configurationService = new TestConfigurationService(); + await configurationService.setUserConfiguration('workbench.editor.useModal', 'some'); + instantiationService.stub(IConfigurationService, configurationService); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + const editorService = disposables.add(instantiationService.createInstance(EditorService, undefined)); + instantiationService.stub(IEditorService, editorService); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + await editorService.openEditor(input1, { pinned: true }, MODAL_GROUP); + + const modalPart = parts.activeModalEditorPart!; + assert.ok(modalPart); + + const input2 = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); + await editorService.openEditor(input2, { pinned: true }, MODAL_GROUP); + + // With 'some' mode, tabs should remain hidden even with multiple editors + assert.strictEqual(modalPart.partOptions.showTabs, 'none'); + + modalPart.close(); + }); }); test('modal editor part editors can be moved to another group', async () => { From b3740268a392b2254228e57f836e1e3fe6ce6b2b Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:23:29 -0700 Subject: [PATCH 343/448] Fixing errors --- .../extension-editing/package-lock.json | 29 +++++++++++++++---- extensions/extension-editing/package.json | 2 +- .../extension-editing/src/extensionLinter.ts | 12 ++++---- src/tsconfig.vscode-dts.json | 2 +- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/extensions/extension-editing/package-lock.json b/extensions/extension-editing/package-lock.json index be1aa96eea6..d96f9a2bcca 100644 --- a/extensions/extension-editing/package-lock.json +++ b/extensions/extension-editing/package-lock.json @@ -14,18 +14,37 @@ "parse5": "^3.0.2" }, "devDependencies": { - "@types/markdown-it": "0.0.2", + "@types/markdown-it": "^14", "@types/node": "22.x" }, "engines": { "vscode": "^1.4.0" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/markdown-it": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.2.tgz", - "integrity": "sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= sha512-A2seE+zJYSjGHy7L/v0EN/xRfgv2A60TuXOwI8tt5aZxF4UeoYIkM2jERnNH8w4VFr7oFEm0lElGOao7fZgygQ==", - "dev": true + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "22.13.10", diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index 3e277dbbfd3..c491fbedca2 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -66,7 +66,7 @@ ] }, "devDependencies": { - "@types/markdown-it": "0.0.2", + "@types/markdown-it": "^14", "@types/node": "22.x" }, "repository": { diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 5c73304b4d8..6249500e2d1 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import { URL } from 'url'; import { parseTree, findNodeAtLocation, Node as JsonNode, getNodeValue } from 'jsonc-parser'; -import * as MarkdownItType from 'markdown-it'; +import type MarkdownIt from 'markdown-it'; import { commands, languages, workspace, Disposable, TextDocument, Uri, Diagnostic, Range, DiagnosticSeverity, Position, env, l10n } from 'vscode'; import { INormalizedVersion, normalizeVersion, parseVersion } from './extensionEngineValidation'; @@ -44,7 +44,7 @@ enum Context { } interface TokenAndPosition { - token: MarkdownItType.Token; + token: MarkdownIt.Token; begin: number; end: number; } @@ -67,7 +67,7 @@ export class ExtensionLinter { private packageJsonQ = new Set(); private readmeQ = new Set(); private timer: NodeJS.Timeout | undefined; - private markdownIt: MarkdownItType.MarkdownIt | undefined; + private markdownIt: MarkdownIt | undefined; private parse5: typeof import('parse5') | undefined; constructor() { @@ -292,7 +292,7 @@ export class ExtensionLinter { this.markdownIt = new ((await import('markdown-it')).default); } const tokens = this.markdownIt.parse(text, {}); - const tokensAndPositions: TokenAndPosition[] = (function toTokensAndPositions(this: ExtensionLinter, tokens: MarkdownItType.Token[], begin = 0, end = text.length): TokenAndPosition[] { + const tokensAndPositions: TokenAndPosition[] = (function toTokensAndPositions(this: ExtensionLinter, tokens: MarkdownIt.Token[], begin = 0, end = text.length): TokenAndPosition[] { const tokensAndPositions = tokens.map(token => { if (token.map) { const tokenBegin = document.offsetAt(new Position(token.map[0], 0)); @@ -313,7 +313,7 @@ export class ExtensionLinter { }); return tokensAndPositions.concat( ...tokensAndPositions.filter(tnp => tnp.token.children && tnp.token.children.length) - .map(tnp => toTokensAndPositions.call(this, tnp.token.children, tnp.begin, tnp.end)) + .map(tnp => toTokensAndPositions.call(this, tnp.token.children ?? [], tnp.begin, tnp.end)) ); }).call(this, tokens); @@ -373,7 +373,7 @@ export class ExtensionLinter { } } - private locateToken(text: string, begin: number, end: number, token: MarkdownItType.Token, content: string | null) { + private locateToken(text: string, begin: number, end: number, token: MarkdownIt.Token, content: string | null) { if (content) { const tokenBegin = text.indexOf(content, begin); if (tokenBegin !== -1) { diff --git a/src/tsconfig.vscode-dts.json b/src/tsconfig.vscode-dts.json index b83f686e4f3..fae0ce15c38 100644 --- a/src/tsconfig.vscode-dts.json +++ b/src/tsconfig.vscode-dts.json @@ -1,7 +1,7 @@ { "compilerOptions": { "noEmit": true, - "module": "None", + "module": "preserve", "experimentalDecorators": false, "noImplicitReturns": true, "noImplicitOverride": true, From a2f85b65d22c0f09cb75b45266caad14a9c63fd3 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:30:42 -0700 Subject: [PATCH 344/448] Fix a few more uint8array errors --- extensions/vscode-test-resolver/src/extension.browser.ts | 8 ++++---- extensions/vscode-test-resolver/src/extension.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/vscode-test-resolver/src/extension.browser.ts b/extensions/vscode-test-resolver/src/extension.browser.ts index 93703fde4df..e93a414a79a 100644 --- a/extensions/vscode-test-resolver/src/extension.browser.ts +++ b/extensions/vscode-test-resolver/src/extension.browser.ts @@ -24,7 +24,7 @@ export function activate(_context: vscode.ExtensionContext) { * actual WebSocket. */ class InitialManagedMessagePassing implements vscode.ManagedMessagePassing { - private readonly dataEmitter = new vscode.EventEmitter(); + private readonly dataEmitter = new vscode.EventEmitter>(); private readonly closeEmitter = new vscode.EventEmitter(); private readonly endEmitter = new vscode.EventEmitter(); @@ -38,7 +38,7 @@ class InitialManagedMessagePassing implements vscode.ManagedMessagePassing { public send(d: Uint8Array): void { if (this._actual) { // we already got the HTTP headers - this._actual.send(d); + this._actual.send(d as Uint8Array); return; } @@ -80,7 +80,7 @@ class OpeningManagedMessagePassing { private readonly socket: WebSocket; private isOpen = false; - private bufferedData: Uint8Array[] = []; + private bufferedData: Uint8Array[] = []; constructor( url: URL, @@ -119,7 +119,7 @@ class OpeningManagedMessagePassing { }); } - public send(d: Uint8Array): void { + public send(d: Uint8Array): void { if (!this.isOpen) { this.bufferedData.push(d); return; diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 3e6c9f0ad49..c342647e672 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -211,12 +211,12 @@ export function activate(context: vscode.ExtensionContext) { console.log('Connecting via a managed authority'); return Promise.resolve(new vscode.ManagedResolvedAuthority(async () => { const remoteSocket = net.createConnection({ port: serverAddr.port }); - const dataEmitter = new vscode.EventEmitter(); + const dataEmitter = new vscode.EventEmitter>(); const closeEmitter = new vscode.EventEmitter(); const endEmitter = new vscode.EventEmitter(); await new Promise((res, rej) => { - remoteSocket.on('data', d => dataEmitter.fire(d)) + remoteSocket.on('data', d => dataEmitter.fire(d as Uint8Array)) .on('error', err => { rej(); closeEmitter.fire(err); }) .on('close', () => endEmitter.fire()) .on('end', () => endEmitter.fire()) From 625d9dd956627df20dba1f1a0efe261676771c83 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:42:50 -0700 Subject: [PATCH 345/448] Add explicit `mocha` reference --- test/monaco/tsconfig.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/monaco/tsconfig.json b/test/monaco/tsconfig.json index dcc46d24c5a..75bd907e41a 100644 --- a/test/monaco/tsconfig.json +++ b/test/monaco/tsconfig.json @@ -9,6 +9,9 @@ "sourceMap": true, "skipLibCheck": true, "declaration": true, + "types": [ + "mocha" + ], "lib": [ "esnext", // for #201187 "dom" From 5e22e5ab3e50a49fef4d875923ee8f9fe0746fdb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 9 Mar 2026 18:26:23 +1100 Subject: [PATCH 346/448] Change migration confirmation UI for delegation (#300117) * Change migration confirmation UI for delegation * Updates * Updates * Updates * Updates * Misc changes --- .../chatAccessibilityProvider.ts | 2 + .../chatResponseAccessibleView.ts | 14 +- .../tools/languageModelToolsService.ts | 11 +- .../chatReferencesContentPart.ts | 3 +- .../media/chatConfirmationWidget.css | 132 ++++++++ .../chatModifiedFilesConfirmationSubPart.ts | 284 ++++++++++++++++++ .../chatToolInvocationPart.ts | 4 + .../chat/common/chatService/chatService.ts | 19 +- .../chatProgressTypes/chatToolInvocation.ts | 4 +- .../tools/builtinTools/confirmationTool.ts | 130 +++++++- .../chat/common/tools/builtinTools/tools.ts | 5 +- .../common/tools/languageModelToolsService.ts | 6 +- .../tools/languageModelToolsService.test.ts | 42 +++ .../modifiedFilesConfirmationTool.test.ts | 67 +++++ 14 files changed, 706 insertions(+), 17 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatModifiedFilesConfirmationSubPart.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/tools/builtinTools/modifiedFilesConfirmationTool.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts index 90397262b5d..5b9b0e488d7 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts @@ -54,6 +54,8 @@ export const getToolConfirmationAlert = (accessor: ServicesAccessor, toolInvocat input = JSON.stringify(v.toolSpecificData.extensions); } else if (v.toolSpecificData.kind === 'input') { input = JSON.stringify(v.toolSpecificData.rawInput); + } else if (v.toolSpecificData.kind === 'modifiedFilesConfirmation') { + input = localize('modifiedFilesConfirmationInput', '{0} files', v.toolSpecificData.modifiedFiles.length); } } const titleObj = state.confirmationMessages?.title; diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 6cad0431d35..f20aabab996 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -17,7 +17,7 @@ import { IStorageService, StorageScope } from '../../../../../platform/storage/c import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatExtensionsContent, IChatPullRequestContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolResourcesInvocationData, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js'; +import { IChatExtensionsContent, IChatModifiedFilesConfirmationData, IChatPullRequestContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolResourcesInvocationData, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { IToolResultInputOutputDetails, IToolResultOutputDetails, isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; @@ -59,7 +59,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation } } -type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData; +type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData | IChatModifiedFilesConfirmationData; type ResultDetails = Array | IToolResultInputOutputDetails | IToolResultOutputDetails | IToolResultOutputDetailsSerialized; export const CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY = 'chat.accessibleView.includeThinking'; @@ -141,6 +141,16 @@ export function getToolSpecificDataDescription(toolSpecificData: ToolSpecificDat const outputText = toolSpecificData.output; return localize('simpleToolInvocation', "Input: {0}, Output: {1}", inputText, outputText); } + case 'modifiedFilesConfirmation': { + if (toolSpecificData.modifiedFiles.length === 0) { + return ''; + } + + return localize('modifiedFilesConfirmation', "Modified files: {0}", toolSpecificData.modifiedFiles.map(file => { + const revivedUri = URI.revive(file.uri); + return revivedUri.fsPath || revivedUri.path; + }).join(', ')); + } default: return ''; } diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index f00d1d149b7..5deef252f00 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -70,7 +70,10 @@ const SkipAutoApproveConfirmationKey = 'vscode.chat.tools.global.autoApprove.tes // This tool will always require user confirmation even in auto approval mode. // Users cannot auto approve this tool via settings either, as this is a tool used before the agentic loop. -const toolIdThatCannotBeAutoApproved = 'vscode_get_confirmation_with_options'; +const toolIdsThatCannotBeAutoApproved = new Set([ + 'vscode_get_confirmation_with_options', + 'vscode_get_modified_files_confirmation', +]); export const globalAutoApproveDescription = localize2( { @@ -838,14 +841,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo ...prepared.confirmationMessages, title: localize('defaultToolConfirmation.title', 'Confirm tool execution'), message: localize('defaultToolConfirmation.message', 'Run the \'{0}\' tool?', fullReferenceName), - disclaimer: tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ text: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval], tooltip: localize('openSettings.autoApproval.tooltip', 'Open settings to configure auto-approval') }, false)), { isTrusted: true }), + disclaimer: toolIdsThatCannotBeAutoApproved.has(tool.data.id) ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ text: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval], tooltip: localize('openSettings.autoApproval.tooltip', 'Open settings to configure auto-approval') }, false)), { isTrusted: true }), allowAutoConfirm: false, }; } if (!isEligibleForAutoApproval && prepared?.confirmationMessages?.title) { // Always overwrite the disclaimer if not eligible for auto-approval - prepared.confirmationMessages.disclaimer = tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ text: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval], tooltip: localize('openSettings.autoApproval.tooltip', 'Open settings to configure auto-approval') }, false)), { isTrusted: true }); + prepared.confirmationMessages.disclaimer = toolIdsThatCannotBeAutoApproved.has(tool.data.id) ? undefined : new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ text: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval], tooltip: localize('openSettings.autoApproval.tooltip', 'Open settings to configure auto-approval') }, false)), { isTrusted: true }); } if (prepared?.confirmationMessages?.title) { @@ -1040,7 +1043,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Special case, this fetch will call an internal tool 'vscode_fetchWebPage_internal' return true; } - if (toolData.id === toolIdThatCannotBeAutoApproved) { + if (toolIdsThatCannotBeAutoApproved.has(toolData.id)) { // Special case, this tool will always require user confirmation as there are multiple options, // These aren't LM generated instead are generated by extension before agentic loop starts. return false; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts index b0edcc5b860..849393873dd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -56,6 +56,7 @@ export interface IChatReferenceListItem extends IChatContentReference { description?: string; state?: ModifiedFileEntryState; excluded?: boolean; + showModifiedState?: boolean; } export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; @@ -433,7 +434,7 @@ class CollapsibleListRenderer implements IListRenderer void>, + this.context, + { + title: this.getTitle(), + icon: tool?.icon && hasKey(tool.icon, { id: true }) ? tool.icon : Codicon.tools, + subtitle: typeof toolInvocation.originMessage === 'string' ? toolInvocation.originMessage : toolInvocation.originMessage?.value, + buttons: this.createButtons(data.options), + message: this.createWidgetContentElement(state.confirmationMessages.message, data), + } + )); + + const hasToolConfirmation = ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService); + hasToolConfirmation.set(true); + + this._register(confirmWidget.onDidClick(button => { + button.data(); + this.chatWidgetService.getWidgetBySessionResource(this.context.element.sessionResource)?.focusInput(); + })); + + this._register(toDisposable(() => hasToolConfirmation.reset())); + this.domNode = confirmWidget.domNode; + } + + private createButtons(options: readonly string[]): IChatConfirmationButton<() => void>[] { + const [primaryOption, ...secondaryOptions] = options; + return [ + { + label: primaryOption, + data: () => this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.UserAction, selectedButton: primaryOption }), + moreActions: secondaryOptions.map(option => ({ + label: option, + data: () => this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.UserAction, selectedButton: option }), + })) + }, + { + label: localize('cancel', 'Cancel'), + data: () => this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.Skipped }), + isSecondary: true, + } + ]; + } + + private createWidgetContentElement(message: string | IMarkdownString | undefined, data: IChatModifiedFilesConfirmationData): HTMLElement { + const container = dom.$('.chat-modified-files-confirmation'); + + if (message) { + const renderedMessage = this._register(this.markdownRendererService.render(typeof message === 'string' ? new MarkdownString(message) : message)); + container.append(renderedMessage.element); + } + + container.append(this.createModifiedFilesElement(data)); + return container; + } + + private createModifiedFilesElement(data: IChatModifiedFilesConfirmationData): HTMLElement { + const container = dom.$('.chat-modified-files-confirmation-list.chat-editing-session-container.show-file-icons'); + const overview = dom.append(container, dom.$('.chat-editing-session-overview')); + const title = dom.append(overview, dom.$('.working-set-title')); + const titleButton = this._register(new ButtonWithIcon(title, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined, + supportIcons: true, + })); + const actions = dom.append(overview, dom.$('.chat-editing-session-actions')); + const countsContainer = dom.$('.working-set-line-counts'); + const addedSpan = dom.append(countsContainer, dom.$('.working-set-lines-added')); + const removedSpan = dom.append(countsContainer, dom.$('.working-set-lines-removed')); + titleButton.element.appendChild(countsContainer); + + const filesLabel = data.modifiedFiles.length === 1 + ? localize('oneFileChanged', '1 file changed') + : localize('manyFilesChanged', '{0} files changed', data.modifiedFiles.length); + titleButton.label = filesLabel; + + let added = 0; + let removed = 0; + let hasDiffStats = false; + for (const file of data.modifiedFiles) { + if (typeof file.insertions === 'number' || typeof file.deletions === 'number') { + hasDiffStats = true; + added += file.insertions ?? 0; + removed += file.deletions ?? 0; + } + } + + if (hasDiffStats) { + addedSpan.textContent = `+${added}`; + removedSpan.textContent = `-${removed}`; + titleButton.element.setAttribute('aria-label', localize('modifiedFilesSummaryWithCounts', '{0}, {1} lines added, {2} lines removed', filesLabel, added, removed)); + countsContainer.setAttribute('aria-label', localize('modifiedFilesCounts', '{0} lines added, {1} lines removed', added, removed)); + } else { + countsContainer.remove(); + titleButton.element.setAttribute('aria-label', filesLabel); + } + + const viewAllChangesButton = this._register(new Button(actions, { + ...defaultButtonStyles, + secondary: true, + small: true, + supportIcons: true, + ariaLabel: localize('viewAllChanges', 'View All Changes'), + title: localize('viewAllChanges', 'View All Changes'), + })); + viewAllChangesButton.element.classList.add('default-colors'); + viewAllChangesButton.icon = Codicon.diffMultiple; + viewAllChangesButton.label = ' '; + this._register(viewAllChangesButton.onDidClick(async () => { + await this.openAllChanges(data); + })); + + const listReference = this._register(this.listPool.get()); + const list = listReference.object; + const listItems = data.modifiedFiles.map(file => { + const resource = URI.revive(file.uri); + const originalUri = file.originalUri ? URI.revive(file.originalUri) : undefined; + return { + kind: 'reference', + reference: resource, + title: file.title, + description: file.description, + state: ModifiedFileEntryState.Accepted, + showModifiedState: true, + options: { + diffMeta: typeof file.insertions === 'number' || typeof file.deletions === 'number' ? { + added: file.insertions ?? 0, + removed: file.deletions ?? 0, + } : undefined, + originalUri, + status: undefined, + } + }; + }); + + this._register(list.onDidOpen(async e => { + if (e.element?.kind !== 'reference' || !URI.isUri(e.element.reference)) { + return; + } + + const modifiedUri = e.element.reference; + const originalUri = e.element.options?.originalUri; + if (originalUri) { + await this.editorService.openEditor({ + original: { resource: originalUri }, + modified: { resource: modifiedUri }, + options: e.editorOptions, + }); + return; + } + + await this.editorService.openEditor({ + resource: modifiedUri, + options: e.editorOptions, + }); + })); + + const maxItemsShown = 6; + const itemsShown = Math.min(listItems.length, maxItemsShown); + const height = itemsShown * 22; + const workingSetContainer = dom.append(container, dom.$('.chat-editing-session-list.collapsed')); + list.layout(height); + list.getHTMLElement().style.height = `${height}px`; + list.splice(0, list.length, listItems); + workingSetContainer.append(list.getHTMLElement()); + + let isCollapsed = true; + const setExpansionState = () => { + titleButton.icon = isCollapsed ? Codicon.chevronRight : Codicon.chevronDown; + workingSetContainer.classList.toggle('collapsed', isCollapsed); + }; + setExpansionState(); + + const toggleWorkingSet = () => { + isCollapsed = !isCollapsed; + setExpansionState(); + }; + + this._register(titleButton.onDidClick(toggleWorkingSet)); + this._register(dom.addDisposableListener(overview, 'click', e => { + if (e.defaultPrevented) { + return; + } + + const target = e.target as HTMLElement; + if (target.closest('.monaco-button')) { + return; + } + + toggleWorkingSet(); + })); + + return container; + } + + private async openAllChanges(data: IChatModifiedFilesConfirmationData): Promise { + await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { + title: localize('modifiedFilesAllChangesTitle', 'All Changes'), + resources: data.modifiedFiles.map(file => ({ + originalUri: file.originalUri ? URI.revive(file.originalUri) : undefined, + modifiedUri: URI.revive(file.uri), + })) + }); + } + + protected createContentElement(): HTMLElement | string { + throw new Error('Not used'); + } + + protected getTitle(): string { + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return ''; + } + + const title = state.confirmationMessages?.title; + return typeof title === 'string' ? title : title?.value ?? ''; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts index 8b2f2365905..246c35d5b13 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -23,6 +23,7 @@ import { ChatInputOutputMarkdownProgressPart } from './chatInputOutputMarkdownPr import { ChatMcpAppSubPart, IMcpAppRenderData } from './chatMcpAppSubPart.js'; import { ChatResultListSubPart } from './chatResultListSubPart.js'; import { ChatSimpleToolProgressPart } from './chatSimpleToolProgressPart.js'; +import { ChatModifiedFilesConfirmationSubPart } from './chatModifiedFilesConfirmationSubPart.js'; import { ChatTerminalToolConfirmationSubPart } from './chatTerminalToolConfirmationSubPart.js'; import { ChatTerminalToolProgressPart } from './chatTerminalToolProgressPart.js'; import { ToolConfirmationSubPart } from './chatToolConfirmationSubPart.js'; @@ -120,6 +121,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa // Add class when displaying a confirmation widget const isConfirmation = this.subPart instanceof ToolConfirmationSubPart || this.subPart instanceof ChatTerminalToolConfirmationSubPart || + this.subPart instanceof ChatModifiedFilesConfirmationSubPart || this.subPart instanceof ExtensionsInstallConfirmationWidgetSubPart || this.subPart instanceof ChatToolPostExecuteConfirmationPart; this.domNode.classList.toggle('has-confirmation', isConfirmation); @@ -173,6 +175,8 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { if (this.toolInvocation.toolSpecificData?.kind === 'terminal') { return this.instantiationService.createInstance(ChatTerminalToolConfirmationSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex); + } else if (this.toolInvocation.toolSpecificData?.kind === 'modifiedFilesConfirmation') { + return this.instantiationService.createInstance(ChatModifiedFilesConfirmationSubPart, this.toolInvocation, this.context, this.listPool); } else { return this.instantiationService.createInstance(ToolConfirmationSubPart, this.toolInvocation, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex); } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 51cea27eb77..5edbc0c6595 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -558,7 +558,7 @@ export type ConfirmedReason = export interface IChatToolInvocation { readonly presentation: IPreparedToolInvocation['presentation']; - readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData; + readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData | IChatModifiedFilesConfirmationData; readonly originMessage: string | IMarkdownString | undefined; readonly invocationMessage: string | IMarkdownString; readonly pastTenseMessage: string | IMarkdownString | undefined; @@ -818,7 +818,7 @@ export interface IToolResultOutputDetailsSerialized { */ export interface IChatToolInvocationSerialized { presentation: IPreparedToolInvocation['presentation']; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData | IChatModifiedFilesConfirmationData; invocationMessage: string | IMarkdownString; originMessage: string | IMarkdownString | undefined; pastTenseMessage: string | IMarkdownString | undefined; @@ -874,7 +874,7 @@ export interface IChatExternalToolInvocationUpdate { errorMessage?: string; invocationMessage?: string | IMarkdownString; pastTenseMessage?: string | IMarkdownString; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatModifiedFilesConfirmationData; subagentInvocationId?: string; } @@ -898,6 +898,19 @@ export interface IChatToolResourcesInvocationData { readonly values: Array; } +export interface IChatModifiedFilesConfirmationData { + readonly kind: 'modifiedFilesConfirmation'; + readonly options: readonly string[]; + readonly modifiedFiles: readonly { + readonly uri: UriComponents; + readonly originalUri?: UriComponents; + readonly insertions?: number; + readonly deletions?: number; + readonly title?: string; + readonly description?: string; + }[]; +} + export interface IChatMcpServersStarting { readonly kind: 'mcpServersStarting'; readonly state?: IObservable; // not hydrated when serialized diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts index da6353f3cd5..a18f4db9d93 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -7,7 +7,7 @@ import { encodeBase64 } from '../../../../../../base/common/buffer.js'; import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IObservable, ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; import { localize } from '../../../../../../nls.js'; -import { ConfirmedReason, IChatExtensionsContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; +import { ConfirmedReason, IChatExtensionsContent, IChatModifiedFilesConfirmationData, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; import { IPreparedToolInvocation, isToolResultOutputDetails, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult, ToolDataSource } from '../../tools/languageModelToolsService.js'; export interface IStreamingToolCallOptions { @@ -33,7 +33,7 @@ export class ChatToolInvocation implements IChatToolInvocation { public generatedTitle?: string; public readonly chatRequestId?: string; - public toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData; + public toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatModifiedFilesConfirmationData; private readonly _progress = observableValue<{ message?: string | IMarkdownString; progress: number | undefined }>(this, { progress: 0 }); private readonly _state: ISettableObservable; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts index 7763b9f8b3d..8b938dfa71a 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts @@ -5,11 +5,13 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; -import { IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IChatModifiedFilesConfirmationData, IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js'; export const ConfirmationToolId = 'vscode_get_confirmation'; export const ConfirmationToolWithOptionsId = 'vscode_get_confirmation_with_options'; +export const ModifiedFilesConfirmationToolId = 'vscode_get_modified_files_confirmation'; export const ConfirmationToolData: IToolData = { id: ConfirmationToolId, @@ -69,6 +71,69 @@ export const ConfirmationToolWithOptionsData: IToolData = { } }; +export const ModifiedFilesConfirmationToolData: IToolData = { + id: ModifiedFilesConfirmationToolId, + displayName: 'Modified Files Confirmation Tool', + modelDescription: 'A tool that shows a modified-files confirmation UI with a split primary button and a hardcoded cancel action.', + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Title for the confirmation dialog' + }, + message: { + type: 'string', + description: 'Message to show in the confirmation dialog' + }, + options: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + description: 'Selectable option labels. The first option is used for the primary split button and the remaining options are placed in the dropdown menu.' + }, + modifiedFiles: { + type: 'array', + items: { + type: 'object', + properties: { + uri: { + type: 'string', + description: 'URI of the modified file.' + }, + originalUri: { + type: 'string', + description: 'Optional original URI used when opening a diff.' + }, + insertions: { + type: 'number', + description: 'Optional number of lines added.' + }, + deletions: { + type: 'number', + description: 'Optional number of lines removed.' + }, + title: { + type: 'string', + description: 'Optional title shown in the file tooltip.' + }, + description: { + type: 'string', + description: 'Optional secondary label shown for the file entry.' + } + }, + required: ['uri'], + additionalProperties: false + }, + description: 'Modified files to show in the confirmation UI.' + } + }, + required: ['title', 'message', 'options', 'modifiedFiles'], + additionalProperties: false + } +}; + export interface IConfirmationToolParams { title: string; message: string; @@ -77,6 +142,20 @@ export interface IConfirmationToolParams { buttons?: string[]; } +export interface IModifiedFilesConfirmationToolParams { + title: string; + message: string; + options: string[]; + modifiedFiles: { + uri: string; + originalUri?: string; + insertions?: number; + deletions?: number; + title?: string; + description?: string; + }[]; +} + export class ConfirmationTool implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise { const parameters = context.parameters as IConfirmationToolParams; @@ -135,3 +214,52 @@ export class ConfirmationTool implements IToolImpl { }; } } + +export class ModifiedFilesConfirmationTool implements IToolImpl { + async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise { + const parameters = context.parameters as IModifiedFilesConfirmationToolParams; + if (!parameters.title || !parameters.message) { + throw new Error('Missing required parameters for ModifiedFilesConfirmationTool'); + } + + if (!parameters.options?.length) { + throw new Error('ModifiedFilesConfirmationTool requires at least one option'); + } + + const toolSpecificData: IChatModifiedFilesConfirmationData = { + kind: 'modifiedFilesConfirmation', + options: parameters.options, + modifiedFiles: parameters.modifiedFiles.map(file => ({ + uri: URI.parse(file.uri).toJSON(), + originalUri: file.originalUri ? URI.parse(file.originalUri).toJSON() : undefined, + insertions: file.insertions, + deletions: file.deletions, + title: file.title, + description: file.description, + })), + }; + + return { + confirmationMessages: { + title: parameters.title, + message: new MarkdownString(parameters.message), + allowAutoConfirm: false, + }, + toolSpecificData, + presentation: ToolInvocationPresentation.HiddenAfterComplete + }; + } + + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise { + if (!invocation.selectedCustomButton) { + throw new Error('ModifiedFilesConfirmationTool requires a selected option'); + } + + return { + content: [{ + kind: 'text', + value: invocation.selectedCustomButton + }] + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 74f61e8b613..41144f8be91 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -8,7 +8,7 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { IWorkbenchContribution } from '../../../../../common/contributions.js'; import { ILanguageModelToolsService } from '../languageModelToolsService.js'; import { AskQuestionsTool, AskQuestionsToolData } from './askQuestionsTool.js'; -import { ConfirmationTool, ConfirmationToolData, ConfirmationToolWithOptionsData } from './confirmationTool.js'; +import { ConfirmationTool, ConfirmationToolData, ConfirmationToolWithOptionsData, ModifiedFilesConfirmationTool, ModifiedFilesConfirmationToolData } from './confirmationTool.js'; import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; import { ResolveDebugEventDetailsTool, ResolveDebugEventDetailsToolData } from './resolveDebugEventDetailsTool.js'; @@ -40,6 +40,9 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); + const modifiedFilesConfirmationTool = instantiationService.createInstance(ModifiedFilesConfirmationTool); + this._register(toolsService.registerTool(ModifiedFilesConfirmationToolData, modifiedFilesConfirmationTool)); + const taskCompleteTool = instantiationService.createInstance(TaskCompleteTool); this._register(toolsService.registerTool(TaskCompleteToolData, taskCompleteTool)); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index fe83345a4a2..ed8bc5bfebc 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -24,7 +24,7 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in import { IProgress } from '../../../../../platform/progress/common/progress.js'; import { ChatRequestToolReferenceEntry } from '../attachments/chatVariableEntries.js'; import { IVariableReference } from '../chatModes.js'; -import { IChatExtensionsContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, type IChatTerminalToolInvocationData } from '../chatService/chatService.js'; +import { IChatExtensionsContent, IChatModifiedFilesConfirmationData, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, type IChatTerminalToolInvocationData } from '../chatService/chatService.js'; import { ILanguageModelChatMetadata, LanguageModelPartAudience } from '../languageModels.js'; import { UserSelectedTools } from '../participants/chatAgents.js'; import { PromptElementJSON, stringifyPromptElementJSON } from './promptTsxTypes.js'; @@ -189,7 +189,7 @@ export interface IToolInvocation { * Lets us add some nicer UI to toolcalls that came from a sub-agent, but in the long run, this should probably just be rendered in a similar way to thinking text + tool call groups */ subAgentInvocationId?: string; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatModifiedFilesConfirmationData; modelId?: string; userSelectedTools?: UserSelectedTools; /** The label of the custom button selected by the user during confirmation, if custom buttons were used. */ @@ -363,7 +363,7 @@ export interface IPreparedToolInvocation { originMessage?: string | IMarkdownString; confirmationMessages?: IToolConfirmationMessages; presentation?: ToolInvocationPresentation; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatModifiedFilesConfirmationData; } export interface IToolImpl { diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index 87928cde0e3..4689f30c00a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -592,6 +592,48 @@ suite('LanguageModelToolsService', () => { await promise; }); + test('skipping modified-files confirmation returns the shared skip message and does not invoke the tool', async () => { + let invoked = false; + const tool = registerToolForTest(service, store, 'testModifiedFilesConfirmationSkip', { + prepareToolInvocation: async () => ({ + confirmationMessages: { + title: 'Confirm', + message: 'Choose', + allowAutoConfirm: false, + }, + toolSpecificData: { + kind: 'modifiedFilesConfirmation', + options: ['Copy Changes', 'Move Changes'], + modifiedFiles: [{ + uri: URI.parse('file:///workspace/file1.ts') + }] + } + }), + invoke: async () => { + invoked = true; + return { content: [{ kind: 'text', value: 'should not run' }] }; + }, + }); + + const sessionId = 'sessionId-modified-files-skip'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-modified-files-skip', capture }); + + const dto = tool.makeDto({ x: 1 }, { sessionId }); + const promise = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + assert.ok(published, 'expected ChatToolInvocation to be published'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.Skipped }); + const result = await promise; + + assert.strictEqual(invoked, false); + assert.deepStrictEqual(result.content, [{ + kind: 'text', + value: 'The user chose to skip the tool call, they want to proceed without running it' + }]); + }); + test('cancel tool call', async () => { const toolBarrier = new Barrier(); const tool = registerToolForTest(service, store, 'testTool', { diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/modifiedFilesConfirmationTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/modifiedFilesConfirmationTool.test.ts new file mode 100644 index 00000000000..19655689064 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/modifiedFilesConfirmationTool.test.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ModifiedFilesConfirmationTool, ModifiedFilesConfirmationToolData } from '../../../../common/tools/builtinTools/confirmationTool.js'; + +suite('ModifiedFilesConfirmationTool', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('tool data exposes the expected schema', () => { + assert.strictEqual(ModifiedFilesConfirmationToolData.id, 'vscode_get_modified_files_confirmation'); + assert.ok(ModifiedFilesConfirmationToolData.inputSchema); + assert.deepStrictEqual(ModifiedFilesConfirmationToolData.inputSchema?.required, ['title', 'message', 'options', 'modifiedFiles']); + assert.ok(ModifiedFilesConfirmationToolData.inputSchema?.properties?.options); + assert.ok(ModifiedFilesConfirmationToolData.inputSchema?.properties?.modifiedFiles); + }); + + test('prepareToolInvocation parses file data and disables auto confirm', async () => { + const tool = new ModifiedFilesConfirmationTool(); + + const result = await tool.prepareToolInvocation({ + parameters: { + title: 'Review modified files', + message: 'Choose how to continue.', + options: ['Copy Changes', 'Move Changes'], + modifiedFiles: [{ + uri: 'file:///workspace/src/file1.ts', + originalUri: 'file:///workspace/src/file1.original.ts', + insertions: 10, + deletions: 3, + title: 'File 1' + }] + }, + toolCallId: 'call-1', + chatSessionResource: URI.parse('vscode-chat://session'), + }, CancellationToken.None); + + assert.ok(result); + assert.strictEqual(result?.confirmationMessages?.allowAutoConfirm, false); + assert.strictEqual(result?.toolSpecificData?.kind, 'modifiedFilesConfirmation'); + + assert.deepStrictEqual(result.toolSpecificData.options, ['Copy Changes', 'Move Changes']); + assert.strictEqual(URI.revive(result.toolSpecificData.modifiedFiles[0].uri).toString(), 'file:///workspace/src/file1.ts'); + assert.strictEqual(result.toolSpecificData.modifiedFiles[0].originalUri ? URI.revive(result.toolSpecificData.modifiedFiles[0].originalUri).toString() : undefined, 'file:///workspace/src/file1.original.ts'); + assert.strictEqual(result.toolSpecificData.modifiedFiles[0].insertions, 10); + assert.strictEqual(result.toolSpecificData.modifiedFiles[0].deletions, 3); + }); + + test('invoke returns the selected option', async () => { + const tool = new ModifiedFilesConfirmationTool(); + + const result = await tool.invoke({ + callId: 'call-1', + toolId: 'vscode_get_modified_files_confirmation', + parameters: {}, + selectedCustomButton: 'Move Changes', + context: undefined, + }, async () => 0, { report: () => undefined }, CancellationToken.None); + + assert.deepStrictEqual(result.content, [{ kind: 'text', value: 'Move Changes' }]); + }); +}); From 2e22bb6bcdbdf76e3df2935f46c7a6ef8d147914 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Mar 2026 09:57:02 +0100 Subject: [PATCH 347/448] Optimizing _doRemoveCustomLineHeight (#299793) * optimizing commit * polishing * removing stagedInserts.length == 0 * fixing test failure --- .../editor/common/viewLayout/lineHeights.ts | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/common/viewLayout/lineHeights.ts b/src/vs/editor/common/viewLayout/lineHeights.ts index 94e81bf561d..3c5ab572d66 100644 --- a/src/vs/editor/common/viewLayout/lineHeights.ts +++ b/src/vs/editor/common/viewLayout/lineHeights.ts @@ -146,28 +146,29 @@ export class LineHeightsManager { this._hasPending = false; const stagedInserts: CustomLine[] = []; + const stagedIdMap = new ArrayMap(); for (const change of changes) { switch (change.kind) { case PendingChangeKind.Remove: - this._doRemoveCustomLineHeight(change.decorationId, stagedInserts); + this._doRemoveCustomLineHeight(change.decorationId, stagedIdMap); break; case PendingChangeKind.InsertOrChange: - this._doInsertOrChangeCustomLineHeight(change.decorationId, change.startLineNumber, change.endLineNumber, change.lineHeight, stagedInserts); + this._doInsertOrChangeCustomLineHeight(change.decorationId, change.startLineNumber, change.endLineNumber, change.lineHeight, stagedInserts, stagedIdMap); break; case PendingChangeKind.LinesDeleted: - this._flushStagedDecorationChanges(stagedInserts); + this._flushStagedDecorationChanges(stagedInserts, stagedIdMap); this._doLinesDeleted(change.fromLineNumber, change.toLineNumber); break; case PendingChangeKind.LinesInserted: - this._flushStagedDecorationChanges(stagedInserts); - this._doLinesInserted(change.fromLineNumber, change.toLineNumber, stagedInserts); + this._flushStagedDecorationChanges(stagedInserts, stagedIdMap); + this._doLinesInserted(change.fromLineNumber, change.toLineNumber, stagedInserts, stagedIdMap); break; } } - this._flushStagedDecorationChanges(stagedInserts); + this._flushStagedDecorationChanges(stagedInserts, stagedIdMap); } - private _doRemoveCustomLineHeight(decorationID: string, stagedInserts: CustomLine[]): void { + private _doRemoveCustomLineHeight(decorationID: string, stagedIdMap: ArrayMap): void { const customLines = this._decorationIDToCustomLine.get(decorationID); if (customLines) { this._decorationIDToCustomLine.delete(decorationID); @@ -176,32 +177,42 @@ export class LineHeightsManager { this._invalidIndex = Math.min(this._invalidIndex, customLine.index); } } - for (let i = stagedInserts.length - 1; i >= 0; i--) { - if (stagedInserts[i].decorationId === decorationID) { - stagedInserts.splice(i, 1); + const stagedLines = stagedIdMap.get(decorationID); + if (stagedLines) { + stagedIdMap.delete(decorationID); + for (const line of stagedLines) { + line.deleted = true; } } } - private _doInsertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number, stagedInserts: CustomLine[]): void { - this._doRemoveCustomLineHeight(decorationId, stagedInserts); + private _doInsertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number, stagedInserts: CustomLine[], stagedIdMap: ArrayMap): void { + this._doRemoveCustomLineHeight(decorationId, stagedIdMap); for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { const customLine = new CustomLine(decorationId, -1, lineNumber, lineHeight, 0); stagedInserts.push(customLine); + stagedIdMap.add(decorationId, customLine); } } - private _flushStagedDecorationChanges(stagedInserts: CustomLine[]): void { + private _flushStagedDecorationChanges(stagedInserts: CustomLine[], stagedIdMap: ArrayMap): void { if (stagedInserts.length === 0 && this._invalidIndex === Infinity) { return; } for (const pendingChange of stagedInserts) { + if (pendingChange.deleted) { + continue; + } const candidateInsertionIndex = this._binarySearchOverOrderedCustomLinesArray(pendingChange.lineNumber); const insertionIndex = candidateInsertionIndex >= 0 ? candidateInsertionIndex : -(candidateInsertionIndex + 1); this._orderedCustomLines.splice(insertionIndex, 0, pendingChange); this._invalidIndex = Math.min(this._invalidIndex, insertionIndex); } stagedInserts.length = 0; + stagedIdMap.clear(); + if (this._invalidIndex === Infinity) { + return; + } const newDecorationIDToSpecialLine = new ArrayMap(); const newOrderedSpecialLines: CustomLine[] = []; @@ -358,7 +369,7 @@ export class LineHeightsManager { } } - private _doLinesInserted(fromLineNumber: number, toLineNumber: number, stagedInserts: CustomLine[]): void { + private _doLinesInserted(fromLineNumber: number, toLineNumber: number, stagedInserts: CustomLine[], stagedIdMap: ArrayMap): void { const insertCount = toLineNumber - fromLineNumber + 1; const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); let startIndexOfInsertion: number; @@ -411,7 +422,7 @@ export class LineHeightsManager { } for (const dec of toReAdd) { - this._doInsertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight, stagedInserts); + this._doInsertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight, stagedInserts, stagedIdMap); } } } @@ -475,4 +486,8 @@ class ArrayMap { delete(key: K): void { this._map.delete(key); } + + clear(): void { + this._map.clear(); + } } From 1f3de37bc2e9b21e710470e128632c544e5aea32 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:16:19 +0100 Subject: [PATCH 348/448] Sessions - use worktree branch name in the files view (#300133) * Sessions - use worktree branch name in the files view * Pull request feedback --- .../browser/sessionsManagementService.ts | 20 +++++++++++++------ .../browser/workspaceFolderManagement.ts | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index f2a5bd0e28e..46f9e56ce0f 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -50,6 +50,7 @@ export interface IActiveSessionItem { readonly label: string | undefined; readonly repository: URI | undefined; readonly worktree: URI | undefined; + readonly worktreeBranchName: string | undefined; readonly providerType: string; } @@ -196,10 +197,10 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } - private getRepositoryFromMetadata(session: IAgentSession): [URI | undefined, URI | undefined] { + private getRepositoryFromMetadata(session: IAgentSession): [URI | undefined, URI | undefined, string | undefined] { const metadata = session.metadata; if (!metadata) { - return [undefined, undefined]; + return [undefined, undefined, undefined]; } if (session.providerType === AgentSessionProviders.Cloud) { @@ -210,12 +211,12 @@ export class SessionsManagementService extends Disposable implements ISessionsMa authority: 'github', path: `/${metadata.owner}/${metadata.name}/${encodeURIComponent(branch)}` }); - return [repositoryUri, undefined]; + return [repositoryUri, undefined, undefined]; } const workingDirectoryPath = metadata?.workingDirectoryPath as string | undefined; if (workingDirectoryPath) { - return [URI.file(workingDirectoryPath), undefined]; + return [URI.file(workingDirectoryPath), undefined, undefined]; } const repositoryPath = metadata?.repositoryPath as string | undefined; @@ -224,9 +225,12 @@ export class SessionsManagementService extends Disposable implements ISessionsMa const worktreePath = metadata?.worktreePath as string | undefined; const worktreePathUri = typeof worktreePath === 'string' ? URI.file(worktreePath) : undefined; + const worktreeBranchName = metadata?.branchName as string | undefined; + return [ URI.isUri(repositoryPathUri) ? repositoryPathUri : undefined, - URI.isUri(worktreePathUri) ? worktreePathUri : undefined]; + URI.isUri(worktreePathUri) ? worktreePathUri : undefined, + worktreeBranchName]; } private getRepositoryFromSessionOption(sessionResource: URI): URI | undefined { @@ -431,13 +435,14 @@ export class SessionsManagementService extends Disposable implements ISessionsMa if (session) { if (isAgentSession(session)) { this.lastSelectedSession = session.resource; - const [repository, worktree] = this.getRepositoryFromMetadata(session); + const [repository, worktree, worktreeBranchName] = this.getRepositoryFromMetadata(session); activeSessionItem = { isUntitled: this.chatService.getSession(session.resource)?.contributedChatSession?.isUntitled ?? true, label: session.label, resource: session.resource, repository, worktree, + worktreeBranchName, providerType: session.providerType, }; } else { @@ -447,6 +452,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa resource: session.resource, repository: session.repoUri, worktree: undefined, + worktreeBranchName: undefined, providerType: session.target, }; this._newActiveSessionDisposables.clear(); @@ -458,6 +464,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa resource: session.resource, repository: session.repoUri, worktree: undefined, + worktreeBranchName: undefined, providerType: session.target, }); } @@ -496,6 +503,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa a.resource.toString() === b.resource.toString() && a.repository?.toString() === b.repository?.toString() && a.worktree?.toString() === b.worktree?.toString() && + a.worktreeBranchName === b.worktreeBranchName && a.providerType === b.providerType ); } diff --git a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts index 1bd80b586c7..15104c8280e 100644 --- a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts +++ b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts @@ -68,7 +68,7 @@ export class WorkspaceFolderManagementContribution extends Disposable implements if (session.worktree) { return { uri: session.worktree, - name: session.repository ? `${this.uriIdentityService.extUri.basename(session.repository)} (${this.uriIdentityService.extUri.basename(session.worktree)})` : this.uriIdentityService.extUri.basename(session.worktree) + name: session.repository ? `${this.uriIdentityService.extUri.basename(session.repository)} (${session.worktreeBranchName ?? this.uriIdentityService.extUri.basename(session.worktree)})` : this.uriIdentityService.extUri.basename(session.worktree) }; } From acd63e770bdedb3ddbc7e73e5d4b3f5da4903007 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Mon, 9 Mar 2026 02:42:55 -0700 Subject: [PATCH 349/448] Fix handoff widget visibility: derive from response mode, persist modeInfo (#298867) * Fix handoff widget visibility: derive from response mode, persist modeInfo, fix lifecycle * Fix handoff widget response mode logic * Make findModeByName search _customModeInstances directly like findModeById does, bypassing the getCustomModes() gate * Fix tests * Fix custom mode lookup to avoid builtin name collisions * address review: use findModeByName consistently, respect getCustomModes guard --- .../contrib/chat/browser/widget/chatWidget.ts | 35 ++++++++----- .../contrib/chat/common/model/chatModel.ts | 3 ++ .../common/model/chatSessionOperationLog.ts | 1 + .../ChatService_can_deserialize.0.snap | 1 + ...rvice_can_deserialize_with_response.0.snap | 1 + .../ChatService_can_serialize.1.snap | 2 + .../ChatService_sendRequest_fails.0.snap | 1 + .../chat/test/common/model/chatModel.test.ts | 50 ++++++++++++++++++- 8 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 266efc7b245..0cae54e9130 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -53,7 +53,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; import { IChatLayoutService } from '../../common/widget/chatLayoutService.js'; import { IChatModel, IChatModelInputState, IChatResponseModel } from '../../common/model/chatModel.js'; -import { ChatMode, getModeNameForTelemetry, IChatModeService } from '../../common/chatModes.js'; +import { ChatMode, getModeNameForTelemetry, IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js'; import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../attachments/chatVariables.js'; @@ -1182,27 +1182,34 @@ export class ChatWidget extends Disposable implements IChatWidget { const lastItem = items[items.length - 1]; const lastResponseComplete = lastItem && isResponseVM(lastItem) && lastItem.isComplete; - if (!lastResponseComplete) { + if (!lastResponseComplete || lastItem.isCanceled) { + this.chatSuggestNextWidget.hide(); return; } - // Get the currently selected mode directly from the observable - // Note: We use currentModeObs instead of currentModeKind because currentModeKind returns - // the ChatModeKind enum (e.g., 'agent'), which doesn't distinguish between custom modes. - // Custom modes all have kind='agent' but different IDs. - const currentMode = this.input.currentModeObs.get(); - const handoffs = currentMode?.handOffs?.get(); - // Only show if: mode has handoffs AND chat has content AND not quick chat - const shouldShow = currentMode && handoffs && handoffs.length > 0; + // Derive handoffs from the mode that generated the last response, not the current UI selection. + // This ensures handoffs reflect what the response agent offers, regardless of mode picker state. + // Fall back to the current mode picker for old sessions where modeInfo was not persisted. + const modeInfo = lastItem.model.request?.modeInfo; + let responseMode: IChatMode | undefined; + if (modeInfo?.modeInstructions?.name) { + responseMode = this.chatModeService.findModeByName(modeInfo.modeInstructions.name); + } else if (modeInfo?.modeId) { + responseMode = this.chatModeService.findModeById(modeInfo.modeId); + } else { + responseMode = this.input.currentModeObs.get(); + } - if (shouldShow) { + const handoffs = responseMode?.handOffs?.get(); + + if (responseMode && handoffs && handoffs.length > 0) { // Log telemetry only when widget transitions from hidden to visible const wasHidden = this.chatSuggestNextWidget.domNode.style.display === 'none'; - this.chatSuggestNextWidget.render(currentMode); + this.chatSuggestNextWidget.render(responseMode); if (wasHidden) { this.telemetryService.publicLog2('chat.handoffWidgetShown', { - agent: getModeNameForTelemetry(currentMode), + agent: getModeNameForTelemetry(responseMode), handoffCount: handoffs.length }); } @@ -2006,6 +2013,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (e.kind === 'addRequest') { this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false); this._sessionIsEmptyContextKey.set(false); + this.chatSuggestNextWidget.hide(); } // Hide widget on request removal if (e.kind === 'removeRequest') { @@ -2036,6 +2044,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.listWidget.scrollToEnd(); } + this.renderChatSuggestNextWidget(); this.updateChatInputContext(); this.input.renderChatTodoListWidget(this.viewModel.sessionResource); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index b7d2f749317..9e9b63d7861 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -1449,6 +1449,7 @@ export interface ISerializableChatRequestData extends ISerializableChatResponseD confirmation?: string; editedFileEvents?: IChatAgentEditedFileEvent[]; modelId?: string; + modeInfo?: IChatRequestModeInfo; } export interface ISerializableMarkdownInfo { @@ -2315,6 +2316,7 @@ export class ChatModel extends Disposable implements IChatModel { confirmation: raw.confirmation, editedFileEvents: raw.editedFileEvents, modelId: raw.modelId, + modeInfo: raw.modeInfo, }); request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; // eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts @@ -2657,6 +2659,7 @@ export class ChatModel extends Disposable implements IChatModel { confirmation: r.confirmation, editedFileEvents: r.editedFileEvents, modelId: r.modelId, + modeInfo: r.modeInfo, ...r.response?.toJSON(), }; }), diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index 767cb087be6..c6da97a55d2 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -148,6 +148,7 @@ const requestSchema = Adapt.object m.response?.contentReferences, objectsEqual), codeCitations: Adapt.v(m => m.response?.codeCitations, objectsEqual), timeSpentWaiting: Adapt.v(m => m.response?.timestamp), // based on response timestamp + modeInfo: Adapt.v(m => m.modeInfo, objectsEqual), }, { sealed: (o) => o.modelState?.value === ResponseModelState.Cancelled || o.modelState?.value === ResponseModelState.Failed || o.modelState?.value === ResponseModelState.Complete, }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap index 643e3727914..374c04da997 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap @@ -75,6 +75,7 @@ confirmation: undefined, editedFileEvents: undefined, modelId: undefined, + modeInfo: undefined, responseId: undefined, result: { metadata: { metadataKey: "value" } }, responseMarkdownInfo: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap index f0e423320dd..3119761173f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -75,6 +75,7 @@ confirmation: undefined, editedFileEvents: undefined, modelId: undefined, + modeInfo: undefined, responseId: undefined, result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, responseMarkdownInfo: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap index 3e5c32aa740..a3d39df663c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap @@ -77,6 +77,7 @@ confirmation: undefined, editedFileEvents: undefined, modelId: undefined, + modeInfo: undefined, responseId: undefined, result: { metadata: { metadataKey: "value" } }, responseMarkdownInfo: undefined, @@ -162,6 +163,7 @@ confirmation: undefined, editedFileEvents: undefined, modelId: undefined, + modeInfo: undefined, responseId: undefined, result: { }, responseMarkdownInfo: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap index 9a7073212a2..5fc5d409eb3 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -77,6 +77,7 @@ confirmation: undefined, editedFileEvents: undefined, modelId: undefined, + modeInfo: undefined, responseId: undefined, result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, responseMarkdownInfo: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts index c0800d4b46d..e89b504b224 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -25,10 +25,10 @@ import { TestExtensionService, TestStorageService } from '../../../../../test/co import { CellUri } from '../../../../notebook/common/notebookCommon.js'; import { IChatRequestImplicitVariableEntry, IChatRequestStringVariableEntry, IChatRequestFileEntry, StringChatContextValue } from '../../../common/attachments/chatVariableEntries.js'; import { ChatAgentService, IChatAgentService } from '../../../common/participants/chatAgents.js'; -import { ChatModel, ChatRequestModel, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response } from '../../../common/model/chatModel.js'; +import { ChatModel, ChatRequestModel, IChatRequestModeInfo, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response } from '../../../common/model/chatModel.js'; import { ChatRequestTextPart } from '../../../common/requestParser/chatParserTypes.js'; import { ChatRequestQueueKind, IChatService, IChatToolInvocation } from '../../../common/chatService/chatService.js'; -import { ChatAgentLocation } from '../../../common/constants.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; import { MockChatService } from '../chatService/mockChatService.js'; suite('ChatModel', () => { @@ -274,6 +274,52 @@ suite('ChatModel', () => { // Should keep file attachments and implicit attachments with URI values assert.deepStrictEqual(serialized.attachments, [fileAttachment, implicitWithUri]); }); + + test('modeInfo roundtrips through serialization', async () => { + const modeInfo: IChatRequestModeInfo = { + kind: ChatModeKind.Agent, + isBuiltin: false, + modeId: 'custom', + modeInstructions: { + name: 'plan', + content: 'You are a planning agent', + toolReferences: [], + }, + applyCodeBlockSuggestionId: undefined, + }; + + const serializableData: ISerializableChatData3 = { + version: 3, + sessionId: 'test-modeinfo-session', + creationDate: Date.now(), + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + responderUsername: 'bot', + requests: [{ + requestId: 'req1', + message: { text: 'plan something', parts: [] }, + variableData: { variables: [] }, + response: [{ value: 'Here is my plan', isTrusted: false }], + modelState: { value: 1 /* ResponseModelState.Complete */, completedAt: Date.now() }, + modeInfo, + }], + }; + + const model = testDisposables.add(instantiationService.createInstance( + ChatModel, + { value: serializableData, serializer: undefined! }, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + + const requests = model.getRequests(); + assert.strictEqual(requests.length, 1); + assert.deepStrictEqual(requests[0].modeInfo, modeInfo); + + // Verify roundtrip through toExport + const exported = model.toExport(); + assert.strictEqual(exported.requests.length, 1); + assert.deepStrictEqual(exported.requests[0].modeInfo, modeInfo); + }); }); suite('Response', () => { From ed27cb432e9bf0cf96e4e4922cade7fce97acbc3 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 9 Mar 2026 12:56:37 +0100 Subject: [PATCH 350/448] Chat: Editing a SKILL.md file won't surface it as automatic context in chat (#300141) --- .../chatAttachmentResolveService.ts | 23 +------------------ .../attachments/chatImplicitContext.ts | 5 +--- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentResolveService.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentResolveService.ts index e5fb7e719a9..f57fcb71541 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentResolveService.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentResolveService.ts @@ -10,7 +10,6 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IRange } from '../../../../../editor/common/core/range.js'; import { SymbolKinds } from '../../../../../editor/common/languages.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IDraggedResourceEditorInput, MarkerTransferData, DocumentSymbolTransferData, NotebookCellOutputTransferData } from '../../../../../platform/dnd/browser/dnd.js'; @@ -27,8 +26,7 @@ import { getOutputViewModelFromId } from '../../../notebook/browser/controller/c import { getNotebookEditorFromEditorPane } from '../../../notebook/browser/notebookBrowser.js'; import { SCMHistoryItemTransferData } from '../../../scm/browser/scmHistoryChatContext.js'; import { CHAT_ATTACHABLE_IMAGE_MIME_TYPES, getAttachableImageExtension } from '../../common/model/chatModel.js'; -import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, toPromptFileVariableEntry, PromptFileVariableKind, ISCMHistoryItemVariableEntry } from '../../common/attachments/chatVariableEntries.js'; -import { getPromptsTypeForLanguageId, PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, ISCMHistoryItemVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { imageToHash } from '../widget/input/editor/chatPasteProviders.js'; import { resizeImage } from '../chatImageUtils.js'; @@ -56,7 +54,6 @@ export class ChatAttachmentResolveService implements IChatAttachmentResolveServi constructor( @IFileService private fileService: IFileService, @IEditorService private editorService: IEditorService, - @ITextModelService private textModelService: ITextModelService, @IExtensionService private extensionService: IExtensionService, @IDialogService private dialogService: IDialogService ) { } @@ -115,27 +112,9 @@ export class ChatAttachmentResolveService implements IChatAttachmentResolveServi let omittedState = OmittedState.NotOmitted; if (!isDirectory) { - - let languageId: string | undefined; - try { - const createdModel = await this.textModelService.createModelReference(resource); - languageId = createdModel.object.getLanguageId(); - createdModel.dispose(); - } catch { - omittedState = OmittedState.Full; - } - if (/\.(svg)$/i.test(resource.path)) { omittedState = OmittedState.Full; } - if (languageId) { - const promptsType = getPromptsTypeForLanguageId(languageId); - if (promptsType === PromptsType.prompt) { - return toPromptFileVariableEntry(resource, PromptFileVariableKind.PromptFile); - } else if (promptsType === PromptsType.instructions) { - return toPromptFileVariableEntry(resource, PromptFileVariableKind.Instruction); - } - } } return { diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts index 111f7d09b7c..5846de6f5ca 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts @@ -26,7 +26,6 @@ import { IChatService } from '../../common/chatService/chatService.js'; import { IChatRequestImplicitVariableEntry, IChatRequestVariableEntry, isStringImplicitContextValue, StringChatContextValue } from '../../common/attachments/chatVariableEntries.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; -import { getPromptsTypeForLanguageId } from '../../common/promptSyntax/promptTypes.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { IChatContextService } from '../contextContrib/chatContextService.js'; import { ITextModel } from '../../../../../editor/common/model.js'; @@ -258,8 +257,6 @@ export class ChatImplicitContextContribution extends Disposable implements IWork return; } - const isPromptFile = languageId && getPromptsTypeForLanguageId(languageId) !== undefined; - const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.EditorInline)]; for (const widget of widgets) { if (!widget.input.implicitContext) { @@ -267,7 +264,7 @@ export class ChatImplicitContextContribution extends Disposable implements IWork } const setting = this._implicitContextEnablement[widget.location]; const isFirstInteraction = widget.viewModel?.getItems().length === 0; - if ((setting === 'always' || setting === 'first' && isFirstInteraction) && !isPromptFile) { // disable implicit context for prompt files + if ((setting === 'always' || setting === 'first' && isFirstInteraction)) { // When there's a non-code active editor (e.g. Settings is open), preserve // existing values so the attachment bar stays visible. // But when there's no active editor at all, clear the values. From 75be16739a17803c212e5c95880053af421b53bd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Mar 2026 13:10:19 +0100 Subject: [PATCH 351/448] Send workspace data by default to ext host in sessions app * Remove unused IConfigurationService references from workspaceContextService * Refactor configuration service creation in SessionsMain to streamline workspace context handling --- .../electron-browser/sessions.main.ts | 28 +++++++++---------- .../browser/workspaceContextService.ts | 9 +----- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/vs/sessions/electron-browser/sessions.main.ts b/src/vs/sessions/electron-browser/sessions.main.ts index 2dc3a764678..6cfdffb6692 100644 --- a/src/vs/sessions/electron-browser/sessions.main.ts +++ b/src/vs/sessions/electron-browser/sessions.main.ts @@ -15,7 +15,7 @@ import { INativeWorkbenchEnvironmentService, NativeWorkbenchEnvironmentService } import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js'; import { ILoggerService, ILogService, LogLevel } from '../../platform/log/common/log.js'; import { NativeWorkbenchStorageService } from '../../workbench/services/storage/electron-browser/storageService.js'; -import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier, IWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; +import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier } from '../../platform/workspace/common/workspace.js'; import { IWorkbenchConfigurationService } from '../../workbench/services/configuration/common/configuration.js'; import { IStorageService } from '../../platform/storage/common/storage.js'; import { Disposable } from '../../base/common/lifecycle.js'; @@ -293,17 +293,19 @@ export class SessionsMain extends Disposable { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! const workspaceIdentifier = getWorkspaceIdentifier(uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(userDataProfilesService.profilesHome), 'agent-sessions.code-workspace')); + const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService); - const [{ configurationService, workspaceContextService }, storageService] = await Promise.all([ - this.createWorkspaceAndConfigurationService(workspaceIdentifier, userDataProfileService, uriIdentityService, fileService, logService, policyService).then(services => { + // Workspace + serviceCollection.set(IWorkspaceContextService, workspaceContextService); + serviceCollection.set(IWorkspaceEditingService, workspaceContextService); + + const [configurationService, storageService] = await Promise.all([ + this.createConfigurationService(workspaceContextService, userDataProfileService, uriIdentityService, fileService, logService, policyService).then(configurationService => { // Configuration - serviceCollection.set(IWorkbenchConfigurationService, services.configurationService); - // Workspace - serviceCollection.set(IWorkspaceContextService, services.workspaceContextService); - serviceCollection.set(IWorkspaceEditingService, services.workspaceContextService); + serviceCollection.set(IWorkbenchConfigurationService, configurationService); - return services; + return configurationService; }), this.createStorageService(workspaceIdentifier, environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => { @@ -344,15 +346,14 @@ export class SessionsMain extends Disposable { return { serviceCollection, logService, storageService, configurationService }; } - private async createWorkspaceAndConfigurationService( - workspaceIdentifier: IWorkspaceIdentifier, + private async createConfigurationService( + workspaceContextService: SessionsWorkspaceContextService, userDataProfileService: IUserDataProfileService, uriIdentityService: IUriIdentityService, fileService: FileService, logService: ILogService, policyService: IPolicyService - ): Promise<{ configurationService: ConfigurationService; workspaceContextService: SessionsWorkspaceContextService }> { - const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService); + ): Promise { const configurationService = new ConfigurationService(userDataProfileService, workspaceContextService, uriIdentityService, fileService, policyService, logService); try { await configurationService.initialize(); @@ -360,8 +361,7 @@ export class SessionsMain extends Disposable { onUnexpectedError(error); } - workspaceContextService.setConfigurationService(configurationService); - return { configurationService, workspaceContextService }; + return configurationService; } private async createStorageService(workspace: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, mainProcessService: IMainProcessService): Promise { diff --git a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts index dce935e3f24..0e2aab0e8fc 100644 --- a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts +++ b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts @@ -13,8 +13,6 @@ import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/co import { getWorkspaceIdentifier } from '../../../../workbench/services/workspaces/browser/workspaces.js'; import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; - export class SessionsWorkspaceContextService extends Disposable implements IWorkspaceContextService, IWorkspaceEditingService { declare readonly _serviceBrand: undefined; @@ -52,13 +50,8 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork return WorkbenchState.WORKSPACE; } - private _configurationService: IConfigurationService | undefined; - setConfigurationService(configurationService: IConfigurationService) { - this._configurationService = configurationService; - } - hasWorkspaceData(): boolean { - return this._configurationService?.getValue('sessions.workspace.sendWorkspaceDataToExtHost') === true; + return true; } getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { From 8c6775810b66b76f0bbc6eda5af82a7402045dc2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 9 Mar 2026 15:45:07 +0100 Subject: [PATCH 352/448] style - reduce padding in `interactive-item-container` --- src/vs/sessions/browser/media/style.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index c4c659e4af9..0a57fa73f7d 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -54,16 +54,16 @@ .agent-sessions-workbench .interactive-session .interactive-item-container { max-width: 950px; margin: 0 auto; - padding-left: 12px; - padding-right: 12px; + padding-left: 8px; + padding-right: 8px; box-sizing: border-box; } .agent-sessions-workbench .interactive-session > .chat-suggest-next-widget { max-width: 950px; margin: 0 auto; - padding-left: 12px; - padding-right: 12px; + padding-left: 8px; + padding-right: 8px; box-sizing: border-box; } @@ -78,7 +78,7 @@ max-width: 950px; margin: 0 auto !important; display: inherit !important; - /* Align with changes view */ - padding: 4px 12px 6px 12px !important; + /* Align with panel (terminal) card margin */ + padding: 4px 8px 6px 8px !important; box-sizing: border-box; } From 4a4e6624c3c0aa57f56d5c6859fdfd0ecd14cb7b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Mar 2026 16:06:15 +0100 Subject: [PATCH 353/448] Show manage models action in the search input (#300165) * Add filter actions to ActionList and manage models option in chat model picker * Refactor model picker to support additional entries and improve manage models action handling * Refactor model picker to conditionally add separator for additional entries and streamline manage models action handling * Improve manage models action visibility logic in ModelPickerWidget * Refactor model picker to streamline additional entry handling and improve action management * Refactor model picker to integrate manage models action and streamline additional entry handling --- .../actionWidget/browser/actionList.ts | 15 +++++- .../actionWidget/browser/actionWidget.css | 18 ++++++- .../browser/widget/input/chatModelPicker.ts | 54 +++++++++++-------- .../widget/input/chatModelPicker.test.ts | 26 ++++----- 4 files changed, 77 insertions(+), 36 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 71f8ee665a2..30aa721ddd5 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -359,6 +359,11 @@ export interface IActionListOptions { */ readonly filterPlaceholder?: string; + /** + * Optional actions shown in the filter row, to the right of the input. + */ + readonly filterActions?: readonly IAction[]; + /** * Section IDs that should be collapsed by default. */ @@ -516,13 +521,21 @@ export class ActionList extends Disposable { if (this._options?.showFilter) { this._filterContainer = document.createElement('div'); this._filterContainer.className = 'action-list-filter'; + const filterRow = dom.append(this._filterContainer, dom.$('.action-list-filter-row')); this._filterInput = document.createElement('input'); this._filterInput.type = 'text'; this._filterInput.className = 'action-list-filter-input'; this._filterInput.placeholder = this._options?.filterPlaceholder ?? localize('actionList.filter.placeholder', "Search..."); this._filterInput.setAttribute('aria-label', localize('actionList.filter.ariaLabel', "Filter items")); - this._filterContainer.appendChild(this._filterInput); + filterRow.appendChild(this._filterInput); + + const filterActions = this._options?.filterActions ?? []; + if (filterActions.length > 0) { + const filterActionsContainer = dom.append(filterRow, dom.$('.action-list-filter-actions')); + const filterActionBar = this._register(new ActionBar(filterActionsContainer)); + filterActionBar.push(filterActions, { icon: true, label: false }); + } this._register(dom.addDisposableListener(this._filterInput, 'input', () => { this._filterText = this._filterInput!.value; diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 31c753580b2..8957a85b01f 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -265,7 +265,13 @@ /* Filter input */ .action-widget .action-list-filter { - padding: 2px 2px 4px 2px + padding: 2px 2px 4px 2px; +} + +.action-widget .action-list-filter-row { + display: flex; + align-items: center; + gap: 4px; } .action-widget .action-list-filter:first-child { @@ -278,6 +284,7 @@ .action-widget .action-list-filter-input { width: 100%; + flex: 1; box-sizing: border-box; padding: 4px 8px; border: 1px solid var(--vscode-input-border, transparent); @@ -294,3 +301,12 @@ .action-widget .action-list-filter-input::placeholder { color: var(--vscode-input-placeholderForeground); } + +.action-widget .action-list-filter-actions .action-label { + padding: 3px; + border-radius: 3px; +} + +.action-widget .action-list-filter-actions .action-label:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index ce390754ad2..2c5f04d1e39 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -109,6 +109,27 @@ function createModelAction( }; } +function shouldShowManageModelsAction(chatEntitlementService: IChatEntitlementService): boolean { + return chatEntitlementService.entitlement === ChatEntitlement.Free || + chatEntitlementService.entitlement === ChatEntitlement.Pro || + chatEntitlementService.entitlement === ChatEntitlement.ProPlus || + chatEntitlementService.entitlement === ChatEntitlement.Business || + chatEntitlementService.entitlement === ChatEntitlement.Enterprise || + chatEntitlementService.isInternal; +} + +function createManageModelsAction(commandService: ICommandService): IActionWidgetDropdownAction { + return { + id: 'manageModels', + enabled: true, + checked: false, + class: ThemeIcon.asClassName(Codicon.gear), + tooltip: localize('chat.manageModels.tooltip', "Manage Language Models"), + label: localize('chat.manageModels', "Manage Models..."), + run: () => { commandService.executeCommand(MANAGE_CHAT_COMMAND_ID); } + }; +} + /** * Builds the grouped items for the model picker dropdown. * @@ -118,7 +139,7 @@ function createModelAction( * - Available models sorted alphabetically, followed by unavailable models * - Unavailable models show upgrade/update/admin status * 3. Other Models (collapsible toggle, available first, then sorted by vendor then name) - * - Last item is "Manage Models..." (always visible during filtering) + * 4. Optional "Manage Models..." action shown in Other Models after a separator */ export function buildModelPickerItems( models: ILanguageModelChatMetadataAndIdentifier[], @@ -130,7 +151,7 @@ export function buildModelPickerItems( onSelect: (model: ILanguageModelChatMetadataAndIdentifier) => void, manageSettingsUrl: string | undefined, canManageModels: boolean, - commandService: ICommandService, + manageModelsAction: IActionWidgetDropdownAction | undefined, chatEntitlementService: IChatEntitlementService, ): IActionListItem[] { const items: IActionListItem[] = []; @@ -340,27 +361,12 @@ export function buildModelPickerItems( } } - if ( - chatEntitlementService.entitlement === ChatEntitlement.Free || - chatEntitlementService.entitlement === ChatEntitlement.Pro || - chatEntitlementService.entitlement === ChatEntitlement.ProPlus || - chatEntitlementService.entitlement === ChatEntitlement.Business || - chatEntitlementService.entitlement === ChatEntitlement.Enterprise || - chatEntitlementService.isInternal - ) { + if (manageModelsAction) { items.push({ kind: ActionListItemKind.Separator, section: otherModels.length ? ModelPickerSection.Other : undefined }); items.push({ - item: { - id: 'manageModels', - enabled: true, - checked: false, - class: undefined, - tooltip: localize('chat.manageModels.tooltip', "Manage Language Models"), - label: localize('chat.manageModels', "Manage Models..."), - run: () => { commandService.executeCommand(MANAGE_CHAT_COMMAND_ID); } - }, + item: manageModelsAction, kind: ActionListItemKind.Action, - label: localize('chat.manageModels', "Manage Models..."), + label: manageModelsAction.label, group: { title: '', icon: Codicon.blank }, hideIcon: false, section: otherModels.length ? ModelPickerSection.Other : undefined, @@ -562,9 +568,12 @@ export class ModelPickerWidget extends Disposable { }; const models = this._delegate.getModels(); + const showFilter = models.length >= 10; const isPro = isProUser(this._entitlementService.entitlement); const manifest = this._languageModelsService.getModelsControlManifest(); const controlModelsForTier = isPro ? manifest.paid : manifest.free; + const canShowManageModelsAction = this._delegate.canManageModels() && shouldShowManageModelsAction(this._entitlementService); + const manageModelsAction = canShowManageModelsAction ? createManageModelsAction(this._commandService) : undefined; const items = buildModelPickerItems( models, this._selectedModel?.identifier, @@ -575,13 +584,14 @@ export class ModelPickerWidget extends Disposable { onSelect, this._productService.defaultChatAgent?.manageSettingsUrl, this._delegate.canManageModels(), - this._commandService, + !showFilter ? manageModelsAction : undefined, this._entitlementService, ); const listOptions = { - showFilter: models.length >= 10, + showFilter, filterPlaceholder: localize('chat.modelPicker.search', "Search models"), + filterActions: showFilter && manageModelsAction ? [manageModelsAction] : undefined, focusFilterOnOpen: true, collapsedByDefault: new Set([ModelPickerSection.Other]), minWidth: 200, diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index d74122c7f61..11be03e3f31 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -10,7 +10,6 @@ import { IStringDictionary } from '../../../../../../../base/common/collections. import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { ActionListItemKind, IActionListItem } from '../../../../../../../platform/actionWidget/browser/actionList.js'; import { IActionWidgetDropdownAction } from '../../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; -import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; import { StateType } from '../../../../../../../platform/update/common/update.js'; import { buildModelPickerItems, getModelPickerAccessibilityProvider } from '../../../../browser/widget/input/chatModelPicker.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, IModelControlEntry } from '../../../../common/languageModels.js'; @@ -48,13 +47,6 @@ function createAutoModel(): ILanguageModelChatMetadataAndIdentifier { return createModel('auto', 'Auto', 'copilot'); } -const stubCommandService: ICommandService = { - _serviceBrand: undefined, - onWillExecuteCommand: () => ({ dispose() { } }), - onDidExecuteCommand: () => ({ dispose() { } }), - executeCommand: () => Promise.resolve(undefined), -}; - function getActionItems(items: IActionListItem[]): IActionListItem[] { return items.filter(i => i.kind === ActionListItemKind.Action); } @@ -67,6 +59,16 @@ function getSeparatorCount(items: IActionListItem[] return items.filter(i => i.kind === ActionListItemKind.Separator).length; } +const stubManageModelsAction: IActionWidgetDropdownAction = { + id: 'manageModels', + enabled: true, + checked: false, + class: undefined, + tooltip: 'Manage Language Models', + label: 'Manage Models...', + run: () => { } +}; + function callBuild( models: ILanguageModelChatMetadataAndIdentifier[], opts: { @@ -95,7 +97,7 @@ function callBuild( onSelect, opts.manageSettingsUrl, true, - stubCommandService, + stubManageModelsAction, entitlementService, ); } @@ -470,7 +472,7 @@ suite('buildModelPickerItems', () => { onSelect, undefined, true, - stubCommandService, + undefined, stubChatEntitlementService, ); const gptItem = getActionItems(items).find(a => a.label === 'GPT-4o'); @@ -552,7 +554,7 @@ suite('buildModelPickerItems', () => { () => { }, 'https://aka.ms/github-copilot-settings', true, - stubCommandService, + undefined, stubChatEntitlementService, ); @@ -635,7 +637,7 @@ suite('buildModelPickerItems', () => { onSelect, undefined, true, - stubCommandService, + undefined, anonymousEntitlementService, ); const gptItem = getActionItems(items).find(a => a.label === 'GPT-4o'); From 13a604e50cccb2ed1631c533ea4ea46f4253b239 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:10:36 -0700 Subject: [PATCH 354/448] Revert "Revert "Port github extension to use esbuild" (#298920)" This reverts commit 51f5cafd6f41af7652eece368ee64dd68bb49310. --- extensions/esbuild-extension-common.mts | 4 +- extensions/github/.vscodeignore | 2 +- ...xtension.webpack.config.js => esbuild.mts} | 30 +++--- extensions/github/package.json | 3 +- extensions/github/src/branchProtection.ts | 2 +- extensions/github/src/canonicalUriProvider.ts | 2 +- extensions/github/src/commands.ts | 3 +- extensions/github/src/credentialProvider.ts | 2 +- extensions/github/src/extension.ts | 2 +- .../github/src/historyItemDetailsProvider.ts | 2 +- extensions/github/src/links.ts | 3 +- extensions/github/src/publish.ts | 2 +- extensions/github/src/pushErrorHandler.ts | 3 +- .../github/src/remoteSourcePublisher.ts | 2 +- extensions/github/src/shareProviders.ts | 2 +- .../github/src/typings/git.constants.ts | 98 +++++++++++++++++++ extensions/github/src/util.ts | 2 +- 17 files changed, 130 insertions(+), 34 deletions(-) rename extensions/github/{extension.webpack.config.js => esbuild.mts} (50%) create mode 100644 extensions/github/src/typings/git.constants.ts diff --git a/extensions/esbuild-extension-common.mts b/extensions/esbuild-extension-common.mts index 1c458e4bfe1..cc716f2ca6a 100644 --- a/extensions/esbuild-extension-common.mts +++ b/extensions/esbuild-extension-common.mts @@ -33,6 +33,7 @@ async function tryBuild(options: BuildOptions, didBuild?: (outDir: string) => un interface RunConfig { readonly platform: 'node' | 'browser'; + readonly format?: 'cjs' | 'esm'; readonly srcDir: string; readonly outdir: string; readonly entryPoints: string[] | Record | { in: string; out: string }[]; @@ -48,6 +49,7 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { sourcemap: true, target: ['es2024'], external: ['vscode'], + format: config.format ?? 'cjs', entryPoints: config.entryPoints, outdir, logOverride: { @@ -57,10 +59,8 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { }; if (config.platform === 'node') { - options.format = 'cjs'; options.mainFields = ['module', 'main']; } else if (config.platform === 'browser') { - options.format = 'cjs'; options.mainFields = ['browser', 'module', 'main']; options.alias = { 'path': 'path-browserify', diff --git a/extensions/github/.vscodeignore b/extensions/github/.vscodeignore index 77ec048a6da..a6590bd3934 100644 --- a/extensions/github/.vscodeignore +++ b/extensions/github/.vscodeignore @@ -2,7 +2,7 @@ src/** !src/common/config.json out/** build/** -extension.webpack.config.js +esbuild*.mts tsconfig*.json package-lock.json testWorkspace/** diff --git a/extensions/github/extension.webpack.config.js b/extensions/github/esbuild.mts similarity index 50% rename from extensions/github/extension.webpack.config.js rename to extensions/github/esbuild.mts index 9e2b191a389..f91916e622d 100644 --- a/extensions/github/extension.webpack.config.js +++ b/extensions/github/esbuild.mts @@ -2,22 +2,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; -export default withDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + platform: 'node', + format: 'esm', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, - output: { - libraryTarget: 'module', - chunkFormat: 'module', - }, - externals: { - 'vscode': 'module vscode', - }, - experiments: { - outputModule: true - } -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/github/package.json b/extensions/github/package.json index 78577f2192d..cb77091cde0 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -19,8 +19,7 @@ "extensionDependencies": [ "vscode.git-base" ], - "main": "./out/extension.js", - "type": "module", + "main": "./dist/extension.js", "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { diff --git a/extensions/github/src/branchProtection.ts b/extensions/github/src/branchProtection.ts index 040df24942a..0c616d33905 100644 --- a/extensions/github/src/branchProtection.ts +++ b/extensions/github/src/branchProtection.ts @@ -6,7 +6,7 @@ import { EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode'; import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema'; import { AuthenticationError, OctokitService } from './auth.js'; -import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.js'; +import type { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.d.ts'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { TelemetryReporter } from '@vscode/extension-telemetry'; diff --git a/extensions/github/src/canonicalUriProvider.ts b/extensions/github/src/canonicalUriProvider.ts index 0838c7377dd..9218707ed26 100644 --- a/extensions/github/src/canonicalUriProvider.ts +++ b/extensions/github/src/canonicalUriProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode'; -import { API } from './typings/git.js'; +import type { API } from './typings/git.d.ts'; const SUPPORTED_SCHEMES = ['ssh', 'https', 'file']; diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 78dd3271588..a8b69f10936 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, RefType, Repository } from './typings/git.js'; +import { RefType } from './typings/git.constants.js'; +import type { API as GitAPI, Repository } from './typings/git.d.ts'; import { publishRepository } from './publish.js'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js'; diff --git a/extensions/github/src/credentialProvider.ts b/extensions/github/src/credentialProvider.ts index d184960c23b..4964724eed6 100644 --- a/extensions/github/src/credentialProvider.ts +++ b/extensions/github/src/credentialProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.js'; +import type { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.d.ts'; import { workspace, Uri, Disposable } from 'vscode'; import { getSession } from './auth.js'; diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 17906c57d44..e6a44f516ac 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -6,7 +6,7 @@ import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode'; import { TelemetryReporter } from '@vscode/extension-telemetry'; import { GithubRemoteSourceProvider } from './remoteSourceProvider.js'; -import { API, GitExtension } from './typings/git.js'; +import type { API, GitExtension } from './typings/git.d.ts'; import { registerCommands } from './commands.js'; import { GithubCredentialProviderManager } from './credentialProvider.js'; import { DisposableStore, repositoryHasGitHubRemote } from './util.js'; diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index 9a267b9e844..d0a145ec9f2 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -5,7 +5,7 @@ import { Command, l10n, LogOutputChannel, workspace } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; -import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.js'; +import type { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.d.ts'; import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util.js'; import { AuthenticationError, OctokitService } from './auth.js'; import { getAvatarLink } from './links.js'; diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index b4f8379e5f7..fbdde106149 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, RefType, Repository } from './typings/git.js'; +import { RefType } from './typings/git.constants.js'; +import type { API as GitAPI, Repository } from './typings/git.d.ts'; import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean { diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index 618f7527450..dab81037d59 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, Repository } from './typings/git.js'; +import type { API as GitAPI, Repository } from './typings/git.d.ts'; import { getOctokit } from './auth.js'; import { TextEncoder } from 'util'; import { basename } from 'path'; diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index f7b0b9ef869..751654515f9 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -6,7 +6,8 @@ import { TextDecoder } from 'util'; import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode'; import { getOctokit } from './auth.js'; -import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git.js'; +import { GitErrorCodes } from './typings/git.constants.js'; +import type { PushErrorHandler, Remote, Repository } from './typings/git.d.ts'; import * as path from 'path'; import { TelemetryReporter } from '@vscode/extension-telemetry'; diff --git a/extensions/github/src/remoteSourcePublisher.ts b/extensions/github/src/remoteSourcePublisher.ts index 97ce05a835c..67c1e567e36 100644 --- a/extensions/github/src/remoteSourcePublisher.ts +++ b/extensions/github/src/remoteSourcePublisher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { publishRepository } from './publish.js'; -import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.js'; +import type { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.d.ts'; export class GithubRemoteSourcePublisher implements RemoteSourcePublisher { readonly name = 'GitHub'; diff --git a/extensions/github/src/shareProviders.ts b/extensions/github/src/shareProviders.ts index d2e94a47147..a52cf84d704 100644 --- a/extensions/github/src/shareProviders.ts +++ b/extensions/github/src/shareProviders.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API } from './typings/git.js'; +import type { API } from './typings/git.d.ts'; import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links.js'; diff --git a/extensions/github/src/typings/git.constants.ts b/extensions/github/src/typings/git.constants.ts new file mode 100644 index 00000000000..5847e21d5d0 --- /dev/null +++ b/extensions/github/src/typings/git.constants.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as git from './git'; + +export type ForcePushMode = git.ForcePushMode; +export type RefType = git.RefType; +export type Status = git.Status; +export type GitErrorCodes = git.GitErrorCodes; + +export const ForcePushMode = Object.freeze({ + Force: 0, + ForceWithLease: 1, + ForceWithLeaseIfIncludes: 2, +}) satisfies typeof git.ForcePushMode; + +export const RefType = Object.freeze({ + Head: 0, + RemoteHead: 1, + Tag: 2, +}) satisfies typeof git.RefType; + +export const Status = Object.freeze({ + INDEX_MODIFIED: 0, + INDEX_ADDED: 1, + INDEX_DELETED: 2, + INDEX_RENAMED: 3, + INDEX_COPIED: 4, + + MODIFIED: 5, + DELETED: 6, + UNTRACKED: 7, + IGNORED: 8, + INTENT_TO_ADD: 9, + INTENT_TO_RENAME: 10, + TYPE_CHANGED: 11, + + ADDED_BY_US: 12, + ADDED_BY_THEM: 13, + DELETED_BY_US: 14, + DELETED_BY_THEM: 15, + BOTH_ADDED: 16, + BOTH_DELETED: 17, + BOTH_MODIFIED: 18, +}) satisfies typeof git.Status; + +export const GitErrorCodes = Object.freeze({ + BadConfigFile: 'BadConfigFile', + BadRevision: 'BadRevision', + AuthenticationFailed: 'AuthenticationFailed', + NoUserNameConfigured: 'NoUserNameConfigured', + NoUserEmailConfigured: 'NoUserEmailConfigured', + NoRemoteRepositorySpecified: 'NoRemoteRepositorySpecified', + NotAGitRepository: 'NotAGitRepository', + NotASafeGitRepository: 'NotASafeGitRepository', + NotAtRepositoryRoot: 'NotAtRepositoryRoot', + Conflict: 'Conflict', + StashConflict: 'StashConflict', + UnmergedChanges: 'UnmergedChanges', + PushRejected: 'PushRejected', + ForcePushWithLeaseRejected: 'ForcePushWithLeaseRejected', + ForcePushWithLeaseIfIncludesRejected: 'ForcePushWithLeaseIfIncludesRejected', + RemoteConnectionError: 'RemoteConnectionError', + DirtyWorkTree: 'DirtyWorkTree', + CantOpenResource: 'CantOpenResource', + GitNotFound: 'GitNotFound', + CantCreatePipe: 'CantCreatePipe', + PermissionDenied: 'PermissionDenied', + CantAccessRemote: 'CantAccessRemote', + RepositoryNotFound: 'RepositoryNotFound', + RepositoryIsLocked: 'RepositoryIsLocked', + BranchNotFullyMerged: 'BranchNotFullyMerged', + NoRemoteReference: 'NoRemoteReference', + InvalidBranchName: 'InvalidBranchName', + BranchAlreadyExists: 'BranchAlreadyExists', + NoLocalChanges: 'NoLocalChanges', + NoStashFound: 'NoStashFound', + LocalChangesOverwritten: 'LocalChangesOverwritten', + NoUpstreamBranch: 'NoUpstreamBranch', + IsInSubmodule: 'IsInSubmodule', + WrongCase: 'WrongCase', + CantLockRef: 'CantLockRef', + CantRebaseMultipleBranches: 'CantRebaseMultipleBranches', + PatchDoesNotApply: 'PatchDoesNotApply', + NoPathFound: 'NoPathFound', + UnknownPath: 'UnknownPath', + EmptyCommitMessage: 'EmptyCommitMessage', + BranchFastForwardRejected: 'BranchFastForwardRejected', + BranchNotYetBorn: 'BranchNotYetBorn', + TagConflict: 'TagConflict', + CherryPickEmpty: 'CherryPickEmpty', + CherryPickConflict: 'CherryPickConflict', + WorktreeContainsChanges: 'WorktreeContainsChanges', + WorktreeAlreadyExists: 'WorktreeAlreadyExists', + WorktreeBranchAlreadyUsed: 'WorktreeBranchAlreadyUsed', +}) satisfies Record; diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 2247292dd93..bcdddaed6e5 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Repository } from './typings/git.js'; +import type { Repository } from './typings/git.d.ts'; export class DisposableStore { From 9c1300d59a95e2b149ec620b5f9d3880bec8f3d6 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:15:47 -0700 Subject: [PATCH 355/448] Bump default extension target version Let's use a more modern default --- .../services/extensions/common/extensionsRegistry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 2ea95cdb8c4..f9d57d5a53e 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -179,8 +179,8 @@ export const schema: IJSONSchema = { properties: { 'vscode': { type: 'string', - description: nls.localize('vscode.extension.engines.vscode', 'For VS Code extensions, specifies the VS Code version that the extension is compatible with. Cannot be *. For example: ^0.10.5 indicates compatibility with a minimum VS Code version of 0.10.5.'), - default: '^1.22.0', + description: nls.localize('vscode.extension.engines.vscode', 'For VS Code extensions, specifies the VS Code version that the extension is compatible with. Cannot be *. For example: ^1.105.0 indicates compatibility with a minimum VS Code version of 1.105.0.'), + default: '^1.105.0', } } }, From 2d85f4314e5c0f445d3235e33eaa8071256d2295 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 9 Mar 2026 16:36:45 +0100 Subject: [PATCH 356/448] cleanup for prompt file variables (#300185) --- .../api/common/extHostChatSessions.ts | 26 ++++++++----------- .../common/attachments/chatVariableEntries.ts | 20 ++++++++------ .../computeAutomaticInstructions.ts | 8 +++--- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index d33a86b6f5b..e964a3720e6 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -13,12 +13,11 @@ import { Disposable, DisposableStore, toDisposable } from '../../../base/common/ import { ResourceMap, ResourceSet } from '../../../base/common/map.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import * as objects from '../../../base/common/objects.js'; -import { basename } from '../../../base/common/resources.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; -import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, IPromptFileVariableEntry, ISymbolVariableEntry, PromptFileVariableKind } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js'; @@ -709,19 +708,16 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } satisfies ISymbolVariableEntry; } - if (URI.isUri(value) && ref.name.startsWith(`prompt:`) && - ref.id.startsWith(PromptFileVariableKind.PromptFile) && - ref.id.endsWith(value.toString())) { - return { - id: ref.id, - name: `prompt:${basename(value)}`, - value, - kind: 'promptFile', - modelDescription: 'Prompt instructions file', - isRoot: true, - automaticallyAdded: false, - range, - } satisfies IPromptFileVariableEntry; + if (URI.isUri(value) && ref.name.startsWith(`prompt:`)) { + if (ref.id.startsWith(PromptFileVariableKind.Instruction)) { + return toPromptFileVariableEntry(value, PromptFileVariableKind.Instruction); + } + if (ref.id.startsWith(PromptFileVariableKind.InstructionReference)) { + return toPromptFileVariableEntry(value, PromptFileVariableKind.InstructionReference); + } + if (ref.id.startsWith(PromptFileVariableKind.PromptFile)) { + return toPromptFileVariableEntry(value, PromptFileVariableKind.PromptFile); + } } const isFile = URI.isUri(value) || (value && typeof value === 'object' && 'uri' in value); diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 3dcb14d7bbb..f03eea0f2a1 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -469,17 +469,17 @@ export function isStringImplicitContextValue(value: unknown): value is StringCha } export enum PromptFileVariableKind { - Instruction = 'vscode.prompt.instructions.root', - InstructionReference = `vscode.prompt.instructions`, - PromptFile = 'vscode.prompt.file' + Instruction = 'vscode.instructions.file.root', + InstructionReference = `vscode.instructions.file.reference`, + PromptFile = 'vscode.prompt.file', } /** * Utility to convert a {@link uri} to a chat variable entry. * The `id` of the chat variable can be one of the following: * - * - `vscode.prompt.instructions__`: for all non-root prompt instructions references - * - `vscode.prompt.instructions.root__`: for *root* prompt instructions references + * - `vscode.instructions.file.reference__`: for all non-root prompt instructions references + * - `vscode.instructions.file.root__`: for *root* prompt instructions references * - `vscode.prompt.file__`: for prompt file references * * @param uri A resource URI that points to a prompt instructions file. @@ -500,13 +500,17 @@ export function toPromptFileVariableEntry(uri: URI, kind: PromptFileVariableKind }; } +enum PromptTextVariableKind { + CustomizationsIndex = 'vscode.customizations.index', +} + export function toPromptTextVariableEntry(content: string, automaticallyAdded = false, toolReferences?: ChatRequestToolReferenceEntry[]): IPromptTextVariableEntry { return { - id: `vscode.prompt.instructions.text`, - name: `prompt:instructionsList`, + id: PromptTextVariableKind.CustomizationsIndex, + name: `prompt:customizationsIndex`, value: content, kind: 'promptText', - modelDescription: 'Prompt instructions list', + modelDescription: 'Chat customizations index', automaticallyAdded, toolReferences }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 16b5459a50c..f3fbb1a8c1e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -109,9 +109,9 @@ export class ComputeAutomaticInstructions { // get copilot instructions await this._addAgentInstructions(variables, telemetryEvent, token); - const instructionsListVariable = await this._getInstructionsWithPatternsList(instructionFiles, variables, telemetryEvent, token); - if (instructionsListVariable) { - variables.add(instructionsListVariable); + const customizationsIndexVariable = await this._getCustomizationsIndex(instructionFiles, variables, telemetryEvent, token); + if (customizationsIndexVariable) { + variables.add(customizationsIndexVariable); telemetryEvent.listedInstructionsCount++; } @@ -293,7 +293,7 @@ export class ComputeAutomaticInstructions { return undefined; } - private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { + private async _getCustomizationsIndex(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { const readTool = this._getTool('readFile'); const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent); From 9cf71aa122f33dd050c1023421a98ab55888bec3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 9 Mar 2026 16:55:19 +0100 Subject: [PATCH 357/448] fix - update workspace identifier path in `SessionsMain` --- src/vs/sessions/electron-browser/sessions.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/electron-browser/sessions.main.ts b/src/vs/sessions/electron-browser/sessions.main.ts index 6cfdffb6692..4cb60de6157 100644 --- a/src/vs/sessions/electron-browser/sessions.main.ts +++ b/src/vs/sessions/electron-browser/sessions.main.ts @@ -292,7 +292,7 @@ export class SessionsMain extends Disposable { // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - const workspaceIdentifier = getWorkspaceIdentifier(uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(userDataProfilesService.profilesHome), 'agent-sessions.code-workspace')); + const workspaceIdentifier = getWorkspaceIdentifier(environmentService.agentSessionsWorkspace); const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService); // Workspace From 26876d30a14b49c67d9ef7323dbb30a2afca6419 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:59:46 -0700 Subject: [PATCH 358/448] Revert main change --- extensions/github/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index cb77091cde0..071f1786884 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -19,7 +19,7 @@ "extensionDependencies": [ "vscode.git-base" ], - "main": "./dist/extension.js", + "main": "./out/extension.js", "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { @@ -182,8 +182,7 @@ "when": "github.hasGitHubRepo && timelineItem =~ /git:file:commit\\b/" } ], - "chat/input/editing/sessionApplyActions": [ - ] + "chat/input/editing/sessionApplyActions": [] }, "configuration": [ { From 02d9c6d239241a0e8386c83e6adef118ed3ff4c4 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:38:05 -0700 Subject: [PATCH 359/448] Also set `type: module` again --- extensions/github/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/github/package.json b/extensions/github/package.json index 071f1786884..a122a856488 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -19,6 +19,7 @@ "extensionDependencies": [ "vscode.git-base" ], + "type": "module", "main": "./out/extension.js", "capabilities": { "virtualWorkspaces": true, From e74071cd6303e2eb8a33d2d4d8ecdf9cf7537b8c Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:38:32 -0700 Subject: [PATCH 360/448] Fix import --- extensions/github/src/typings/git.constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/github/src/typings/git.constants.ts b/extensions/github/src/typings/git.constants.ts index 5847e21d5d0..e39a3fb03b3 100644 --- a/extensions/github/src/typings/git.constants.ts +++ b/extensions/github/src/typings/git.constants.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as git from './git'; +import type * as git from './git.d.ts'; export type ForcePushMode = git.ForcePushMode; export type RefType = git.RefType; From 0fb3dfe61f578e9ebba5d8310722adf98eb4a50f Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 10 Mar 2026 02:09:51 +0900 Subject: [PATCH 361/448] fix: remove applications folder on stable (#300211) --- build/gulpfile.vscode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index e343569490d..0912be85425 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -550,7 +550,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d '**', '!LICENSE', '!version', - ...(platform === 'darwin' && !isInsiderOrExploration ? ['!**/Contents/Applications'] : []), + ...(platform === 'darwin' && !isInsiderOrExploration ? ['!**/Contents/Applications', '!**/Contents/Applications/**'] : []), ...(platform === 'win32' && !isInsiderOrExploration ? ['!**/electron_proxy.exe'] : []), ], { dot: true })); From dcea9a598cc54f39d997baf6e9ed8174b3f4193d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:48:14 -0700 Subject: [PATCH 362/448] Bump dompurify from 3.2.7 to 3.3.2 in /extensions/markdown-language-features (#299899) Bump dompurify in /extensions/markdown-language-features Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.2.7 to 3.3.2. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.2.7...3.3.2) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.3.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../markdown-language-features/package-lock.json | 11 +++++++---- extensions/markdown-language-features/package.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index fbde1da49b9..162ff688b0a 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "dompurify": "^3.2.7", + "dompurify": "^3.3.2", "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.4", @@ -386,10 +386,13 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 2993574a7fa..b4141d80bf5 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -782,7 +782,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "dompurify": "^3.2.7", + "dompurify": "^3.3.2", "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.4", From 7b4ba7d0935e54e83020e893dcb52ad4d2765d4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:51:20 -0700 Subject: [PATCH 363/448] Bump express-rate-limit from 8.2.1 to 8.3.0 in /test/mcp (#299836) Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.3.0. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.3.0) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.3.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/mcp/package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index f480569530b..311b01f21bf 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -489,12 +489,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -753,9 +753,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" From 072e99c1215d4acf06da82f9475d5a9b2fbc6e15 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:58:54 -0700 Subject: [PATCH 364/448] Fix browser preventing system sleep (#300215) No more insomnia --- src/vs/platform/browserView/electron-main/browserView.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index bcac609f8e6..e08d161db21 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -480,8 +480,7 @@ export class BrowserView extends Disposable implements ICDPTarget { async captureScreenshot(options?: IBrowserViewCaptureScreenshotOptions): Promise { const quality = options?.quality ?? 80; const image = await this._view.webContents.capturePage(options?.rect, { - stayHidden: true, - stayAwake: true + stayHidden: true }); const buffer = image.toJPEG(quality); const screenshot = VSBuffer.wrap(buffer); From 600073511e5bb305ca820444b0ec6d2c658590da Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:09:48 -0700 Subject: [PATCH 365/448] Remove confusing `chatResource` from `NewChatSessionOpenOptions` Looks like this is no longer being used --- .../chat/browser/chatSessions/chatSessions.contribution.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 34118f93f1f..e551298fb3a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -1287,7 +1287,6 @@ export type NewChatSessionOpenOptions = { readonly type: string; readonly position: ChatSessionPosition; readonly displayName: string; - readonly chatResource?: UriComponents; readonly replaceEditor?: boolean; }; @@ -1363,10 +1362,6 @@ async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatS } export function getResourceForNewChatSession(options: NewChatSessionOpenOptions): URI { - if (options.chatResource) { - return URI.revive(options.chatResource); - } - const isRemoteSession = options.type !== AgentSessionProviders.Local; if (isRemoteSession) { return URI.from({ @@ -1380,7 +1375,7 @@ export function getResourceForNewChatSession(options: NewChatSessionOpenOptions) return ChatEditorInput.getNewEditorUri(); } - return LocalChatSessionUri.forSession(generateUuid()); + return LocalChatSessionUri.getNewSessionUri(); } function isAgentSessionProviderType(type: string): boolean { From 6c1f6d84e8c0e8dee13235287ed3ca4a0096160c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 9 Mar 2026 13:53:33 -0700 Subject: [PATCH 366/448] chat: add symbol paste provider (#300243) * chat: add symbol paste provider Adds PasteSymbolProvider to automatically convert copied symbol identifiers into chat symbol variable references (@sym:identifier) when pasting into the chat input. - Adds ResolvedSymbolReference interface to represent a resolved symbol with location, icon, and metadata - Implements CopyTextProvider.prepareDocumentPaste to prime a symbol reference cache when copying from code editors - Adds resolveSymbolReference function that uses language definition providers and document outline to resolve copied identifiers to their definitions and determine appropriate icons - Adds symbol reference cache with TTL-based expiration to avoid repeatedly resolving the same symbols - Implements PasteSymbolProvider.provideDocumentPasteEdits to detect when an identifier is pasted into chat input and convert it to a symbol variable reference - Registers PasteSymbolProvider alongside other paste providers in ChatPasteProvidersFeature (Commit message generated by Copilot) * comment --- .../widget/input/editor/chatPasteProviders.ts | 295 +++++++++++++++++- 1 file changed, 292 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts index 527e9e9becf..0cd75d575cd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts @@ -13,11 +13,14 @@ import { Mimes } from '../../../../../../../base/common/mime.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { basename, joinPath } from '../../../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../../../base/common/uri.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; import { IRange } from '../../../../../../../editor/common/core/range.js'; -import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../../../../editor/common/languages.js'; +import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, SymbolKinds } from '../../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'; import { IModelService } from '../../../../../../../editor/common/services/model.js'; +import { IOutlineModelService } from '../../../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; +import { getDefinitionsAtPosition } from '../../../../../../../editor/contrib/gotoSymbol/browser/goToSymbol.js'; import { localize } from '../../../../../../../nls.js'; import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; @@ -25,6 +28,7 @@ import { IInstantiationService } from '../../../../../../../platform/instantiati import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { IExtensionService, isProposedApiEnabled } from '../../../../../../services/extensions/common/extensions.js'; import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, isImageVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; +import { chatVariableLeader } from '../../../../common/requestParser/chatParserTypes.js'; import { IDynamicVariable } from '../../../../common/attachments/chatVariables.js'; import { IChatWidgetService } from '../../../chat.js'; import { getDynamicVariablesForWidget } from '../../../attachments/chatVariables.js'; @@ -38,6 +42,16 @@ interface SerializedCopyData { readonly range: IRange; } +interface ResolvedSymbolReference { + id: string; + fullName: string; + data: { + uri: URI; + range: IRange; + }; + icon: IDynamicVariable['icon']; +} + export class PasteImageProvider implements DocumentPasteEditProvider { private readonly imagesFolder: URI; @@ -179,6 +193,12 @@ export class CopyTextProvider implements DocumentPasteEditProvider { public readonly copyMimeTypes = [COPY_MIME_TYPES]; public readonly pasteMimeTypes = []; + constructor( + @IModelService private readonly modelService: IModelService, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IOutlineModelService private readonly outlineModelService: IOutlineModelService, + ) { } + async prepareDocumentPaste(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { if (model.uri.scheme === Schemas.vscodeChatInput) { return; @@ -187,8 +207,35 @@ export class CopyTextProvider implements DocumentPasteEditProvider { const customDataTransfer = new VSDataTransfer(); const data: SerializedCopyData = { range: ranges[0], uri: model.uri.toJSON() }; customDataTransfer.append(COPY_MIME_TYPES, createStringDataTransferItem(JSON.stringify(data))); + + const text = dataTransfer.get(Mimes.text); + if (text && ranges.length) { + void this.primeSymbolReferenceCache(model, ranges[0], text, token); + } + return customDataTransfer; } + + private async primeSymbolReferenceCache(model: ITextModel, range: IRange, textItem: IDataTransferItem, token: CancellationToken): Promise { + const copiedText = model.getValueInRange(range); + if (range.startLineNumber !== range.endLineNumber) { + return; + } + + if (token.isCancellationRequested || !identifierPattern.test(copiedText)) { + return; + } + + cacheSymbolReference(model.uri, range, copiedText, resolveSymbolReference( + this.modelService, + this.languageFeaturesService, + this.outlineModelService, + model.uri, + range, + copiedText, + token, + )); + } } class CopyAttachmentsProvider implements DocumentPasteEditProvider { @@ -433,6 +480,248 @@ function createEditSession(edit: DocumentPasteEdit): DocumentPasteEditsSession { }; } +const identifierPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; +const symbolCacheTtlMs = 15_000; +type SymbolReferenceCacheEntry = { + expiresAt: number; + value?: ResolvedSymbolReference; + promise?: Promise; +}; + +const symbolReferenceCache = new Map(); + +function getSymbolReferenceCacheKey(uri: URI, range: IRange, text: string): string { + return `${uri.toString()}|${range.startLineNumber}:${range.startColumn}-${range.endLineNumber}:${range.endColumn}|${text}`; +} + +function pruneSymbolReferenceCache(): void { + const now = Date.now(); + for (const [key, value] of symbolReferenceCache) { + if (value.expiresAt <= now) { + symbolReferenceCache.delete(key); + } + } +} + +async function getCachedSymbolReference(uri: URI, range: IRange, text: string): Promise { + const key = getSymbolReferenceCacheKey(uri, range, text); + const entry = symbolReferenceCache.get(key); + pruneSymbolReferenceCache(); + + if (!entry) { + return; + } + + if (entry.value) { + return entry.value; + } + + if (entry.promise) { + return entry.promise; + } + + return; +} + +function cacheSymbolReference(uri: URI, range: IRange, text: string, valuePromise: Promise): void { + const key = getSymbolReferenceCacheKey(uri, range, text); + const wrappedPromise = valuePromise.then(value => { + const current = symbolReferenceCache.get(key); + if (current?.promise !== wrappedPromise) { + return value; + } + + if (!value) { + symbolReferenceCache.delete(key); + return; + } + + symbolReferenceCache.set(key, { + value, + expiresAt: Date.now() + symbolCacheTtlMs + }); + pruneSymbolReferenceCache(); + return value; + }, () => { + const current = symbolReferenceCache.get(key); + if (current?.promise === wrappedPromise) { + symbolReferenceCache.delete(key); + } + return undefined; + }); + + symbolReferenceCache.set(key, { + promise: wrappedPromise, + expiresAt: Date.now() + symbolCacheTtlMs + }); + pruneSymbolReferenceCache(); + + void wrappedPromise; +} + +async function resolveSymbolReference( + modelService: IModelService, + languageFeaturesService: ILanguageFeaturesService, + outlineModelService: IOutlineModelService, + sourceUri: URI, + sourceRange: IRange, + pastedText: string, + token: CancellationToken, +): Promise { + const sourceModel = modelService.getModel(sourceUri); + if (!sourceModel) { + return; + } + + const sourcePosition = new Position(sourceRange.startLineNumber, sourceRange.startColumn); + const definitions = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, sourceModel, sourcePosition, false, token); + if (token.isCancellationRequested || !definitions.length) { + return; + } + + const def = definitions[0]; + const defRange = def.targetSelectionRange ?? def.range; + const defLocation = { uri: def.uri, range: defRange }; + + let icon = Codicon.symbolProperty; + const defModel = modelService.getModel(def.uri); + if (defModel) { + try { + const outline = await outlineModelService.getOrCreate(defModel, token); + if (!token.isCancellationRequested) { + const element = outline.getItemEnclosingPosition({ lineNumber: defRange.startLineNumber, column: defRange.startColumn }); + if (element) { + icon = SymbolKinds.toIcon(element.symbol.kind); + } + } + } catch { + // Use default icon. + } + } + + if (token.isCancellationRequested) { + return; + } + + return { + id: `vscode.symbol/${JSON.stringify(defLocation)}`, + fullName: pastedText, + data: defLocation, + icon + }; +} + +class PasteSymbolProvider implements DocumentPasteEditProvider { + + public readonly kind = new HierarchicalKind('chat.attach.symbol'); + public readonly providedPasteEditKinds = [this.kind]; + + public readonly copyMimeTypes = []; + public readonly pasteMimeTypes = [COPY_MIME_TYPES]; + + constructor( + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IModelService private readonly modelService: IModelService, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IOutlineModelService private readonly outlineModelService: IOutlineModelService, + ) { } + + async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, _context: DocumentPasteContext, token: CancellationToken): Promise { + if (model.uri.scheme !== Schemas.vscodeChatInput) { + return; + } + + const text = dataTransfer.get(Mimes.text); + const additionalEditorData = dataTransfer.get(COPY_MIME_TYPES); + if (!text || !additionalEditorData) { + return; + } + + const pastedText = await text.asString(); + if (!identifierPattern.test(pastedText)) { + return; + } + + let additionalData: SerializedCopyData; + try { + additionalData = JSON.parse(await additionalEditorData.asString()); + } catch { + return; + } + + const sourceUri = URI.revive(additionalData.uri); + const sourceRange = additionalData.range; + + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return; + } + + const cached = await getCachedSymbolReference(sourceUri, sourceRange, pastedText); + let resolved = cached; + if (!resolved) { + resolved = await resolveSymbolReference( + this.modelService, + this.languageFeaturesService, + this.outlineModelService, + sourceUri, + sourceRange, + pastedText, + token, + ); + } + if (!resolved) { + return; + } + + if (token.isCancellationRequested) { + return; + } + + const symText = `${chatVariableLeader}sym:${pastedText}`; + const pasteRange = ranges[0]; + const insertText = `${symText} `; + + const refRange = { + startLineNumber: pasteRange.startLineNumber, + startColumn: pasteRange.startColumn, + endLineNumber: pasteRange.startLineNumber, + endColumn: pasteRange.startColumn + symText.length + }; + + const dynamicRef = { + id: resolved.id, + fullName: resolved.fullName, + range: refRange, + data: resolved.data, + icon: resolved.icon + }; + + const edit: DocumentPasteEdit = { + insertText, + title: localize('pastedSymbolReference', 'Pasted Symbol Reference'), + kind: this.kind, + handledMimeType: COPY_MIME_TYPES, + additionalEdit: { + edits: [{ + resource: model.uri, + redo: () => { + const w = this.chatWidgetService.getWidgetByInputUri(model.uri); + w?.getContrib(ChatDynamicVariableModel.ID)?.addReference(dynamicRef); + }, + undo: () => { + // The text removal by undo is sufficient; the dynamic variable + // model auto-cleans when the decoration text changes. + } + }] + } + }; + + edit.yieldTo = [{ kind: new HierarchicalKind('chat.attach.text') }]; + return createEditSession(edit); + } +} + export class ChatPasteProvidersFeature extends Disposable { constructor( @IInstantiationService instaService: IInstantiationService, @@ -448,7 +737,7 @@ export class ChatPasteProvidersFeature extends Disposable { this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, instaService.createInstance(CopyAttachmentsProvider))); this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService, fileService, environmentService, logService))); this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService, modelService))); - this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider())); - this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider())); + this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, instaService.createInstance(PasteSymbolProvider))); + this._register(languageFeaturesService.documentPasteEditProvider.register('*', instaService.createInstance(CopyTextProvider))); } } From b3fff3f2102a2c6c2671f6ca7ddbc2d6b487e256 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:22:34 -0700 Subject: [PATCH 367/448] Reduce size of `IChatSessionContext` All of this info is already on the chatSessionResource --- .../browser/sessionsManagementService.ts | 3 ++- .../api/browser/mainThreadChatAgents2.ts | 7 +++---- .../chat/browser/actions/chatExecuteActions.ts | 6 +++--- .../browser/chatDebug/chatDebugOverviewView.ts | 4 ++-- .../chatSessions/chatSessions.contribution.ts | 4 ++-- .../chat/browser/widget/input/chatInputPart.ts | 16 ++++++++-------- .../widget/input/editor/chatInputCompletions.ts | 3 ++- .../chat/common/chatService/chatService.ts | 2 -- .../chat/common/chatService/chatServiceImpl.ts | 2 -- .../contrib/chat/common/model/chatUri.ts | 4 ++++ 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 46f9e56ce0f..be6b61aa821 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -25,6 +25,7 @@ import { INewSession, LocalNewSession, RemoteNewSession } from '../../chat/brows import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; +import { isUntitledChatSession } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -437,7 +438,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.lastSelectedSession = session.resource; const [repository, worktree, worktreeBranchName] = this.getRepositoryFromMetadata(session); activeSessionItem = { - isUntitled: this.chatService.getSession(session.resource)?.contributedChatSession?.isUntitled ?? true, + isUntitled: isUntitledChatSession(session.resource), label: session.label, resource: session.resource, repository, diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index b851f3fe206..08f039daab2 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -42,6 +42,7 @@ import { IExtensionService } from '../../services/extensions/common/extensions.j import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, IInstructionDto, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js'; import { NotebookDto } from './mainThreadNotebookDto.js'; +import { getChatSessionType, isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js'; interface AgentData { dispose: () => void; @@ -247,12 +248,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA let chatSessionContext: IChatSessionContextDto | undefined; if (contributedSession) { let chatSessionResource = contributedSession.chatSessionResource; - let isUntitled = contributedSession.isUntitled; + let isUntitled = isUntitledChatSession(chatSessionResource); // For new untitled sessions, invoke the controller's newChatSessionItemHandler // to let the extension create a proper session item before the first request. if (isUntitled) { - const newItem = await this._chatSessionService.createNewChatSessionItem(contributedSession.chatSessionType, request, token); + const newItem = await this._chatSessionService.createNewChatSessionItem(getChatSessionType(contributedSession.chatSessionResource), request, token); if (newItem) { chatSessionResource = newItem.resource; isUntitled = false; @@ -261,9 +262,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA // so subsequent requests don't re-invoke newChatSessionItemHandler // and getChatSessionFromInternalUri returns the real resource. chatSession?.setContributedChatSession({ - chatSessionType: contributedSession.chatSessionType, chatSessionResource, - isUntitled: false, initialSessionOptions: contributedSession.initialSessionOptions, }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 14b0cb77d71..26f26818255 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -31,7 +31,7 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { isInClaudeAgentsFolder } from '../../common/promptSyntax/config/promptFileLocations.js'; -import { IChatSessionsService } from '../../common/chatSessionsService.js'; +import { IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { getAgentSessionProvider, AgentSessionProviders } from '../agentSessions/agentSessions.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; @@ -651,7 +651,7 @@ export class OpenWorkspacePickerAction extends Action2 { order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo('local'), + ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), IsSessionsWindowContext ), group: 'navigation', @@ -661,7 +661,7 @@ export class OpenWorkspacePickerAction extends Action2 { order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo('local'), + ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), IsSessionsWindowContext.negate() ), group: 'navigation', diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugOverviewView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugOverviewView.ts index 6555bf09308..fc5ce77add7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugOverviewView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugOverviewView.ts @@ -16,7 +16,7 @@ import { defaultBreadcrumbsWidgetStyles, defaultButtonStyles } from '../../../.. import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { IChatSessionsService } from '../../common/chatSessionsService.js'; +import { IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js'; import { IChatWidgetService } from '../chat.js'; import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem } from './chatDebugTypes.js'; @@ -159,7 +159,7 @@ export class ChatDebugOverviewView extends Disposable { // Session type const sessionType = getChatSessionType(sessionUri); const contribution = this.chatSessionsService.getChatSessionContribution(sessionType); - const sessionTypeName = contribution?.displayName || (sessionType === 'local' + const sessionTypeName = contribution?.displayName || (sessionType === localChatSessionType ? localize('chatDebug.sessionType.local', "Local") : sessionType); details.push({ label: localize('chatDebug.detail.sessionType', "Session Type"), value: sessionTypeName }); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index e551298fb3a..4bf43903031 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -47,7 +47,7 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { AgentSessionProviders, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { BugIndicatingError, isCancellationError } from '../../../../../base/common/errors.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; -import { LocalChatSessionUri } from '../../common/model/chatUri.js'; +import { isUntitledChatSession, LocalChatSessionUri } from '../../common/model/chatUri.js'; import { assertNever } from '../../../../../base/common/assert.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { Target } from '../../common/promptSyntax/promptTypes.js'; @@ -1047,7 +1047,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ let session: IChatSession; const newSessionOptions = this.getNewSessionOptionsForSessionType(resolvedType); - if (sessionResource.path.startsWith('/untitled') && newSessionOptions) { + if (isUntitledChatSession(sessionResource) && newSessionOptions) { session = { sessionResource: sessionResource, onWillDispose: Event.None, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 85f1d2dc4b3..206e6f5d4b8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -570,7 +570,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (sessionResource) { const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); - if (ctx?.chatSessionType === chatSessionType || delegateSessionType === chatSessionType) { + if (ctx && (getChatSessionType(ctx.chatSessionResource) === chatSessionType) || delegateSessionType === chatSessionType) { this.refreshChatSessionPickers(); } } @@ -1134,7 +1134,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } const sessionResource = this._widget?.viewModel?.model.sessionResource; const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; - return ctx?.chatSessionType; + return ctx ? getChatSessionType(ctx.chatSessionResource) : undefined; } /** @@ -1169,7 +1169,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private preselectModelFromSessionHistory(): void { const sessionResource = this._widget?.viewModel?.model.sessionResource; const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; - const requiresCustomModels = ctx && this.chatSessionsService.requiresCustomModelsForSessionType(ctx.chatSessionType); + const requiresCustomModels = ctx && this.chatSessionsService.requiresCustomModelsForSessionType(getChatSessionType(ctx.chatSessionResource)); if (!requiresCustomModels) { return; } @@ -1605,11 +1605,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; // Check if this session type has a customAgentTarget - const customAgentTarget = ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType); + const customAgentTarget = ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(getChatSessionType(ctx.chatSessionResource)); this.chatSessionHasCustomAgentTarget.set(customAgentTarget !== Target.Undefined); // Check if this session type requires custom models - const requiresCustomModels = ctx && this.chatSessionsService.requiresCustomModelsForSessionType(ctx.chatSessionType); + const requiresCustomModels = ctx && this.chatSessionsService.requiresCustomModelsForSessionType(getChatSessionType(ctx.chatSessionResource)); this.chatSessionHasTargetedModels.set(!!requiresCustomModels); // Handle agent option from session - set initial mode @@ -1633,7 +1633,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // - Panel/Editor: Use actual session's type (ctx available) // - Welcome view: Use delegate's type (ctx may not exist yet) const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); - const effectiveSessionType = delegateSessionType ?? ctx?.chatSessionType; + const effectiveSessionType = delegateSessionType ?? (ctx ? getChatSessionType(ctx.chatSessionResource) : undefined); if (!effectiveSessionType) { setNoOptions(); @@ -1817,7 +1817,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private getEffectiveSessionType(ctx: IChatSessionContext | undefined, delegate: ISessionTypePickerDelegate | undefined): string { - return this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.() || ctx?.chatSessionType || ''; + return this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.() || (ctx && getChatSessionType(ctx.chatSessionResource)) || ''; } /** @@ -2238,7 +2238,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge customAgentTarget: () => { const sessionResource = this._widget?.viewModel?.model.sessionResource; const ctx = sessionResource && this.chatService.getChatSessionFromInternalUri(sessionResource); - return (ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType)) ?? Target.Undefined; + return (ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(getChatSessionType(ctx.chatSessionResource))) ?? Target.Undefined; }, }; return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index f72ccab3f56..0a068f1b964 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -67,6 +67,7 @@ import { IChatService } from '../../../../common/chatService/chatService.js'; import { IChatDebugService } from '../../../../common/chatDebugService.js'; import { createDebugEventsAttachment } from '../../../chatDebug/chatDebugAttachment.js'; import { getPromptFileType } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { getChatSessionType } from '../../../../common/model/chatUri.js'; /** * Regex matching a slash command word (e.g. `/foo`). Uses `\p{L}` for Unicode @@ -108,7 +109,7 @@ class SlashCommandCompletions extends Disposable { } const sessionResource = widget.viewModel.model.sessionResource; const ctx = sessionResource && chatService.getChatSessionFromInternalUri(sessionResource); - customAgentTarget = (ctx ? chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType) : undefined) ?? Target.Undefined; + customAgentTarget = (ctx ? chatSessionsService.getCustomAgentTargetForSessionType(getChatSessionType(sessionResource)) : undefined) ?? Target.Undefined; } const range = computeCompletionRanges(model, position, SlashCommandWord); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 5edbc0c6595..8a7cfb38025 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1481,9 +1481,7 @@ export interface IChatService { } export interface IChatSessionContext { - readonly chatSessionType: string; readonly chatSessionResource: URI; - readonly isUntitled: boolean; readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | { id: string; name: string } }>; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index e91a75cd468..6aab2cb9598 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -615,8 +615,6 @@ export class ChatService extends Disposable implements IChatService { modelRef.object.setContributedChatSession({ chatSessionResource: sessionResource, - chatSessionType, - isUntitled: sessionResource.path.startsWith('/untitled-') //TODO(jospicer) }); if (providedSession.title) { diff --git a/src/vs/workbench/contrib/chat/common/model/chatUri.ts b/src/vs/workbench/contrib/chat/common/model/chatUri.ts index 911494c6e80..0a0830b7950 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatUri.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatUri.ts @@ -91,3 +91,7 @@ export function getChatSessionType(resource: URI): string { return resource.scheme; } + +export function isUntitledChatSession(resource: URI): boolean { + return resource.path.startsWith('/untitled-'); +} From 4bfee9c8ea9c422816b70d1f25e297f27a14a6e1 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 10 Mar 2026 09:19:23 +1100 Subject: [PATCH 368/448] fix: add resultDetails to ChatToolInvocationPart and Response handling (#300238) --- src/vs/workbench/api/common/extHostTypeConverters.ts | 1 + src/vs/workbench/contrib/chat/common/chatService/chatService.ts | 1 + src/vs/workbench/contrib/chat/common/model/chatModel.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 78b0c904947..4515d21525a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2934,6 +2934,7 @@ export namespace ChatToolInvocationPart { pastTenseMessage: part.pastTenseMessage ? MarkdownString.from(part.pastTenseMessage) : undefined, toolSpecificData, subagentInvocationId: part.subAgentInvocationId, + resultDetails }; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 5edbc0c6595..432f78c7370 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -876,6 +876,7 @@ export interface IChatExternalToolInvocationUpdate { pastTenseMessage?: string | IMarkdownString; toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatModifiedFilesConfirmationData; subagentInvocationId?: string; + resultDetails?: IToolResultInputOutputDetails; } export interface IChatTodoListContent { diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 9e9b63d7861..8d433ce3efd 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -850,6 +850,7 @@ export class Response extends AbstractResponse implements IDisposable { content: [], toolResultMessage: progress.pastTenseMessage, toolResultError: progress.errorMessage, + toolResultDetails: progress.resultDetails }); } if (progress.toolSpecificData !== undefined) { @@ -886,6 +887,7 @@ export class Response extends AbstractResponse implements IDisposable { content: [], toolResultMessage: progress.pastTenseMessage, toolResultError: progress.errorMessage, + toolResultDetails: progress.resultDetails }); if (progress.toolSpecificData !== undefined) { invocation.toolSpecificData = progress.toolSpecificData; From cd1a5c39cb564a1fd4db8916c207e4cbeacdfc95 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:19:44 -0700 Subject: [PATCH 369/448] Ensure all the commands passed to srt as positional params instead of options. (#300252) * changes to ensure all the network requests are passed through proxy * changes to ensure all the network requests are passed through proxy * changes to quote shell arguments passed to sandbox * updates to default paths --- .../contrib/mcp/common/mcpSandboxService.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts index 4cfa41a6cdf..0776dcd3bff 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts @@ -57,6 +57,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService private _sandboxSettingsId: string | undefined; private _remoteEnvDetailsPromise: Promise; private readonly _defaultAllowedDomains: readonly string[] = ['registry.npmjs.org']; // Default allowed domains that are commonly needed for MCP servers, even if the user doesn't specify them in their sandbox config + private _defaultAllowWritePaths: string[] = ['~/.npm']; private _sandboxConfigPerConfigurationTarget: Map = new Map(); constructor( @@ -87,7 +88,9 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService if (await this.isEnabled(serverDef, remoteAuthority)) { this._logService.trace(`McpSandboxService: Launching with config target ${configTarget}`); const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, launch.sandbox, launch.cwd); - const sandboxArgs = this._getSandboxCommandArgs(launch.command, launch.args, launchDetails.sandboxConfigPath); + const quotedCommand = this._quoteShellArgument(launch.command); + const quotedArgs = launch.args.map(arg => this._quoteShellArgument(arg)); + const sandboxArgs = this._getSandboxCommandArgs(quotedCommand, quotedArgs, launchDetails.sandboxConfigPath); const sandboxEnv = await this._getSandboxEnvVariables(launch.env, launchDetails.tempDir, launchDetails.rgPath, remoteAuthority); if (launchDetails.srtPath) { if (launchDetails.execPath) { @@ -294,6 +297,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService const result: string[] = []; if (sandboxConfigPath) { result.push('--settings', sandboxConfigPath); + result.push('--'); } result.push(command, ...args); return result; @@ -381,14 +385,13 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService } private _getDefaultAllowWrite(directories?: string[]): readonly string[] { - const defaultAllowWrite: string[] = ['~/.npm']; for (const launchCwd of directories ?? []) { const trimmed = launchCwd.trim(); if (trimmed) { - defaultAllowWrite.push(trimmed); + this._defaultAllowWritePaths.push(trimmed); } } - return defaultAllowWrite; + return this._defaultAllowWritePaths; } private _pathJoin = (os: OperatingSystem, ...segments: string[]) => { @@ -401,4 +404,8 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService return os === OperatingSystem.Windows ? win32.delimiter : posix.delimiter; }; + private _quoteShellArgument(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; + } + } From 2817ed09f54a83f7d8f297a5223ed623356ff8ec Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:30:47 +0000 Subject: [PATCH 370/448] Fix JavaScript syntax highlighting for RunPlaywrightCode tool input (#299918) * Initial plan * fix: use correct language for input syntax highlighting in tool invocation parts Co-authored-by: jruales <1588988+jruales@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jruales <1588988+jruales@users.noreply.github.com> --- .../electron-browser/tools/runPlaywrightCodeTool.ts | 1 + .../chatInputOutputMarkdownProgressPart.ts | 7 ++++--- .../toolInvocationParts/chatToolInvocationPart.ts | 2 ++ .../contrib/chat/common/tools/languageModelToolsService.ts | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/runPlaywrightCodeTool.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/runPlaywrightCodeTool.ts index bcc288f07ea..e07efe6265d 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/runPlaywrightCodeTool.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/runPlaywrightCodeTool.ts @@ -97,6 +97,7 @@ export class RunPlaywrightCodeTool implements IToolImpl { ], toolResultDetails: { input: params.code.trim(), + inputLanguage: 'javascript', output: result.result ? [{ type: 'embed', isText: true, value: JSON.stringify(result.result, null, 2) }] : [], diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts index 32dcfbc757a..c9327e5097f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts @@ -42,6 +42,7 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS message: string | IMarkdownString, subtitle: string | IMarkdownString | undefined, input: string, + inputLanguage: string | undefined, output: IToolResultInputOutputDetails['output'] | undefined, isError: boolean, @IInstantiationService instantiationService: IInstantiationService, @@ -54,10 +55,10 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS let codeBlockIndex = codeBlockStartIndex; // Simple factory to create code part data objects - const createCodePart = (data: string): IChatCollapsibleIOCodePart => ({ + const createCodePart = (data: string, languageId = 'json'): IChatCollapsibleIOCodePart => ({ kind: 'code', data, - languageId: 'json', + languageId, codeBlockIndex: codeBlockIndex++, ownerMarkdownPartId: this.codeblocksPartId, options: { @@ -82,7 +83,7 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS subtitle, this.getAutoApproveMessageContent(), context, - createCodePart(input), + createCodePart(input, inputLanguage), processedOutput && processedOutput.length > 0 ? { parts: processedOutput.map((o, i): ChatCollapsibleIOPart => { const permalinkBasename = o.type === 'ref' || o.uri diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts index 246c35d5b13..eacf8b39b11 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -226,6 +226,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, this.toolInvocation.originMessage, resultDetails.input, + resultDetails.inputLanguage, resultDetails.output, !!resultDetails.isError, ); @@ -241,6 +242,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa this.toolInvocation.originMessage, typeof this.toolInvocation.toolSpecificData.rawInput === 'string' ? this.toolInvocation.toolSpecificData.rawInput : JSON.stringify(this.toolInvocation.toolSpecificData.rawInput, null, 2), undefined, + undefined, false, ); } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index ed8bc5bfebc..93be052e04b 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -241,6 +241,8 @@ export type ToolInputOutputReference = ToolInputOutputBase & { type: 'ref'; uri: export interface IToolResultInputOutputDetails { readonly input: string; + /** Language identifier for syntax highlighting the input. Defaults to 'json'. */ + readonly inputLanguage?: string; readonly output: (ToolInputOutputEmbedded | ToolInputOutputReference)[]; readonly isError?: boolean; /** Raw MCP tool result for MCP App UI rendering */ From f36dd9fa16457a3569ebda5b5f9274774ec9543c Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:36:17 -0700 Subject: [PATCH 371/448] Add experiment mode to terminal sandbox (#300259) --- .../common/terminalChatAgentToolsConfiguration.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index aafc4af0856..f50dfea219f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -519,6 +519,9 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Mon, 9 Mar 2026 15:41:31 -0700 Subject: [PATCH 372/448] feat: enhance file attachment rendering with icons and language support --- .../chat/browser/newChatContextAttachments.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts index a7440dfc691..5b5234c961e 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts @@ -18,11 +18,14 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { FileKind, IFileService } from '../../../../platform/files/common/files.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; import { basename } from '../../../../base/common/resources.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -73,6 +76,8 @@ export class NewChatContextAttachments extends Disposable { @ISearchService private readonly searchService: ISearchService, @IConfigurationService private readonly configurationService: IConfigurationService, @IOpenerService private readonly openerService: IOpenerService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, ) { super(); } @@ -98,17 +103,27 @@ export class NewChatContextAttachments extends Disposable { } this._container.style.display = ''; + this._container.classList.add('show-file-icons'); for (const entry of this._attachedContext) { const pill = dom.append(this._container, dom.$('.sessions-chat-attachment-pill')); pill.tabIndex = 0; pill.role = 'button'; - const icon = entry.kind === 'image' ? Codicon.fileMedia : entry.kind === 'directory' ? Codicon.folder : Codicon.file; - dom.append(pill, renderIcon(icon)); + const resource = URI.isUri(entry.value) ? entry.value : isLocation(entry.value) ? entry.value.uri : undefined; + if (entry.kind === 'image') { + dom.append(pill, renderIcon(Codicon.fileMedia)); + } else if (entry.kind === 'directory') { + const iconSpan = dom.$('span'); + iconSpan.classList.add(...getIconClasses(this.modelService, this.languageService, resource, FileKind.FOLDER)); + dom.append(pill, iconSpan); + } else { + const iconSpan = dom.$('span'); + iconSpan.classList.add(...getIconClasses(this.modelService, this.languageService, resource, FileKind.FILE)); + dom.append(pill, iconSpan); + } dom.append(pill, dom.$('span.sessions-chat-attachment-name', undefined, entry.name)); // Click to open the resource - const resource = URI.isUri(entry.value) ? entry.value : isLocation(entry.value) ? entry.value.uri : undefined; if (resource) { pill.style.cursor = 'pointer'; this._renderDisposables.add(registerOpenEditorListeners(pill, async () => { @@ -390,7 +405,7 @@ export class NewChatContextAttachments extends Disposable { return searchResult.results.map(result => ({ label: basename(result.resource), description: this.labelService.getUriLabel(result.resource, { relative: true }), - iconClass: ThemeIcon.asClassName(Codicon.file), + iconClasses: getIconClasses(this.modelService, this.languageService, result.resource, FileKind.FILE), id: result.resource.toString(), } satisfies IQuickPickItem)); } catch { @@ -434,7 +449,7 @@ export class NewChatContextAttachments extends Disposable { picks.push({ label: child.name, description: this.labelService.getUriLabel(child.resource, { relative: true }), - iconClass: ThemeIcon.asClassName(Codicon.file), + iconClasses: getIconClasses(this.modelService, this.languageService, child.resource, FileKind.FILE), id: child.resource.toString(), }); } From ec99a8f391aae387e464d935e48fa91920a00636 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 15:44:37 -0700 Subject: [PATCH 373/448] feat: add repository grouping option for agent sessions --- .../agentSessions/agentSessionsControl.ts | 6 +- .../agentSessions/agentSessionsFilter.ts | 78 ++++++++++++++++++- .../agentSessions/agentSessionsModel.ts | 2 +- .../agentSessions/agentSessionsViewer.ts | 35 +++++++++ 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 9fba2ec928c..d1c1457baa6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -179,7 +179,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo private static readonly SECTION_COLLAPSE_STATE_KEY = 'agentSessions.sectionCollapseState'; - private getSavedCollapseState(section: AgentSessionSection): boolean | undefined { + private getSavedCollapseState(section: string): boolean | undefined { const raw = this.storageService.get(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, StorageScope.PROFILE); if (raw) { try { @@ -194,7 +194,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo return undefined; } - private saveSectionCollapseState(section: AgentSessionSection, collapsed: boolean): void { + private saveSectionCollapseState(section: string, collapsed: boolean): void { let state: Record = {}; const raw = this.storageService.get(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, StorageScope.PROFILE); if (raw) { @@ -227,7 +227,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo return true; // Archived section is collapsed when archived are excluded } if (this.options.collapseOlderSections?.()) { - const olderSections = [AgentSessionSection.Week, AgentSessionSection.Older, AgentSessionSection.Archived]; + const olderSections: string[] = [AgentSessionSection.Week, AgentSessionSection.Older, AgentSessionSection.Archived]; if (olderSections.includes(element.section)) { return true; // Collapse older time sections if option is enabled } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index c6cabc14a0d..8c464b098d8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -17,7 +17,8 @@ import { IAgentSessionsFilter, IAgentSessionsFilterExcludes } from './agentSessi export enum AgentSessionsGrouping { Capped = 'capped', - Date = 'date' + Date = 'date', + Repository = 'repository' } export interface IAgentSessionsFilterOptions extends Partial { @@ -54,12 +55,15 @@ const DEFAULT_EXCLUDES: IAgentSessionsFilterExcludes = Object.freeze({ export class AgentSessionsFilter extends Disposable implements Required { private readonly STORAGE_KEY = `agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu`; + private readonly GROUPING_STORAGE_KEY = `agentSessions.grouping`; private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; readonly limitResults = () => this.options.limitResults?.(); - readonly groupResults = () => this.options.groupResults?.(); + readonly groupResults = () => this.groupingOverride ?? this.options.groupResults?.(); + + private groupingOverride: AgentSessionsGrouping | undefined; private excludes = DEFAULT_EXCLUDES; private isStoringExcludes = false; @@ -74,6 +78,7 @@ export class AgentSessionsFilter extends Disposable implements Required this.updateFilterActions())); this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.STORAGE_KEY, this._store)(() => this.updateExcludes(true))); + this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.GROUPING_STORAGE_KEY, this._store)(() => this.updateGrouping(true))); } private updateExcludes(fromEvent: boolean): void { @@ -106,6 +112,31 @@ export class AgentSessionsFilter extends Disposable implements Required(); + const noRepoLabel = localize('agentSessions.noRepository', "Other"); + + for (const session of sortedSessions) { + const badge = session.badge; + let repoName: string; + if (badge) { + repoName = typeof badge === 'string' ? badge : renderAsPlaintext(new MarkdownString(badge.value)); + } else { + repoName = noRepoLabel; + } + + let group = repoMap.get(repoName); + if (!group) { + group = []; + repoMap.set(repoName, group); + } + group.push(session); + } + + const result: AgentSessionListItem[] = []; + for (const [repoName, sessions] of repoMap) { + result.push({ + section: `repo-${repoName}`, + label: repoName, + sessions, + }); + } + + return result; + } } export const AgentSessionSectionLabels = { From 783a49347958176c42aeb87bb0d3c5cc45c2d8be Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:53:16 -0700 Subject: [PATCH 374/448] Merging copilot/popular-wallaby to main (#300265) Exclude Claude hook file paths from sessions app Disable the Claude-specific hook file locations (.claude/settings.json, .claude/settings.local.json, ~/.claude/settings.json) in the sessions window by setting them to false in the chat.hookFilesLocations config default. This uses the existing PromptsConfig mechanism for disabling individual default paths. Fixes microsoft/vscode#300138 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../configuration/browser/configuration.contribution.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 7bb224f596d..22e2a3bd28d 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -9,6 +9,11 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; Registry.as(Extensions.Configuration).registerDefaultConfigurations([{ overrides: { 'chat.experimentalSessionsWindowOverride': true, + 'chat.hookFilesLocations': { + '.claude/settings.local.json': false, + '.claude/settings.json': false, + '~/.claude/settings.json': false, + }, 'chat.agent.maxRequests': 1000, 'chat.customizationsMenu.userStoragePath': '~/.copilot', 'chat.viewSessions.enabled': false, From c02f8e37663fcce2a460d52a7d218f73ad26d432 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:15:14 -0700 Subject: [PATCH 375/448] Move `registerChatModelChangeListeners` onto chat service No sure what this method was doing on the sessions service. Really should be deleted entirely but moving to a better home on the chat service as a first step --- .../api/browser/mainThreadChatSessions.ts | 3 +- .../localAgentSessionsController.ts | 2 +- .../chatSessions/chatSessions.contribution.ts | 44 +------------------ .../chat/common/chatService/chatService.ts | 7 ++- .../common/chatService/chatServiceImpl.ts | 40 ++++++++++++++++- .../chat/common/chatSessionsService.ts | 1 - .../localAgentSessionsController.test.ts | 26 +++++++++-- .../common/chatService/mockChatService.ts | 20 +++++++++ .../test/common/mockChatSessionsService.ts | 18 -------- 9 files changed, 91 insertions(+), 70 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 42cb2f2ddaa..522e1afba19 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -449,8 +449,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat dispose: () => disposables.dispose(), }); - disposables.add(this._chatSessionsService.registerChatModelChangeListeners( - this._chatService, + disposables.add(this._chatService.registerChatModelChangeListeners( chatSessionType, () => controller.fireOnDidChangeChatSessionItems() )); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts index 18d0bcb1c29..569dffd129c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts @@ -57,7 +57,7 @@ export class LocalAgentsSessionsController extends Disposable implements IChatSe this._onDidChangeChatSessionItems.fire(); }; - this._register(this.chatSessionsService.registerChatModelChangeListeners(this.chatService, Schemas.vscodeLocalChatSession, refreshItems)); + this._register(this.chatService.registerChatModelChangeListeners(Schemas.vscodeLocalChatSession, refreshItems)); this._register(this.chatService.onDidDisposeSession(e => { const session = e.sessionResource.filter(resource => getChatSessionType(resource) === this.chatSessionType); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 4bf43903031..766b321d55b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -8,7 +8,7 @@ import { raceCancellationError } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { AsyncEmitter, Emitter, Event } from '../../../../../base/common/event.js'; -import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; import * as resources from '../../../../../base/common/resources.js'; @@ -37,7 +37,7 @@ import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; import { IChatModel } from '../../common/model/chatModel.js'; import { IChatService, IChatToolInvocation } from '../../common/chatService/chatService.js'; -import { autorun, autorunIterableDelta, observableFromEvent, observableSignalFromEvent } from '../../../../../base/common/observable.js'; +import { autorun, observableFromEvent } from '../../../../../base/common/observable.js'; import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; @@ -915,46 +915,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ }; } - public registerChatModelChangeListeners( - chatService: IChatService, - chatSessionType: string, - onChange: () => void - ): IDisposable { - const disposableStore = new DisposableStore(); - const chatModelsICareAbout = chatService.chatModels.map(models => - Array.from(models).filter((model: IChatModel) => model.sessionResource.scheme === chatSessionType) - ); - - const listeners = new ResourceMap(); - const autoRunDisposable = autorunIterableDelta( - reader => chatModelsICareAbout.read(reader), - ({ addedValues, removedValues }) => { - removedValues.forEach((removed) => { - const listener = listeners.get(removed.sessionResource); - if (listener) { - listeners.delete(removed.sessionResource); - listener.dispose(); - } - }); - addedValues.forEach((added) => { - const requestChangeListener = added.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange)); - const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', added.onDidChange); - listeners.set(added.sessionResource, autorun(reader => { - requestChangeListener.read(reader)?.read(reader); - modelChangeListener.read(reader); - onChange(); - })); - }); - } - ); - disposableStore.add(toDisposable(() => { - for (const listener of listeners.values()) { listener.dispose(); } - })); - disposableStore.add(autoRunDisposable); - return disposableStore; - } - - public getInProgressSessionDescription(chatModel: IChatModel): string | undefined { const requests = chatModel.getRequests(); if (requests.length === 0) { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 733e7769c19..309ed397d88 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -8,7 +8,7 @@ import { DeferredPromise } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { DisposableStore, IReference } from '../../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable, IReference } from '../../../../../base/common/lifecycle.js'; import { autorun, autorunSelfDisposable, IObservable, IReader } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { hasKey } from '../../../../../base/common/types.js'; @@ -1470,6 +1470,11 @@ export interface IChatService { readonly requestInProgressObs: IObservable; + /** + * @deprecated + */ + registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable; + /** * For tests only! */ diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 6aab2cb9598..598a45ebec2 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -10,10 +10,10 @@ import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common import { Emitter, Event } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../../base/common/iterator.js'; -import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { revive } from '../../../../../base/common/marshalling.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { autorun, autorunIterableDelta, derived, IObservable, ISettableObservable, observableSignalFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { StopWatch } from '../../../../../base/common/stopwatch.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -54,6 +54,7 @@ import { ILanguageModelToolsService } from '../tools/languageModelToolsService.j import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; import { IPromptsService } from '../promptSyntax/service/promptsService.js'; import { ChatRequestHooks, mergeHooks } from '../promptSyntax/hookSchema.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; const serializedChatKey = 'interactive.sessions'; @@ -1613,4 +1614,39 @@ export class ChatService extends Disposable implements IChatService { } return localSessionId; } + + public registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { + const disposableStore = new DisposableStore(); + const chatModelsICareAbout = this.chatModels.map(models => + Array.from(models).filter((model: IChatModel) => model.sessionResource.scheme === chatSessionType) + ); + + const listeners = new ResourceMap(); + const autoRunDisposable = autorunIterableDelta( + reader => chatModelsICareAbout.read(reader), + ({ addedValues, removedValues }) => { + removedValues.forEach((removed) => { + const listener = listeners.get(removed.sessionResource); + if (listener) { + listeners.delete(removed.sessionResource); + listener.dispose(); + } + }); + addedValues.forEach((added) => { + const requestChangeListener = added.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange)); + const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', added.onDidChange); + listeners.set(added.sessionResource, autorun(reader => { + requestChangeListener.read(reader)?.read(reader); + modelChangeListener.read(reader); + onChange(); + })); + }); + } + ); + disposableStore.add(toDisposable(() => { + for (const listener of listeners.values()) { listener.dispose(); } + })); + disposableStore.add(autoRunDisposable); + return disposableStore; + } } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 7d4396c392b..d693e5b34b7 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -298,7 +298,6 @@ export interface IChatSessionsService { readonly onRequestNotifyExtension: Event; notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise; - registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable; getInProgressSessionDescription(chatModel: IChatModel): string | undefined; /** diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index ae6d57e0a80..344a7b7238a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; @@ -212,6 +212,26 @@ class MockChatService implements IChatService { getMetadataForSession(sessionResource: URI): Promise { throw new Error('Method not implemented.'); } + + + private onChange?: () => void; + + registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { + // Store the emitter so tests can trigger it + this.onChange = onChange; + return { + dispose: () => { + this.onChange = undefined; + } + }; + } + + // Helper method for tests to trigger progress events + triggerProgressEvent(): void { + if (this.onChange) { + this.onChange(); + } + } } function createMockChatModel(options: { @@ -767,7 +787,7 @@ suite('LocalAgentsSessionsController', () => { })); // Simulate progress change by triggering the progress listener - mockChatSessionsService.triggerProgressEvent(); + mockChatService.triggerProgressEvent(); assert.strictEqual(changeEventCount, 1); }); @@ -793,7 +813,7 @@ suite('LocalAgentsSessionsController', () => { })); // Simulate progress change by triggering the progress listener - mockChatSessionsService.triggerProgressEvent(); + mockChatService.triggerProgressEvent(); assert.strictEqual(changeEventCount, 1); }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index 4911cfe126c..e8fb7d438c2 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -12,6 +12,7 @@ import { IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableC import { IParsedChatRequest } from '../../../common/requestParser/chatParserTypes.js'; import { ChatRequestQueueKind, ChatSendResult, IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../common/constants.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; export class MockChatService implements IChatService { chatModels: IObservable> = observableValue('chatModels', []); @@ -164,4 +165,23 @@ export class MockChatService implements IChatService { getMetadataForSession(sessionResource: URI): Promise { throw new Error('Method not implemented.'); } + + private onChange?: () => void; + + registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { + // Store the emitter so tests can trigger it + this.onChange = onChange; + return { + dispose: () => { + this.onChange = undefined; + } + }; + } + + // Helper method for tests to trigger progress events + triggerProgressEvent(): void { + if (this.onChange) { + this.onChange(); + } + } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index da463673ef1..68f264c5322 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -11,7 +11,6 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/participants/chatAgents.js'; import { IChatModel } from '../../common/model/chatModel.js'; -import { IChatService } from '../../common/chatService/chatService.js'; import { IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { Target } from '../../common/promptSyntax/promptTypes.js'; @@ -47,7 +46,6 @@ export class MockChatSessionsService implements IChatSessionsService { private optionGroups = new Map(); private sessionOptions = new ResourceMap>(); private inProgress = new Map(); - private onChange = () => { }; // For testing: allow triggering events fireDidChangeItemsProviders(event: { chatSessionType: string }): void { @@ -232,20 +230,4 @@ export class MockChatSessionsService implements IChatSessionsService { registerSessionResourceAlias(_untitledResource: URI, _realResource: URI): void { // noop } - - registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable { - // Store the emitter so tests can trigger it - this.onChange = onChange; - return { - dispose: () => { - } - }; - } - - // Helper method for tests to trigger progress events - triggerProgressEvent(): void { - if (this.onChange) { - this.onChange(); - } - } } From c3788b7bbeb9339a66df13c71f275bc53f6cc6aa Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 9 Mar 2026 16:36:39 -0700 Subject: [PATCH 376/448] plugins/mcp: allow disabling/enabling similar to extensions (#300273) * plugins/mcp: allow disabling/enabling similar to extensions Introduces an EnablementModel which is used to allow users to enable and disable both plugins and MCP at both a workspace and global level. Accessible on the mcp/plugins editors, inline within the marketplace view, and in the chat customizations view. * comments --- .../browser/aiCustomizationOverviewView.ts | 2 +- .../sessions/browser/customizationCounts.ts | 2 +- .../customizationsToolbar.contribution.ts | 4 +- .../aiCustomizationShortcutsWidget.fixture.ts | 1 - .../chat/browser/agentPluginActions.ts | 276 ++++++++++++++++++ .../agentPluginEditor/agentPluginEditor.ts | 127 +++----- .../contrib/chat/browser/agentPluginsView.ts | 143 +-------- .../customizationGroupHeaderRenderer.ts | 99 +++++++ .../browser/aiCustomization/mcpListWidget.ts | 96 +----- .../media/aiCustomizationManagement.css | 9 + .../aiCustomization/pluginListWidget.ts | 182 ++---------- .../contrib/chat/browser/enablementActions.ts | 60 ++++ .../chat/browser/enablementStatusWidget.ts | 74 +++++ .../contrib/chat/common/enablement.ts | 142 +++++++++ .../chat/common/plugins/agentPluginService.ts | 9 +- .../common/plugins/agentPluginServiceImpl.ts | 98 ++----- .../service/promptsServiceImpl.ts | 11 +- .../computeAutomaticInstructions.test.ts | 3 +- .../service/promptsService.test.ts | 13 +- .../contrib/mcp/browser/mcpCommands.ts | 29 +- .../contrib/mcp/browser/mcpServerActions.ts | 177 +++++++++++ .../contrib/mcp/browser/mcpServerEditor.ts | 4 +- .../mcp/browser/mcpWorkbenchService.ts | 34 ++- .../common/discovery/pluginMcpDiscovery.ts | 4 + .../mcpLanguageModelToolContribution.ts | 6 + .../workbench/contrib/mcp/common/mcpServer.ts | 5 + .../contrib/mcp/common/mcpService.ts | 18 +- .../workbench/contrib/mcp/common/mcpTypes.ts | 9 +- .../mcpGatewayToolBrokerChannel.test.ts | 3 + .../test/common/mcpResourceFilesystem.test.ts | 5 +- .../contrib/mcp/test/common/testMcpService.ts | 9 + 31 files changed, 1080 insertions(+), 574 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/agentPluginActions.ts create mode 100644 src/vs/workbench/contrib/chat/browser/aiCustomization/customizationGroupHeaderRenderer.ts create mode 100644 src/vs/workbench/contrib/chat/browser/enablementActions.ts create mode 100644 src/vs/workbench/contrib/chat/browser/enablementStatusWidget.ts create mode 100644 src/vs/workbench/contrib/chat/common/enablement.ts diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index 30d034ce089..2bcd3717a81 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -193,7 +193,7 @@ export class AICustomizationOverviewView extends ViewPane { const pluginSection = this.sections.find(s => s.id === AICustomizationManagementSection.Plugins); if (pluginSection) { this._register(autorun(reader => { - const plugins = this.agentPluginService.allPlugins.read(reader); + const plugins = this.agentPluginService.plugins.read(reader); pluginSection.count = plugins.length; this.updateCountElements(); })); diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index 4f6c1951b51..88d932c7ee5 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -149,6 +149,6 @@ export async function getCustomizationTotalCount( return getSourceCounts(promptsService, type, filter, workspaceContextService, workspaceService) .then(counts => getSourceCountsTotal(counts, filter)); })); - const pluginCount = agentPluginService?.allPlugins.get().length ?? 0; + const pluginCount = agentPluginService?.plugins.get().length ?? 0; return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length + pluginCount; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index 350a9fa19b8..07c6cf93903 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -163,7 +163,7 @@ export class CustomizationLinkViewItem extends ActionViewItem { this._updateCounts(); })); this._viewItemDisposables.add(autorun(reader => { - this._agentPluginService.allPlugins.read(reader); + this._agentPluginService.plugins.read(reader); this._updateCounts(); })); this._viewItemDisposables.add(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateCounts())); @@ -198,7 +198,7 @@ export class CustomizationLinkViewItem extends ActionViewItem { const total = this._mcpService.servers.get().length; this._renderTotalCount(this._countContainer, total); } else if (this._config.isPlugins) { - const total = this._agentPluginService.allPlugins.get().length; + const total = this._agentPluginService.plugins.get().length; this._renderTotalCount(this._countContainer, total); } } diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts index 6fc39f22725..0f4a7bf7a28 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts @@ -194,7 +194,6 @@ function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: reg.defineInstance(IWorkspaceContextService, createMockWorkspaceContextService()); reg.defineInstance(IAgentPluginService, new class extends mock() { override readonly plugins = observableValue('mockPlugins', []); - override readonly allPlugins = observableValue('mockAllPlugins', []); }()); // Additional services needed by CustomizationLinkViewItem reg.defineInstance(ILanguageModelsService, new class extends mock() { diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginActions.ts b/src/vs/workbench/contrib/chat/browser/agentPluginActions.ts new file mode 100644 index 00000000000..29d788865e8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentPluginActions.ts @@ -0,0 +1,276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action, IAction, IActionChangeEvent } from '../../../../base/common/actions.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; +import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { dirname, joinPath } from '../../../../base/common/resources.js'; +import { ContributionEnablementState, IEnablementModel, isContributionDisabled, isContributionEnabled } from '../common/enablement.js'; +import { IAgentPlugin, IAgentPluginService } from '../common/plugins/agentPluginService.js'; +import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; +import { IMarketplacePluginItem } from './agentPluginEditor/agentPluginItems.js'; +import { buildEnablementContextMenuGroup } from './enablementActions.js'; +import { hasKey } from '../../../../base/common/types.js'; + +//#region Simple actions + +export class InstallPluginAction extends Action { + constructor( + item: IMarketplacePluginItem, + @IPluginInstallService pluginInstallService: IPluginInstallService, + ) { + super('agentPlugin.install', localize('install', "Install"), 'extension-action label prominent install', true, + () => pluginInstallService.installPlugin({ + name: item.name, + description: item.description, + version: '', + source: item.source, + sourceDescriptor: item.sourceDescriptor, + marketplace: item.marketplace, + marketplaceReference: item.marketplaceReference, + marketplaceType: item.marketplaceType, + readmeUri: item.readmeUri, + })); + } +} + +export class UninstallPluginAction extends Action { + constructor(plugin: IAgentPlugin) { + super('agentPlugin.uninstall', localize('uninstall', "Uninstall"), 'extension-action label uninstall', true, + () => { plugin.remove(); return Promise.resolve(); }); + } +} + +export class OpenPluginFolderAction extends Action { + constructor( + plugin: IAgentPlugin, + @ICommandService commandService: ICommandService, + @IOpenerService openerService: IOpenerService, + ) { + super('agentPlugin.openFolder', localize('openPluginFolder', "Open Plugin Folder"), undefined, true, + async () => { + try { + await commandService.executeCommand('revealFileInOS', plugin.uri); + } catch { + await openerService.open(dirname(plugin.uri)); + } + }); + } +} + +export class OpenPluginReadmeAction extends Action { + constructor( + readmeUri: import('../../../../base/common/uri.js').URI, + @IOpenerService openerService: IOpenerService, + ) { + super('agentPlugin.openReadme', localize('openReadme', "Open README"), undefined, true, + () => openerService.open(readmeUri)); + } +} + +//#endregion + +//#region Context menu + +/** + * Builds the standard context menu action groups for an installed plugin. + */ +export function getInstalledPluginContextMenuActions(plugin: IAgentPlugin, instantiationService: IInstantiationService): IAction[][] { + return instantiationService.invokeFunction(accessor => { + const agentPluginService = accessor.get(IAgentPluginService); + const workspaceService = accessor.get(IWorkspaceContextService); + const groups: IAction[][] = []; + groups.push(buildEnablementContextMenuGroup( + plugin.enablement.get(), + plugin.uri.toString(), + agentPluginService.enablementModel, + workspaceService, + 'agentPlugin', + )); + groups.push([ + instantiationService.createInstance(OpenPluginFolderAction, plugin), + instantiationService.createInstance(OpenPluginReadmeAction, joinPath(plugin.uri, 'README.md')), + ]); + if (plugin.fromMarketplace) { + groups.push([new UninstallPluginAction(plugin)]); + } + return groups; + }); +} + +//#endregion + +//#region Dropdown enablement actions for editor-style action bars + +/** + * Sub-action base class that auto-hides when disabled, for use inside + * {@link EnablementDropDownAction}. + */ +class EnablementSubAction extends Action { + private _hidden: boolean; + get hidden(): boolean { return this._hidden; } + set hidden(v: boolean) { this._hidden = v; } + + constructor(id: string, label: string, cssClass: string, enabled: boolean, actionCallback: () => Promise) { + super(id, label, cssClass, enabled, actionCallback); + this._hidden = !enabled; + } + + protected override _setEnabled(value: boolean): void { + super._setEnabled(value); + this.hidden = !value; + } +} + +interface IEnablementActionChangeEvent extends IActionChangeEvent { + readonly menuActions?: IAction[]; +} + +/** + * Dropdown action that aggregates enablement sub-actions and shows the + * first visible one as the primary button, with others in the dropdown. + * Hides itself entirely when all sub-actions are hidden. + */ +export class EnablementDropDownAction extends Action { + readonly menuActionClassNames = ['extension-action', 'label', 'action-dropdown']; + private _menuActions: IAction[] = []; + get menuActions(): IAction[] { return [...this._menuActions]; } + + private _isHidden = false; + get isHidden(): boolean { return this._isHidden; } + + protected override readonly _onDidChange = new Emitter(); + override get onDidChange() { return this._onDidChange.event; } + + private readonly subActions: EnablementSubAction[]; + + constructor(id: string, subActions: EnablementSubAction[]) { + super(id, undefined, 'extension-action label action-dropdown'); + this.subActions = subActions; + for (const a of subActions) { + a.onDidChange(() => this._updateDropdown()); + } + this._updateDropdown(); + } + + private _updateDropdown(): void { + const visible = this.subActions.filter(a => !a.hidden); + const primary = visible[0]; + this._menuActions = visible.length > 1 ? [...visible] : []; + + if (primary) { + this._isHidden = false; + this.enabled = true; + this.label = primary.label; + this.tooltip = primary.tooltip; + } else { + this._isHidden = true; + this.enabled = false; + } + this._onDidChange.fire({ menuActions: this._menuActions }); + } + + override async run(): Promise { + const primary = this.subActions.find(a => !a.hidden); + await primary?.run(); + } + + override dispose(): void { + for (const a of this.subActions) { + a.dispose(); + } + super.dispose(); + } +} + +/** + * View item for {@link EnablementDropDownAction} that properly hides + * the dropdown chevron when there are no secondary actions. + */ +export class EnablementDropdownActionViewItem extends ActionWithDropdownActionViewItem { + constructor( + action: EnablementDropDownAction, + options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions, + contextMenuProvider: IContextMenuProvider, + ) { + super(null, action, options, contextMenuProvider); + this._register(action.onDidChange(e => { + if (hasKey(e, { menuActions: true })) { + this.updateClass(); + } + })); + } + + override render(container: HTMLElement): void { + super.render(container); + this.updateClass(); + } + + protected override updateClass(): void { + super.updateClass(); + if (this.element && this.dropdownMenuActionViewItem?.element) { + const action = this._action as EnablementDropDownAction; + this.element.classList.toggle('hide', action.isHidden); + const isMenuEmpty = action.menuActions.length === 0; + this.element.classList.toggle('empty', isMenuEmpty); + this.dropdownMenuActionViewItem.element.classList.toggle('hide', isMenuEmpty); + } + } +} + +/** + * Creates the enable dropdown action for a plugin, containing Enable + * and Enable (Workspace) sub-actions. + */ +export function createEnablePluginDropDown( + plugin: IAgentPlugin, + enablementModel: IEnablementModel, + workspaceContextService: IWorkspaceContextService, +): EnablementDropDownAction { + const key = plugin.uri.toString(); + const hasWorkspace = workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY; + + const enable = new EnablementSubAction('agentPlugin.enable', localize('enable', "Enable"), 'extension-action label prominent', + isContributionDisabled(plugin.enablement.get()), + () => { enablementModel.setEnabled(key, ContributionEnablementState.EnabledProfile); return Promise.resolve(); }); + + const enableWorkspace = new EnablementSubAction('agentPlugin.enableForWorkspace', localize('enableForWorkspace', "Enable (Workspace)"), 'extension-action label', + isContributionDisabled(plugin.enablement.get()) && hasWorkspace, + () => { enablementModel.setEnabled(key, ContributionEnablementState.EnabledWorkspace); return Promise.resolve(); }); + + return new EnablementDropDownAction('agentPlugin.enableDropdown', [enable, enableWorkspace]); +} + +/** + * Creates the disable dropdown action for a plugin, containing Disable + * and Disable (Workspace) sub-actions. + */ +export function createDisablePluginDropDown( + plugin: IAgentPlugin, + enablementModel: IEnablementModel, + workspaceContextService: IWorkspaceContextService, +): EnablementDropDownAction { + const key = plugin.uri.toString(); + const hasWorkspace = workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY; + + const disable = new EnablementSubAction('agentPlugin.disable', localize('disable', "Disable"), 'extension-action label disable', + isContributionEnabled(plugin.enablement.get()), + () => { enablementModel.setEnabled(key, ContributionEnablementState.DisabledProfile); return Promise.resolve(); }); + + const disableWorkspace = new EnablementSubAction('agentPlugin.disableForWorkspace', localize('disableForWorkspace', "Disable (Workspace)"), 'extension-action label disable', + isContributionEnabled(plugin.enablement.get()) && hasWorkspace, + () => { enablementModel.setEnabled(key, ContributionEnablementState.DisabledWorkspace); return Promise.resolve(); }); + + return new EnablementDropDownAction('agentPlugin.disableDropdown', [disable, disableWorkspace]); +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts index bca23294bf6..2f36d348165 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts @@ -5,7 +5,8 @@ import { $, Dimension, EventType, addDisposableListener, append, reset, setParentFlowTo } from '../../../../../base/browser/dom.js'; import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js'; -import { Action } from '../../../../../base/common/actions.js'; +import { IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { Action, IAction } from '../../../../../base/common/actions.js'; import * as arrays from '../../../../../base/common/arrays.js'; import { Cache, CacheResult } from '../../../../../base/common/cache.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; @@ -21,6 +22,7 @@ import { generateTokensCSSForColorMap } from '../../../../../editor/common/langu import { localize } from '../../../../../nls.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { IRequestService, asText } from '../../../../../platform/request/common/request.js'; @@ -36,7 +38,10 @@ import { IWebview, IWebviewService } from '../../../webview/browser/webview.js'; import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPluginService.js'; import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js'; import { AgentPluginEditorInput } from './agentPluginEditorInput.js'; -import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from './agentPluginItems.js'; +import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem } from './agentPluginItems.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { EnablementStatusWidget, pluginEnablementLabels } from '../enablementStatusWidget.js'; +import { InstallPluginAction, UninstallPluginAction, createEnablePluginDropDown, createDisablePluginDropDown, EnablementDropDownAction, EnablementDropdownActionViewItem } from '../agentPluginActions.js'; import './media/agentPluginEditor.css'; interface IAgentPluginEditorTemplate { @@ -44,6 +49,7 @@ interface IAgentPluginEditorTemplate { description: HTMLElement; marketplace: HTMLElement; actionBar: ActionBar; + statusContainer: HTMLElement; content: HTMLElement; header: HTMLElement; } @@ -92,6 +98,7 @@ export class AgentPluginEditor extends EditorPane { @IAgentPluginService private readonly agentPluginService: IAgentPluginService, @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, @ILabelService private readonly labelService: ILabelService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { super(AgentPluginEditor.ID, group, telemetryService, themeService, storageService); } @@ -119,10 +126,28 @@ export class AgentPluginEditor extends EditorPane { const actionsAndStatusContainer = append(details, $('.actions-status-container')); const actionBar = this._register(new ActionBar(actionsAndStatusContainer, { + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { + if (action instanceof EnablementDropDownAction) { + return new EnablementDropdownActionViewItem( + action, + { + ...options, + icon: true, + label: true, + menuActionsOrProvider: { getActions: () => action.menuActions }, + menuActionClassNames: action.menuActionClassNames, + }, + this.contextMenuService, + ); + } + return undefined; + }, focusOnlyEnabledItems: true })); actionBar.setFocusable(true); + const statusContainer = append(actionsAndStatusContainer, $('.status')); + const body = append(root, $('.body')); const content = append(body, $('.content')); content.id = generateUuid(); @@ -134,6 +159,7 @@ export class AgentPluginEditor extends EditorPane { name, marketplace, actionBar, + statusContainer, }; } @@ -191,7 +217,7 @@ export class AgentPluginEditor extends EditorPane { template.actionBar.clear(); // Read observables to subscribe to changes - const allPlugins = this.agentPluginService.allPlugins.read(reader); + const allPlugins = this.agentPluginService.plugins.read(reader); let currentItem = item; @@ -234,8 +260,8 @@ export class AgentPluginEditor extends EditorPane { return; } } else { - // Read enabled state for reactivity - stillInstalled.enabled.read(reader); + // Read enablement state for reactivity + stillInstalled.enablement.read(reader); currentItem = this.installedPluginToItem(stillInstalled); } } @@ -247,6 +273,16 @@ export class AgentPluginEditor extends EditorPane { for (const action of actions) { actionDisposables.add(action); } + + // Update enablement status widget + if (currentItem.kind === AgentPluginItemKind.Installed) { + actionDisposables.add(this.instantiationService.createInstance( + EnablementStatusWidget, + template.statusContainer, + currentItem.plugin.enablement, + pluginEnablementLabels, + )); + } })); // Open readme @@ -255,16 +291,14 @@ export class AgentPluginEditor extends EditorPane { private getItemActions(item: IAgentPluginItem): Action[] { if (item.kind === AgentPluginItemKind.Marketplace) { - return [this.instantiationService.createInstance(InstallPluginEditorAction, item)]; + return [this.instantiationService.createInstance(InstallPluginAction, item)]; } + const workspaceService = this.instantiationService.invokeFunction(a => a.get(IWorkspaceContextService)); const actions: Action[] = []; - if (item.plugin.enabled.get()) { - actions.push(this.instantiationService.createInstance(DisablePluginEditorAction, item.plugin)); - } else { - actions.push(this.instantiationService.createInstance(EnablePluginEditorAction, item.plugin)); - } - actions.push(this.instantiationService.createInstance(UninstallPluginEditorAction, item.plugin)); + actions.push(createEnablePluginDropDown(item.plugin, this.agentPluginService.enablementModel, workspaceService)); + actions.push(createDisablePluginDropDown(item.plugin, this.agentPluginService.enablementModel, workspaceService)); + actions.push(new UninstallPluginAction(item.plugin)); return actions; } @@ -501,73 +535,4 @@ export class AgentPluginEditor extends EditorPane { } } -//#region Actions - -class InstallPluginEditorAction extends Action { - static readonly ID = 'agentPlugin.editor.install'; - - constructor( - private readonly item: IMarketplacePluginItem, - @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, - ) { - super(InstallPluginEditorAction.ID, localize('install', "Install"), 'extension-action label prominent install'); - } - - override async run(): Promise { - await this.pluginInstallService.installPlugin({ - name: this.item.name, - description: this.item.description, - version: '', - source: this.item.source, - sourceDescriptor: this.item.sourceDescriptor, - marketplace: this.item.marketplace, - marketplaceReference: this.item.marketplaceReference, - marketplaceType: this.item.marketplaceType, - readmeUri: this.item.readmeUri, - }); - } -} - -class EnablePluginEditorAction extends Action { - static readonly ID = 'agentPlugin.editor.enable'; - - constructor( - private readonly plugin: IAgentPlugin, - @IAgentPluginService private readonly agentPluginService: IAgentPluginService, - ) { - super(EnablePluginEditorAction.ID, localize('enable', "Enable"), 'extension-action label prominent'); - } - - override async run(): Promise { - this.agentPluginService.setPluginEnabled(this.plugin.uri, true); - } -} - -class DisablePluginEditorAction extends Action { - static readonly ID = 'agentPlugin.editor.disable'; - - constructor( - private readonly plugin: IAgentPlugin, - @IAgentPluginService private readonly agentPluginService: IAgentPluginService, - ) { - super(DisablePluginEditorAction.ID, localize('disable', "Disable"), 'extension-action label disable'); - } - - override async run(): Promise { - this.agentPluginService.setPluginEnabled(this.plugin.uri, false); - } -} - -class UninstallPluginEditorAction extends Action { - static readonly ID = 'agentPlugin.editor.uninstall'; - - constructor(private readonly plugin: IAgentPlugin) { - super(UninstallPluginEditorAction.ID, localize('uninstall', "Uninstall"), 'extension-action label uninstall'); - } - - override async run(): Promise { - this.plugin.remove(); - } -} - //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts index 0033b0888ef..883a6cf7feb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts @@ -17,11 +17,9 @@ import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, isDispos import { ThemeIcon } from '../../../../base/common/themables.js'; import { autorun } from '../../../../base/common/observable.js'; import { IPagedModel, PagedModel } from '../../../../base/common/paging.js'; -import { dirname, joinPath } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; +import { dirname } from '../../../../base/common/resources.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -45,10 +43,12 @@ import { AbstractExtensionsListView } from '../../extensions/browser/extensionsV import { DefaultViewsContext, extensionsFilterSubMenu, IExtensionsWorkbenchService, SearchAgentPluginsContext } from '../../extensions/common/extensions.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IAgentPlugin, IAgentPluginService } from '../common/plugins/agentPluginService.js'; +import { isContributionEnabled } from '../common/enablement.js'; import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js'; import { AgentPluginEditorInput } from './agentPluginEditor/agentPluginEditorInput.js'; import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from './agentPluginEditor/agentPluginItems.js'; +import { getInstalledPluginContextMenuActions, InstallPluginAction, OpenPluginReadmeAction } from './agentPluginActions.js'; export const HasInstalledAgentPluginsContext = new RawContextKey('hasInstalledAgentPlugins', false); export const InstalledAgentPluginsViewId = 'workbench.views.agentPlugins.installed'; @@ -80,126 +80,6 @@ function marketplacePluginToItem(plugin: IMarketplacePlugin): IMarketplacePlugin //#region Actions -class InstallPluginAction extends Action { - static readonly ID = 'agentPlugin.install'; - - constructor( - private readonly item: IMarketplacePluginItem, - @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, - ) { - super(InstallPluginAction.ID, localize('install', "Install"), 'extension-action label prominent install'); - } - - override async run(): Promise { - await this.pluginInstallService.installPlugin({ - name: this.item.name, - description: this.item.description, - version: '', - source: this.item.source, - sourceDescriptor: this.item.sourceDescriptor, - marketplace: this.item.marketplace, - marketplaceReference: this.item.marketplaceReference, - marketplaceType: this.item.marketplaceType, - readmeUri: this.item.readmeUri, - }); - } -} - -class EnablePluginAction extends Action { - static readonly ID = 'agentPlugin.enable'; - - constructor( - private readonly plugin: IAgentPlugin, - @IAgentPluginService private readonly agentPluginService: IAgentPluginService, - ) { - super(EnablePluginAction.ID, localize('enable', "Enable")); - } - - override async run(): Promise { - this.agentPluginService.setPluginEnabled(this.plugin.uri, true); - } -} - -class DisablePluginAction extends Action { - static readonly ID = 'agentPlugin.disable'; - - constructor( - private readonly plugin: IAgentPlugin, - @IAgentPluginService private readonly agentPluginService: IAgentPluginService, - ) { - super(DisablePluginAction.ID, localize('disable', "Disable")); - } - - override async run(): Promise { - this.agentPluginService.setPluginEnabled(this.plugin.uri, false); - } -} - -class UninstallPluginAction extends Action { - static readonly ID = 'agentPlugin.uninstall'; - - constructor( - private readonly plugin: IAgentPlugin, - ) { - super(UninstallPluginAction.ID, localize('uninstall', "Uninstall")); - } - - override async run(): Promise { - this.plugin.remove(); - } -} - -class OpenPluginFolderAction extends Action { - static readonly ID = 'agentPlugin.openFolder'; - - constructor( - private readonly plugin: IAgentPlugin, - @ICommandService private readonly commandService: ICommandService, - @IOpenerService private readonly openerService: IOpenerService, - ) { - super(OpenPluginFolderAction.ID, localize('openPluginFolder', "Open Plugin Folder")); - } - - override async run(): Promise { - try { - await this.commandService.executeCommand('revealFileInOS', this.plugin.uri); - } catch { - // Fallback for web where 'revealFileInOS' is not available - await this.openerService.open(dirname(this.plugin.uri)); - } - } -} - -class OpenPluginReadmeAction extends Action { - static readonly ID = 'agentPlugin.openReadme'; - - constructor( - private readonly readmeUri: URI, - @IOpenerService private readonly openerService: IOpenerService, - ) { - super(OpenPluginReadmeAction.ID, localize('openReadme', "Open README")); - } - - override async run(): Promise { - await this.openerService.open(this.readmeUri); - } -} - -function getInstalledPluginContextMenuActionGroups(plugin: IAgentPlugin, instantiationService: IInstantiationService): IAction[][] { - const groups: IAction[][] = []; - if (plugin.enabled.get()) { - groups.push([instantiationService.createInstance(DisablePluginAction, plugin)]); - } else { - groups.push([instantiationService.createInstance(EnablePluginAction, plugin)]); - } - groups.push([ - instantiationService.createInstance(OpenPluginFolderAction, plugin), - instantiationService.createInstance(OpenPluginReadmeAction, joinPath(plugin.uri, 'README.md')), - ]); - groups.push([instantiationService.createInstance(UninstallPluginAction, plugin)]); - return groups; -} - class ManagePluginAction extends Action { static readonly ID = 'agentPlugin.manage'; static readonly CLASS = `extension-action icon manage ${ThemeIcon.asClassName(manageExtensionIcon)}`; @@ -311,7 +191,7 @@ class AgentPluginRenderer implements IPagedRenderer { - data.root.classList.toggle('disabled', element.kind === AgentPluginItemKind.Installed && !element.plugin.enabled.read(reader)); + data.root.classList.toggle('disabled', element.kind === AgentPluginItemKind.Installed && !isContributionEnabled(element.plugin.enablement.read(reader))); })); data.actionbar.clear(); @@ -323,7 +203,7 @@ class AgentPluginRenderer implements IPagedRenderer getInstalledPluginContextMenuActionGroups(element.plugin, this.instantiationService)); + () => getInstalledPluginContextMenuActions(element.plugin, this.instantiationService)); data.elementDisposables.push(manageAction); data.actionbar.push([manageAction], { icon: true, label: false }); } @@ -391,7 +271,10 @@ export class AgentPluginsListView extends AbstractExtensionsListView { - this.agentPluginService.plugins.read(reader); + const plugins = this.agentPluginService.plugins.read(reader); + for (const plugin of plugins) { + plugin.enablement.read(reader); + } if (this.list && this.isBodyVisible()) { this.refreshOnPluginsChangedScheduler.schedule(); } @@ -467,7 +350,7 @@ export class AgentPluginsListView extends AbstractExtensionsListView [...group, new Separator()]); if (actions.length > 0) { actions.pop(); @@ -540,8 +423,8 @@ export class AgentPluginsListView extends AbstractExtensionsListView installedPluginToItem(p, this.labelService)); + const plugins = this.agentPluginService.plugins.get(); + return plugins.map(p => installedPluginToItem(p, this.labelService)); } private async queryMarketplace(text: string): Promise { @@ -616,7 +499,7 @@ export class AgentPluginsViewsContribution extends Disposable implements IWorkbe const hasInstalledKey = HasInstalledAgentPluginsContext.bindTo(contextKeyService); this._register(autorun(reader => { - hasInstalledKey.set(agentPluginService.allPlugins.read(reader).length > 0); + hasInstalledKey.set(agentPluginService.plugins.read(reader).length > 0); })); registerAction2(AgentPluginsBrowseCommand); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationGroupHeaderRenderer.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationGroupHeaderRenderer.ts new file mode 100644 index 00000000000..2ad6d993447 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationGroupHeaderRenderer.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from '../../../../../base/browser/dom.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { IListRenderer } from '../../../../../base/browser/ui/list/list.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; + +const $ = DOM.$; + +export const CUSTOMIZATION_GROUP_HEADER_HEIGHT = 36; +export const CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; + +/** + * Common shape for a collapsible group header entry used in the + * MCP-server and plugin list widgets. + */ +export interface ICustomizationGroupHeaderEntry { + readonly type: 'group-header'; + readonly id: string; + readonly label: string; + readonly icon: ThemeIcon; + readonly count: number; + readonly isFirst: boolean; + readonly description: string; + collapsed: boolean; +} + +interface ICustomizationGroupHeaderTemplateData { + readonly container: HTMLElement; + readonly chevron: HTMLElement; + readonly icon: HTMLElement; + readonly label: HTMLElement; + readonly count: HTMLElement; + readonly infoIcon: HTMLElement; + readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; +} + +/** + * Shared renderer for collapsible group headers in the AI Customization + * list widgets (MCP servers, plugins, etc.). + */ +export class CustomizationGroupHeaderRenderer implements IListRenderer { + + constructor( + readonly templateId: string, + private readonly hoverService: IHoverService, + ) { } + + renderTemplate(container: HTMLElement): ICustomizationGroupHeaderTemplateData { + const disposables = new DisposableStore(); + const elementDisposables = new DisposableStore(); + container.classList.add('ai-customization-group-header'); + + const chevron = DOM.append(container, $('.group-chevron')); + const icon = DOM.append(container, $('.group-icon')); + const labelGroup = DOM.append(container, $('.group-label-group')); + const label = DOM.append(labelGroup, $('.group-label')); + const infoIcon = DOM.append(labelGroup, $('.group-info')); + infoIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); + const count = DOM.append(container, $('.group-count')); + + return { container, chevron, icon, label, count, infoIcon, disposables, elementDisposables }; + } + + renderElement(element: T, _index: number, templateData: ICustomizationGroupHeaderTemplateData): void { + templateData.elementDisposables.clear(); + + templateData.chevron.className = 'group-chevron'; + templateData.chevron.classList.add(...ThemeIcon.asClassNameArray(element.collapsed ? Codicon.chevronRight : Codicon.chevronDown)); + + templateData.icon.className = 'group-icon'; + templateData.icon.classList.add(...ThemeIcon.asClassNameArray(element.icon)); + + templateData.label.textContent = element.label; + templateData.count.textContent = `${element.count}`; + + templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.infoIcon, () => ({ + content: element.description, + appearance: { + compact: true, + skipFadeInAnimation: true, + } + }))); + + templateData.container.classList.toggle('collapsed', element.collapsed); + templateData.container.classList.toggle('has-previous-group', !element.isFirst); + } + + disposeTemplate(templateData: ICustomizationGroupHeaderTemplateData): void { + templateData.elementDisposables.dispose(); + templateData.disposables.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index 962e0854053..4b4f7895b41 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -17,6 +17,7 @@ import { Button } from '../../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IMcpWorkbenchService, IWorkbenchMcpServer, McpConnectionState, McpServerInstallState, IMcpService } from '../../../../contrib/mcp/common/mcpTypes.js'; +import { isContributionDisabled } from '../../common/enablement.js'; import { McpCommandIds } from '../../../../contrib/mcp/common/mcpCommandIds.js'; import { autorun } from '../../../../../base/common/observable.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; @@ -31,26 +32,17 @@ import { LocalMcpServerScope } from '../../../../services/mcp/common/mcpWorkbenc import { workspaceIcon, userIcon, extensionIcon } from './aiCustomizationIcons.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; +import { CustomizationGroupHeaderRenderer, ICustomizationGroupHeaderEntry, CUSTOMIZATION_GROUP_HEADER_HEIGHT, CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR } from './customizationGroupHeaderRenderer.js'; const $ = DOM.$; const MCP_ITEM_HEIGHT = 36; -const MCP_GROUP_HEADER_HEIGHT = 36; -const MCP_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; /** * Represents a collapsible group header in the MCP server list. */ -interface IMcpGroupHeaderEntry { - readonly type: 'group-header'; - readonly id: string; +interface IMcpGroupHeaderEntry extends ICustomizationGroupHeaderEntry { readonly scope: LocalMcpServerScope | 'builtin'; - readonly label: string; - readonly icon: ThemeIcon; - readonly count: number; - readonly isFirst: boolean; - readonly description: string; - collapsed: boolean; } /** @@ -79,7 +71,7 @@ type IMcpListEntry = IMcpGroupHeaderEntry | IMcpServerItemEntry | IMcpBuiltinIte class McpServerItemDelegate implements IListVirtualDelegate { getHeight(element: IMcpListEntry): number { if (element.type === 'group-header') { - return element.isFirst ? MCP_GROUP_HEADER_HEIGHT : MCP_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; + return element.isFirst ? CUSTOMIZATION_GROUP_HEADER_HEIGHT : CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; } if (element.type === 'server-item' && element.server.gallery && !element.server.local) { return 62; @@ -99,73 +91,6 @@ class McpServerItemDelegate implements IListVirtualDelegate { } } -interface IMcpGroupHeaderTemplateData { - readonly container: HTMLElement; - readonly chevron: HTMLElement; - readonly icon: HTMLElement; - readonly label: HTMLElement; - readonly count: HTMLElement; - readonly infoIcon: HTMLElement; - readonly disposables: DisposableStore; - readonly elementDisposables: DisposableStore; -} - -/** - * Renderer for collapsible group headers (Workspace, User). - */ -class McpGroupHeaderRenderer implements IListRenderer { - readonly templateId = 'mcpGroupHeader'; - - constructor( - private readonly hoverService: IHoverService, - ) { } - - renderTemplate(container: HTMLElement): IMcpGroupHeaderTemplateData { - const disposables = new DisposableStore(); - const elementDisposables = new DisposableStore(); - container.classList.add('ai-customization-group-header'); - - const chevron = DOM.append(container, $('.group-chevron')); - const icon = DOM.append(container, $('.group-icon')); - const labelGroup = DOM.append(container, $('.group-label-group')); - const label = DOM.append(labelGroup, $('.group-label')); - const infoIcon = DOM.append(labelGroup, $('.group-info')); - infoIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); - const count = DOM.append(container, $('.group-count')); - - return { container, chevron, icon, label, count, infoIcon, disposables, elementDisposables }; - } - - renderElement(element: IMcpGroupHeaderEntry, _index: number, templateData: IMcpGroupHeaderTemplateData): void { - templateData.elementDisposables.clear(); - - templateData.chevron.className = 'group-chevron'; - templateData.chevron.classList.add(...ThemeIcon.asClassNameArray(element.collapsed ? Codicon.chevronRight : Codicon.chevronDown)); - - templateData.icon.className = 'group-icon'; - templateData.icon.classList.add(...ThemeIcon.asClassNameArray(element.icon)); - - templateData.label.textContent = element.label; - templateData.count.textContent = `${element.count}`; - - templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.infoIcon, () => ({ - content: element.description, - appearance: { - compact: true, - skipFadeInAnimation: true, - } - }))); - - templateData.container.classList.toggle('collapsed', element.collapsed); - templateData.container.classList.toggle('has-previous-group', !element.isFirst); - } - - disposeTemplate(templateData: IMcpGroupHeaderTemplateData): void { - templateData.elementDisposables.dispose(); - templateData.disposables.dispose(); - } -} - interface IMcpServerItemTemplateData { readonly container: HTMLElement; readonly name: HTMLElement; @@ -225,12 +150,14 @@ class McpServerItemRenderer implements IListRenderer s.definition.id === element.server.id); templateData.disposables.add(autorun(reader => { + const disabled = server ? isContributionDisabled(server.enablement.read(reader)) : false; const connectionState = server?.connectionState.read(reader); - this.updateStatus(templateData.status, connectionState?.state); + templateData.container.classList.toggle('disabled', disabled); + this.updateStatus(templateData.status, disabled ? 'disabled' : connectionState?.state); })); } - private updateStatus(statusElement: HTMLElement, state: McpConnectionState.Kind | undefined): void { + private updateStatus(statusElement: HTMLElement, state: McpConnectionState.Kind | 'disabled' | undefined): void { statusElement.className = 'mcp-server-status'; if (this.workspaceService.isSessionsWindow) { @@ -240,6 +167,11 @@ class McpServerItemRenderer implements IListRenderer('mcpGroupHeader', this.hoverService); const localRenderer = this.instantiationService.createInstance(McpServerItemRenderer); const galleryRenderer = new McpGalleryItemRenderer(this.mcpWorkbenchService); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index cb14d86bc56..34fae734958 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -783,6 +783,15 @@ color: var(--vscode-badge-foreground); } +.mcp-server-item.disabled { + opacity: 0.5; +} + +.mcp-server-item .mcp-server-status.disabled { + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); +} + /* Button group for Add Server + Browse Marketplace */ .mcp-list-widget .list-button-group { display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts index bd990886dca..bec772e44fc 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -22,38 +22,30 @@ import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js'; import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Delayer } from '../../../../../base/common/async.js'; -import { IAction, Action, Separator } from '../../../../../base/common/actions.js'; +import { IAction, Separator } from '../../../../../base/common/actions.js'; import { basename, dirname } from '../../../../../base/common/resources.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPluginService.js'; +import { isContributionEnabled } from '../../common/enablement.js'; +import { getInstalledPluginContextMenuActions } from '../agentPluginActions.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plugins/pluginMarketplaceService.js'; import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js'; import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from '../agentPluginEditor/agentPluginItems.js'; import { pluginIcon } from './aiCustomizationIcons.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { CustomizationGroupHeaderRenderer, ICustomizationGroupHeaderEntry, CUSTOMIZATION_GROUP_HEADER_HEIGHT, CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR } from './customizationGroupHeaderRenderer.js'; const $ = DOM.$; const PLUGIN_ITEM_HEIGHT = 36; -const PLUGIN_GROUP_HEADER_HEIGHT = 36; -const PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; //#region Entry types /** * Represents a collapsible group header in the plugin list. */ -interface IPluginGroupHeaderEntry { - readonly type: 'group-header'; - readonly id: string; +interface IPluginGroupHeaderEntry extends ICustomizationGroupHeaderEntry { readonly group: 'enabled' | 'disabled'; - readonly label: string; - readonly icon: ThemeIcon; - readonly count: number; - readonly isFirst: boolean; - readonly description: string; - collapsed: boolean; } /** @@ -81,7 +73,7 @@ type IPluginListEntry = IPluginGroupHeaderEntry | IPluginInstalledItemEntry | IP class PluginItemDelegate implements IListVirtualDelegate { getHeight(element: IPluginListEntry): number { if (element.type === 'group-header') { - return element.isFirst ? PLUGIN_GROUP_HEADER_HEIGHT : PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; + return element.isFirst ? CUSTOMIZATION_GROUP_HEADER_HEIGHT : CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; } if (element.type === 'marketplace-item') { return 62; @@ -102,72 +94,6 @@ class PluginItemDelegate implements IListVirtualDelegate { //#endregion -//#region Group Header Renderer (reuses .ai-customization-group-header CSS) - -interface IPluginGroupHeaderTemplateData { - readonly container: HTMLElement; - readonly chevron: HTMLElement; - readonly icon: HTMLElement; - readonly label: HTMLElement; - readonly count: HTMLElement; - readonly infoIcon: HTMLElement; - readonly disposables: DisposableStore; - readonly elementDisposables: DisposableStore; -} - -class PluginGroupHeaderRenderer implements IListRenderer { - readonly templateId = 'pluginGroupHeader'; - - constructor( - private readonly hoverService: IHoverService, - ) { } - - renderTemplate(container: HTMLElement): IPluginGroupHeaderTemplateData { - const disposables = new DisposableStore(); - const elementDisposables = new DisposableStore(); - container.classList.add('ai-customization-group-header'); - - const chevron = DOM.append(container, $('.group-chevron')); - const icon = DOM.append(container, $('.group-icon')); - const labelGroup = DOM.append(container, $('.group-label-group')); - const label = DOM.append(labelGroup, $('.group-label')); - const infoIcon = DOM.append(labelGroup, $('.group-info')); - infoIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); - const count = DOM.append(container, $('.group-count')); - - return { container, chevron, icon, label, count, infoIcon, disposables, elementDisposables }; - } - - renderElement(element: IPluginGroupHeaderEntry, _index: number, templateData: IPluginGroupHeaderTemplateData): void { - templateData.elementDisposables.clear(); - - templateData.chevron.className = 'group-chevron'; - templateData.chevron.classList.add(...ThemeIcon.asClassNameArray(element.collapsed ? Codicon.chevronRight : Codicon.chevronDown)); - - templateData.icon.className = 'group-icon'; - templateData.icon.classList.add(...ThemeIcon.asClassNameArray(element.icon)); - - templateData.label.textContent = element.label; - templateData.count.textContent = `${element.count}`; - - templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.infoIcon, () => ({ - content: element.description, - appearance: { - compact: true, - skipFadeInAnimation: true, - } - }))); - - templateData.container.classList.toggle('collapsed', element.collapsed); - templateData.container.classList.toggle('has-previous-group', !element.isFirst); - } - - disposeTemplate(templateData: IPluginGroupHeaderTemplateData): void { - templateData.elementDisposables.dispose(); - templateData.disposables.dispose(); - } -} - //#endregion //#region Installed Plugin Renderer (reuses .mcp-server-item CSS) @@ -208,14 +134,15 @@ class PluginInstalledItemRenderer implements IListRenderer { - const enabled = element.item.plugin.enabled.read(reader); + const enabled = isContributionEnabled(element.item.plugin.enablement.read(reader)); + templateData.container.classList.toggle('disabled', !enabled); templateData.status.className = 'mcp-server-status'; if (enabled) { templateData.status.textContent = localize('enabled', "Enabled"); templateData.status.classList.add('running'); } else { templateData.status.textContent = localize('disabled', "Disabled"); - templateData.status.classList.add('stopped'); + templateData.status.classList.add('disabled'); } })); } @@ -307,82 +234,6 @@ class PluginMarketplaceItemRenderer implements IListRenderer { - this.agentPluginService.setPluginEnabled(this.plugin.uri, true); - } -} - -class DisablePluginAction extends Action { - constructor( - private readonly plugin: IAgentPlugin, - @IAgentPluginService private readonly agentPluginService: IAgentPluginService, - ) { - super('pluginListWidget.disable', localize('disable', "Disable")); - } - - override async run(): Promise { - this.agentPluginService.setPluginEnabled(this.plugin.uri, false); - } -} - -class OpenPluginFolderAction extends Action { - constructor( - private readonly plugin: IAgentPlugin, - @ICommandService private readonly commandService: ICommandService, - @IOpenerService private readonly openerService: IOpenerService, - ) { - super('pluginListWidget.openFolder', localize('openPluginFolder', "Open Plugin Folder")); - } - - override async run(): Promise { - try { - await this.commandService.executeCommand('revealFileInOS', this.plugin.uri); - } catch { - await this.openerService.open(dirname(this.plugin.uri)); - } - } -} - -class UninstallPluginAction extends Action { - constructor( - private readonly plugin: IAgentPlugin, - ) { - super('pluginListWidget.uninstall', localize('uninstall', "Uninstall")); - } - - override async run(): Promise { - this.plugin.remove(); - } -} - -//#endregion - //#region Helpers function installedPluginToItem(plugin: IAgentPlugin, labelService: ILabelService): IInstalledPluginItem { @@ -541,7 +392,7 @@ export class PluginListWidget extends Disposable { // Create list const delegate = new PluginItemDelegate(); - const groupHeaderRenderer = new PluginGroupHeaderRenderer(this.hoverService); + const groupHeaderRenderer = new CustomizationGroupHeaderRenderer('pluginGroupHeader', this.hoverService); const installedRenderer = new PluginInstalledItemRenderer(); const marketplaceRenderer = new PluginMarketplaceItemRenderer(this.pluginInstallService); @@ -601,7 +452,10 @@ export class PluginListWidget extends Disposable { // Listen to plugin service changes this._register(autorun(reader => { - this.agentPluginService.allPlugins.read(reader); + const plugins = this.agentPluginService.plugins.read(reader); + for (const plugin of plugins) { + plugin.enablement.read(reader); + } if (!this.browseMode) { this.refresh(); } @@ -669,7 +523,7 @@ export class PluginListWidget extends Disposable { : plugins; // Filter out already-installed plugins - const installedUris = new Set(this.agentPluginService.allPlugins.get().map(p => p.uri.toString())); + const installedUris = new Set(this.agentPluginService.plugins.get().map(p => p.uri.toString())); this.marketplaceItems = filtered .filter(p => { const expectedUri = this.pluginInstallService.getPluginInstallUri(p); @@ -711,7 +565,7 @@ export class PluginListWidget extends Disposable { private filterPlugins(): void { const query = this.searchQuery.toLowerCase().trim(); - const allPlugins = this.agentPluginService.allPlugins.get(); + const allPlugins = this.agentPluginService.plugins.get(); this.installedItems = allPlugins .map(p => installedPluginToItem(p, this.labelService)) @@ -737,8 +591,8 @@ export class PluginListWidget extends Disposable { } // Group plugins: enabled vs disabled - const enabledPlugins = this.installedItems.filter(item => item.plugin.enabled.get()); - const disabledPlugins = this.installedItems.filter(item => !item.plugin.enabled.get()); + const enabledPlugins = this.installedItems.filter(item => isContributionEnabled(item.plugin.enablement.get())); + const disabledPlugins = this.installedItems.filter(item => !isContributionEnabled(item.plugin.enablement.get())); const entries: IPluginListEntry[] = []; let isFirst = true; diff --git a/src/vs/workbench/contrib/chat/browser/enablementActions.ts b/src/vs/workbench/contrib/chat/browser/enablementActions.ts new file mode 100644 index 00000000000..4b49ed7b426 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/enablementActions.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action, IAction } from '../../../../base/common/actions.js'; +import { localize } from '../../../../nls.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { ContributionEnablementState, IEnablementModel, isContributionDisabled } from '../common/enablement.js'; + +/** + * Creates the four standard enablement actions (Enable, Enable Workspace, + * Disable, Disable Workspace) for a contribution identified by a string key. + */ +export function createEnablementActions( + key: string, + enablementModel: IEnablementModel, + idPrefix: string, +): [enable: Action, enableWorkspace: Action, disable: Action, disableWorkspace: Action] { + return [ + new Action(`${idPrefix}.enable`, localize('enable', "Enable"), undefined, true, + () => { enablementModel.setEnabled(key, ContributionEnablementState.EnabledProfile); return Promise.resolve(); }), + new Action(`${idPrefix}.enableForWorkspace`, localize('enableForWorkspace', "Enable (Workspace)"), undefined, true, + () => { enablementModel.setEnabled(key, ContributionEnablementState.EnabledWorkspace); return Promise.resolve(); }), + new Action(`${idPrefix}.disable`, localize('disable', "Disable"), undefined, true, + () => { enablementModel.setEnabled(key, ContributionEnablementState.DisabledProfile); return Promise.resolve(); }), + new Action(`${idPrefix}.disableForWorkspace`, localize('disableForWorkspace', "Disable (Workspace)"), undefined, true, + () => { enablementModel.setEnabled(key, ContributionEnablementState.DisabledWorkspace); return Promise.resolve(); }), + ]; +} + +/** + * Builds the standard enablement context-menu action group for a + * contribution. Returns either the enable or disable actions depending + * on the current state, with workspace variants included only when a + * workspace is open. + */ +export function buildEnablementContextMenuGroup( + enablementState: ContributionEnablementState, + key: string, + enablementModel: IEnablementModel, + workspaceContextService: IWorkspaceContextService, + idPrefix: string, +): IAction[] { + const hasWorkspace = workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY; + const [enable, enableWorkspace, disable, disableWorkspace] = createEnablementActions(key, enablementModel, idPrefix); + const actions: IAction[] = []; + if (isContributionDisabled(enablementState)) { + actions.push(enable); + if (hasWorkspace) { + actions.push(enableWorkspace); + } + } else { + actions.push(disable); + if (hasWorkspace) { + actions.push(disableWorkspace); + } + } + return actions; +} diff --git a/src/vs/workbench/contrib/chat/browser/enablementStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/enablementStatusWidget.ts new file mode 100644 index 00000000000..fae6918b782 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/enablementStatusWidget.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { reset } from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, autorun } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { ContributionEnablementState } from '../common/enablement.js'; + +/** + * A small reusable widget that renders an enablement status message inside + * a `.status` container, matching the style used by the extension and MCP + * server editors. The message is shown only when the contribution is + * disabled and is rendered as markdown with a theme icon prefix. + */ +export class EnablementStatusWidget extends Disposable { + + private readonly _renderDisposables = this._register(new MutableDisposable()); + + constructor( + private readonly _container: HTMLElement, + enablement: IObservable, + private readonly _labels: { + disabledProfile: string; + disabledWorkspace: string; + }, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, + ) { + super(); + this._register(autorun(reader => { + this._render(enablement.read(reader)); + })); + } + + private _render(state: ContributionEnablementState): void { + reset(this._container); + this._renderDisposables.value = undefined; + + let message: string | undefined; + if (state === ContributionEnablementState.DisabledProfile) { + message = this._labels.disabledProfile; + } else if (state === ContributionEnablementState.DisabledWorkspace) { + message = this._labels.disabledWorkspace; + } + + if (!message) { + return; + } + + const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + markdown.appendMarkdown(`$(${Codicon.info.id}) `); + markdown.appendText(message); + const rendered = this._markdownRendererService.render(markdown); + this._renderDisposables.value = rendered; + this._container.appendChild(rendered.element); + } +} + +/** Default labels for plugin enablement status. */ +export const pluginEnablementLabels = { + disabledProfile: localize('pluginDisabled', "This plugin is disabled."), + disabledWorkspace: localize('pluginDisabledWorkspace', "This plugin is disabled for this workspace."), +}; + +/** Default labels for MCP server enablement status. */ +export const mcpServerEnablementLabels = { + disabledProfile: localize('mcpServerDisabled', "This MCP server is disabled."), + disabledWorkspace: localize('mcpServerDisabledWorkspace', "This MCP server is disabled for this workspace."), +}; diff --git a/src/vs/workbench/contrib/chat/common/enablement.ts b/src/vs/workbench/contrib/chat/common/enablement.ts new file mode 100644 index 00000000000..e8cb2d26c7b --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/enablement.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IReader } from '../../../../base/common/observable.js'; +import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +export const enum ContributionEnablementState { + DisabledProfile, + DisabledWorkspace, + EnabledProfile, + EnabledWorkspace, +} + +export function isContributionEnabled(state: ContributionEnablementState): boolean { + return state === ContributionEnablementState.EnabledProfile || state === ContributionEnablementState.EnabledWorkspace; +} + +export function isContributionDisabled(state: ContributionEnablementState): boolean { + return !isContributionEnabled(state); +} + +export interface IEnablementModel { + readEnabled(key: string, reader?: IReader): ContributionEnablementState; + setEnabled(key: string, state: ContributionEnablementState): void; +} + +type EnablementMap = ReadonlyMap; + +function mapToStorage(value: EnablementMap): string { + return JSON.stringify([...value]); +} + +function mapFromStorage(value: string): EnablementMap { + const parsed = JSON.parse(value); + return new Map(Array.isArray(parsed) ? parsed : []); +} + +/** + * A reusable enablement model for string-keyed contributions. Uses + * `observableMemento` to persist enable/disable state in both profile-scoped + * and workspace-scoped storage. + * + * Resolution order: if a workspace-scoped entry exists for a key, it wins. + * Otherwise, the profile-scoped entry is used. The default (absence of any + * entry) is {@link ContributionEnablementState.EnabledProfile}. + */ +export class EnablementModel extends Disposable implements IEnablementModel { + private readonly _profileState: ObservableMemento; + private readonly _workspaceState: ObservableMemento; + + constructor( + storageKey: string, + @IStorageService storageService: IStorageService, + ) { + super(); + + const mapMemento = observableMemento({ + key: storageKey, + defaultValue: new Map(), + toStorage: mapToStorage, + fromStorage: mapFromStorage, + }); + + this._profileState = this._register( + mapMemento(StorageScope.PROFILE, StorageTarget.MACHINE, storageService) + ); + + this._workspaceState = this._register( + mapMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE, storageService) + ); + } + + readEnabled(key: string, reader?: IReader): ContributionEnablementState { + const wsMap = this._workspaceState.read(reader); + if (wsMap.has(key)) { + return wsMap.get(key)! + ? ContributionEnablementState.EnabledWorkspace + : ContributionEnablementState.DisabledWorkspace; + } + + const profileMap = this._profileState.read(reader); + if (profileMap.has(key)) { + return profileMap.get(key)! + ? ContributionEnablementState.EnabledProfile + : ContributionEnablementState.DisabledProfile; + } + + return ContributionEnablementState.EnabledProfile; + } + + setEnabled(key: string, state: ContributionEnablementState): void { + switch (state) { + case ContributionEnablementState.EnabledProfile: { + // Enabled-profile is the default: remove key from profile state, + // and also remove any workspace override. + this._deleteFromMap(this._profileState, key); + this._deleteFromMap(this._workspaceState, key); + break; + } + case ContributionEnablementState.DisabledProfile: { + // Store disabled in profile, remove workspace override. + this._setInMap(this._profileState, key, false); + this._deleteFromMap(this._workspaceState, key); + break; + } + case ContributionEnablementState.EnabledWorkspace: { + // Workspace override: always store explicitly. + this._setInMap(this._workspaceState, key, true); + break; + } + case ContributionEnablementState.DisabledWorkspace: { + // Workspace override: always store explicitly. + this._setInMap(this._workspaceState, key, false); + break; + } + } + } + + private _setInMap(memento: ObservableMemento, key: string, value: boolean): void { + const current = memento.get(); + if (current.get(key) === value) { + return; + } + const next = new Map(current); + next.set(key, value); + memento.set(next, undefined); + } + + private _deleteFromMap(memento: ObservableMemento, key: string): void { + const current = memento.get(); + if (!current.has(key)) { + return; + } + const next = new Map(current); + next.delete(key); + memento.set(next, undefined); + } +} diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index 4efdce7078e..348cf81f5f5 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -10,6 +10,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { SyncDescriptor0 } from '../../../../../platform/instantiation/common/descriptors.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMcpServerConfiguration } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { ContributionEnablementState, IEnablementModel } from '../enablement.js'; import { IHookCommand } from '../promptSyntax/hookSchema.js'; import { HookType } from '../promptSyntax/hookTypes.js'; import { IMarketplacePlugin } from './pluginMarketplaceService.js'; @@ -46,8 +47,7 @@ export interface IAgentPlugin { readonly uri: URI; /** Human-readable display name for the plugin. */ readonly label: string; - readonly enabled: IObservable; - setEnabled(enabled: boolean): void; + readonly enablement: IObservable; /** Removes this plugin from its discovery source (config or installed storage). */ remove(): void; readonly hooks: IObservable; @@ -62,13 +62,12 @@ export interface IAgentPlugin { export interface IAgentPluginService { readonly _serviceBrand: undefined; readonly plugins: IObservable; - readonly allPlugins: IObservable; - setPluginEnabled(pluginUri: URI, enabled: boolean): void; + readonly enablementModel: IEnablementModel; } export interface IAgentPluginDiscovery extends IDisposable { readonly plugins: IObservable; - start(): void; + start(enablementModel: IEnablementModel): void; } export function getCanonicalPluginCommandId(plugin: IAgentPlugin, commandName: string): string { diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 7377041a9e6..71f88c2d67f 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -9,7 +9,7 @@ import { untildify } from '../../../../../base/common/labels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { cloneAndChange } from '../../../../../base/common/objects.js'; -import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { autorun, derived, IObservable, observableValue } from '../../../../../base/common/observable.js'; import { posix, win32 @@ -34,8 +34,10 @@ import { parseClaudeHooks } from '../promptSyntax/hookClaudeCompat.js'; import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js'; import { IHookCommand } from '../promptSyntax/hookSchema.js'; import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; +import { EnablementModel, IEnablementModel } from '../enablement.js'; import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from './pluginMarketplaceService.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; const COMMAND_FILE_SUFFIX = '.md'; @@ -195,15 +197,18 @@ export class AgentPluginService extends Disposable implements IAgentPluginServic declare readonly _serviceBrand: undefined; - public readonly allPlugins: IObservable; public readonly plugins: IObservable; + public readonly enablementModel: IEnablementModel; constructor( @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, ) { super(); + this.enablementModel = this._register(new EnablementModel('agentPlugins.enablement', storageService)); + const pluginsEnabled = observableConfigValue(ChatConfiguration.PluginsEnabled, true, configurationService); const discoveries: IAgentPluginDiscovery[] = []; @@ -211,28 +216,16 @@ export class AgentPluginService extends Disposable implements IAgentPluginServic const discovery = instantiationService.createInstance(descriptor); this._register(discovery); discoveries.push(discovery); - discovery.start(); + discovery.start(this.enablementModel); } - this.allPlugins = derived(read => { + this.plugins = derived(read => { if (!pluginsEnabled.read(read)) { return []; } return this._dedupeAndSort(discoveries.flatMap(d => d.plugins.read(read))); }); - - this.plugins = derived(reader => { - const all = this.allPlugins.read(reader); - return all.filter(p => p.enabled.read(reader)); - }); - } - - public setPluginEnabled(pluginUri: URI, enabled: boolean): void { - const plugin = this.allPlugins.get().find(p => p.uri.toString() === pluginUri.toString()); - if (plugin) { - plugin.setEnabled(enabled); - } } private _dedupeAndSort(plugins: readonly IAgentPlugin[]): readonly IAgentPlugin[] { @@ -253,7 +246,7 @@ export class AgentPluginService extends Disposable implements IAgentPluginServic } } -type PluginEntry = IAgentPlugin & { enabled: ISettableObservable }; +type PluginEntry = IAgentPlugin; /** * Describes a single discovered plugin source, before the shared @@ -261,10 +254,7 @@ type PluginEntry = IAgentPlugin & { enabled: ISettableObservable }; */ interface IPluginSource { readonly uri: URI; - readonly enabled: boolean; readonly fromMarketplace: IMarketplacePlugin | undefined; - /** Called when setEnabled is invoked on the plugin */ - setEnabled(value: boolean): void; /** Called when remove is invoked on the plugin */ remove(): void; } @@ -285,6 +275,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements public readonly plugins: IObservable = this._plugins; protected _discoverVersion = 0; + protected _enablementModel!: IEnablementModel; constructor( protected readonly _fileService: IFileService, @@ -295,7 +286,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements super(); } - public abstract start(): void; + public abstract start(enablementModel: IEnablementModel): void; protected async _refreshPlugins(): Promise { const version = ++this._discoverVersion; @@ -320,7 +311,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements if (!seenPluginUris.has(key)) { seenPluginUris.add(key); const adapter = await this._detectPluginFormatAdapter(source.uri); - plugins.push(this._toPlugin(source.uri, source.enabled, adapter, source.fromMarketplace, value => source.setEnabled(value), () => source.remove())); + plugins.push(this._toPlugin(source.uri, adapter, source.fromMarketplace, () => source.remove())); } } @@ -348,7 +339,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements } } - private _toPlugin(uri: URI, initialEnabled: boolean, adapter: IAgentPluginFormatAdapter, fromMarketplace: IMarketplacePlugin | undefined, setEnabledCallback: (value: boolean) => void, removeCallback: () => void): IAgentPlugin { + private _toPlugin(uri: URI, adapter: IAgentPluginFormatAdapter, fromMarketplace: IMarketplacePlugin | undefined, removeCallback: () => void): IAgentPlugin { const key = uri.toString(); const existing = this._pluginEntries.get(key); if (existing) { @@ -356,7 +347,6 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements existing.store.dispose(); this._pluginEntries.delete(key); } else { - existing.plugin.enabled.set(initialEnabled, undefined); return existing.plugin; } } @@ -367,7 +357,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements const agents = observableValue('agentPluginAgents', []); const hooks = observableValue('agentPluginHooks', []); const mcpServerDefinitions = observableValue('agentPluginMcpServerDefinitions', []); - const enabled = observableValue('agentPluginEnabled', initialEnabled); + const enablement = derived(r => this._enablementModel.readEnabled(key, r)); const commandsDir = joinPath(uri, 'commands'); const skillsDir = joinPath(uri, 'skills'); @@ -418,8 +408,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements const plugin: PluginEntry = { uri, label: fromMarketplace?.name ?? basename(uri), - enabled, - setEnabled: setEnabledCallback, + enablement, remove: removeCallback, hooks, commands, @@ -717,7 +706,8 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery this._pluginLocationsConfig = observableConfigValue>(ChatConfiguration.PluginLocations, {}, _configurationService); } - public override start(): void { + public override start(enablementModel: IEnablementModel): void { + this._enablementModel = enablementModel; const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); this._register(autorun(reader => { this._pluginLocationsConfig.read(reader); @@ -732,7 +722,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery const userHome = await this._getUserHome(); for (const [path, enabled] of Object.entries(config)) { - if (!path.trim()) { + if (!path.trim() || enabled === false) { continue; } @@ -755,9 +745,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery const configKey = path; sources.push({ uri: stat.resource, - enabled, fromMarketplace, - setEnabled: (value: boolean) => this._updatePluginPathEnabled(configKey, value), remove: () => this._removePluginPath(configKey), }); } @@ -792,44 +780,6 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery ); } - /** - * Updates the enabled state of a plugin path in the configuration, - * writing to the most specific config target where the key is defined. - */ - private _updatePluginPathEnabled(configKey: string, value: boolean): void { - const inspected = this._configurationService.inspect>(ChatConfiguration.PluginLocations); - - // Walk from most specific to least specific to find where this key is defined - const targets = [ - ConfigurationTarget.WORKSPACE_FOLDER, - ConfigurationTarget.WORKSPACE, - ConfigurationTarget.USER_LOCAL, - ConfigurationTarget.USER_REMOTE, - ConfigurationTarget.USER, - ConfigurationTarget.APPLICATION, - ]; - - for (const target of targets) { - const mapping = getConfigValueInTarget(inspected, target); - if (mapping && Object.prototype.hasOwnProperty.call(mapping, configKey)) { - this._configurationService.updateValue( - ChatConfiguration.PluginLocations, - { ...mapping, [configKey]: value }, - target, - ); - return; - } - } - - // Key not found in any target; write to USER_LOCAL as default - const current = getConfigValueInTarget(inspected, ConfigurationTarget.USER_LOCAL) ?? {}; - this._configurationService.updateValue( - ChatConfiguration.PluginLocations, - { ...current, [configKey]: value }, - ConfigurationTarget.USER_LOCAL, - ); - } - /** * Removes a plugin path from `chat.pluginLocations` in the most specific * config target where the key is defined. @@ -875,7 +825,8 @@ export class MarketplaceAgentPluginDiscovery extends AbstractAgentPluginDiscover super(fileService, pathService, logService, instantiationService); } - public override start(): void { + public override start(enablementModel: IEnablementModel): void { + this._enablementModel = enablementModel; const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); this._register(autorun(reader => { this._pluginMarketplaceService.installedPlugins.read(reader); @@ -904,16 +855,9 @@ export class MarketplaceAgentPluginDiscovery extends AbstractAgentPluginDiscover sources.push({ uri: stat.resource, - enabled: entry.enabled, fromMarketplace: entry.plugin, - setEnabled: (value: boolean) => this._pluginMarketplaceService.setInstalledPluginEnabled(entry.pluginUri, value), remove: () => { - // Always remove the metadata entry first so the plugin - // disappears from the UI immediately. this._pluginMarketplaceService.removeInstalledPlugin(entry.pluginUri); - // For non-marketplace (direct-source) plugins, also clean up the - // on-disk cache. This is best-effort — failures are logged but - // do not block removal. this._pluginRepositoryService.cleanupPluginSource(entry.plugin).catch(error => { this._logService.error('[MarketplaceAgentPluginDiscovery] Failed to clean up plugin source', error); }); 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 9599b40394a..6d38cd93e98 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -46,6 +46,7 @@ import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders import { StopWatch } from '../../../../../../base/common/stopwatch.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { getCanonicalPluginCommandId, IAgentPlugin, IAgentPluginService } from '../../plugins/agentPluginService.js'; +import { isContributionEnabled } from '../../enablement.js'; import { assertNever } from '../../../../../../base/common/assert.js'; /** @@ -249,7 +250,9 @@ export class PromptsService extends Disposable implements IPromptsService { this._register(autorun(reader => { const plugins = this.agentPluginService.plugins.read(reader); for (const plugin of plugins) { - plugin.hooks.read(reader); + if (isContributionEnabled(plugin.enablement.read(reader))) { + plugin.hooks.read(reader); + } } this._onDidPluginHooksChange.fire(); })); @@ -263,6 +266,9 @@ export class PromptsService extends Disposable implements IPromptsService { const plugins = this.agentPluginService.plugins.read(reader); const nextFiles: IPluginPromptPath[] = []; for (const plugin of plugins) { + if (!isContributionEnabled(plugin.enablement.read(reader))) { + continue; + } for (const item of getItems(plugin, reader)) { nextFiles.push({ uri: item.uri, @@ -1314,6 +1320,9 @@ export class PromptsService extends Disposable implements IPromptsService { // Collect hooks from agent plugins const plugins = this.agentPluginService.plugins.get(); for (const plugin of plugins) { + if (!isContributionEnabled(plugin.enablement.get())) { + continue; + } for (const hook of plugin.hooks.get()) { let bucket = collectedHooks.get(hook.type); if (!bucket) { 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 e4a970b5271..899d04339a4 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 @@ -186,8 +186,7 @@ suite('ComputeAutomaticInstructions', () => { instaService.stub(IAgentPluginService, { plugins: observableValue('testPlugins', []), - allPlugins: observableValue('testAllPlugins', []), - setPluginEnabled: () => { }, + enablementModel: { readEnabled: () => 2 /* EnabledProfile */, setEnabled: () => { } }, }); service = disposables.add(instaService.createInstance(PromptsService)); 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 2889362b11f..52884d9ad87 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 @@ -65,7 +65,6 @@ suite('PromptsService', () => { let testConfigService: TestConfigurationService; let fileService: IFileService; let testPluginsObservable: ISettableObservable; - let testAllPluginsObservable: ISettableObservable; let workspaceTrustService: TestWorkspaceTrustManagementService; setup(async () => { @@ -172,12 +171,10 @@ suite('PromptsService', () => { instaService.stub(IWorkspaceTrustManagementService, workspaceTrustService); testPluginsObservable = observableValue('testPlugins', []); - testAllPluginsObservable = observableValue('testAllPlugins', []); instaService.stub(IAgentPluginService, { plugins: testPluginsObservable, - allPlugins: testAllPluginsObservable, - setPluginEnabled: () => { }, + enablementModel: { readEnabled: () => 2 /* EnabledProfile */, setEnabled: () => { } }, }); service = disposables.add(instaService.createInstance(PromptsService)); @@ -3514,7 +3511,7 @@ suite('PromptsService', () => { suite('hooks', () => { const createTestPlugin = (path: string, initialHooks: readonly IAgentPluginHook[]): { plugin: IAgentPlugin; hooks: ISettableObservable } => { - const enabled = observableValue('testPluginEnabled', true); + const enablement = observableValue('testPluginEnablement', 2 /* ContributionEnablementState.EnabledProfile */); const hooks = observableValue('testPluginHooks', initialHooks); const commands = observableValue('testPluginCommands', []); const skills = observableValue('testPluginSkills', []); @@ -3525,8 +3522,7 @@ suite('PromptsService', () => { plugin: { uri: URI.file(path), label: basename(URI.file(path)), - enabled, - setEnabled: () => { }, + enablement, remove: () => { }, hooks, commands, @@ -3600,7 +3596,6 @@ suite('PromptsService', () => { }]); testPluginsObservable.set([plugin], undefined); - testAllPluginsObservable.set([plugin], undefined); const result = await service.getHooks(CancellationToken.None); assert.ok(result, 'Expected hooks result'); @@ -3622,7 +3617,6 @@ suite('PromptsService', () => { }]); testPluginsObservable.set([plugin], undefined); - testAllPluginsObservable.set([plugin], undefined); const before = await service.getHooks(CancellationToken.None); assert.ok(before, 'Expected hooks result before plugin update'); @@ -3714,7 +3708,6 @@ suite('PromptsService', () => { }]); testPluginsObservable.set([plugin], undefined); - testAllPluginsObservable.set([plugin], undefined); await workspaceTrustService.setWorkspaceTrust(false); const result = await service.getHooks(CancellationToken.None); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 204a7326b88..a6d8c584080 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -61,10 +61,11 @@ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; import { McpContextKeys } from '../common/mcpContextKeys.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; -import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js'; +import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, IMcpWorkbenchService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js'; import { McpAddConfigurationCommand, McpInstallFromManifestCommand } from './mcpCommandsAddConfiguration.js'; import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js'; import { startServerAndWaitForLiveTools } from '../common/mcpTypesUtils.js'; +import { isContributionDisabled } from '../../chat/common/enablement.js'; import './media/mcpServerAction.css'; import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js'; @@ -104,6 +105,7 @@ export class ListMcpServerCommand extends Action2 { const mcpService = accessor.get(IMcpService); const commandService = accessor.get(ICommandService); const quickInput = accessor.get(IQuickInputService); + const mcpWorkbenchService = accessor.get(IMcpWorkbenchService); type ItemType = { id: string } & IQuickPickItem; @@ -122,11 +124,16 @@ export class ListMcpServerCommand extends Action2 { { id: '$add', label: localize('mcp.addServer', 'Add Server'), description: localize('mcp.addServer.description', 'Add a new server configuration'), alwaysShow: true, iconClass: ThemeIcon.asClassName(Codicon.add) }, ...Object.values(servers).filter(s => s!.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [ { type: 'separator', label: servers![0].collection.label, id: servers![0].collection.id }, - ...servers!.map(server => ({ - id: server.definition.id, - label: server.definition.label, - description: McpConnectionState.toString(server.connectionState.read(reader)), - })), + ...servers!.map(server => { + const disabled = isContributionDisabled(server.enablement.read(reader)); + return { + id: server.definition.id, + label: server.definition.label, + description: disabled + ? localize('mcp.disabled', 'Disabled') + : McpConnectionState.toString(server.connectionState.read(reader)), + }; + }), ]), ]; @@ -153,7 +160,15 @@ export class ListMcpServerCommand extends Action2 { } else if (picked.id === '$add') { commandService.executeCommand(McpCommandIds.AddConfiguration); } else { - commandService.executeCommand(McpCommandIds.ServerOptions, picked.id); + const server = mcpService.servers.get().find(s => s.definition.id === picked.id); + if (server && isContributionDisabled(server.enablement.get())) { + const workbenchServer = mcpWorkbenchService.local.find(s => s.id === picked.id); + if (workbenchServer) { + mcpWorkbenchService.open(workbenchServer); + } + } else { + commandService.executeCommand(McpCommandIds.ServerOptions, picked.id); + } } } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index 10f62eb9c83..25152d40926 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -37,6 +37,7 @@ import { ExtensionAction } from '../../extensions/browser/extensionsActions.js'; import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js'; import Severity from '../../../../base/common/severity.js'; +import { ContributionEnablementState, isContributionDisabled, isContributionEnabled } from '../../chat/common/enablement.js'; export interface IMcpServerActionChangeEvent extends IActionChangeEvent { readonly hidden?: boolean; @@ -133,10 +134,12 @@ export class ButtonWithDropDownExtensionAction extends McpServerAction { this._onDidChange.fire({ menuActions: this._menuActions }); if (this.primaryAction) { + this.hidden = false; this.enabled = this.primaryAction.enabled; this.label = this.getLabel(this.primaryAction as ExtensionAction); this.tooltip = this.primaryAction.tooltip; } else { + this.hidden = true; this.enabled = false; } } @@ -508,6 +511,174 @@ export class UninstallAction extends McpServerAction { } } +export class EnableMcpServerGloballyAction extends McpServerAction { + + static readonly ID = 'mcpServer.enableGlobally'; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + ) { + super(EnableMcpServerGloballyAction.ID, localize('enableGlobally', "Enable"), McpServerAction.LABEL_ACTION_CLASS); + this.tooltip = localize('enableGloballyTooltip', "Enable this MCP server"); + this.update(); + } + + update(): void { + this.enabled = false; + if (!this.mcpServer?.local) { + return; + } + const server = this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id); + if (!server) { + return; + } + const enablement = server.enablement.get(); + this.enabled = isContributionDisabled(enablement); + } + + override async run(): Promise { + if (!this.mcpServer) { + return; + } + this.mcpService.enablementModel.setEnabled(this.mcpServer.id, ContributionEnablementState.EnabledProfile); + } +} + +export class EnableMcpServerForWorkspaceAction extends McpServerAction { + + static readonly ID = 'mcpServer.enableForWorkspace'; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + ) { + super(EnableMcpServerForWorkspaceAction.ID, localize('enableForWorkspace', "Enable (Workspace)"), McpServerAction.LABEL_ACTION_CLASS); + this.tooltip = localize('enableForWorkspaceTooltip', "Enable this MCP server only in this workspace"); + this.update(); + } + + update(): void { + this.enabled = false; + if (!this.mcpServer?.local) { + return; + } + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return; + } + const server = this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id); + if (!server) { + return; + } + const enablement = server.enablement.get(); + this.enabled = isContributionDisabled(enablement); + } + + override async run(): Promise { + if (!this.mcpServer) { + return; + } + this.mcpService.enablementModel.setEnabled(this.mcpServer.id, ContributionEnablementState.EnabledWorkspace); + } +} + +export class DisableMcpServerGloballyAction extends McpServerAction { + + static readonly ID = 'mcpServer.disableGlobally'; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + ) { + super(DisableMcpServerGloballyAction.ID, localize('disableGlobally', "Disable"), McpServerAction.LABEL_ACTION_CLASS); + this.tooltip = localize('disableGloballyTooltip', "Disable this MCP server"); + this.update(); + } + + update(): void { + this.enabled = false; + if (!this.mcpServer?.local) { + return; + } + const server = this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id); + if (!server) { + return; + } + const enablement = server.enablement.get(); + this.enabled = isContributionEnabled(enablement); + } + + override async run(): Promise { + if (!this.mcpServer) { + return; + } + this.mcpService.enablementModel.setEnabled(this.mcpServer.id, ContributionEnablementState.DisabledProfile); + } +} + +export class DisableMcpServerForWorkspaceAction extends McpServerAction { + + static readonly ID = 'mcpServer.disableForWorkspace'; + + constructor( + @IMcpService private readonly mcpService: IMcpService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + ) { + super(DisableMcpServerForWorkspaceAction.ID, localize('disableForWorkspace', "Disable (Workspace)"), McpServerAction.LABEL_ACTION_CLASS); + this.tooltip = localize('disableForWorkspaceTooltip', "Disable this MCP server only in this workspace"); + this.update(); + } + + update(): void { + this.enabled = false; + if (!this.mcpServer?.local) { + return; + } + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return; + } + const server = this.mcpService.servers.get().find(s => s.definition.id === this.mcpServer?.id); + if (!server) { + return; + } + const enablement = server.enablement.get(); + this.enabled = isContributionEnabled(enablement); + } + + override async run(): Promise { + if (!this.mcpServer) { + return; + } + this.mcpService.enablementModel.setEnabled(this.mcpServer.id, ContributionEnablementState.DisabledWorkspace); + } +} + +export class EnableMcpDropDownAction extends ButtonWithDropDownExtensionAction { + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super('mcpServer.enable', McpServerAction.LABEL_ACTION_CLASS, [ + [ + instantiationService.createInstance(EnableMcpServerGloballyAction), + instantiationService.createInstance(EnableMcpServerForWorkspaceAction), + ] + ]); + } +} + +export class DisableMcpDropDownAction extends ButtonWithDropDownExtensionAction { + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super('mcpServer.disable', McpServerAction.LABEL_ACTION_CLASS, [ + [ + instantiationService.createInstance(DisableMcpServerGloballyAction), + instantiationService.createInstance(DisableMcpServerForWorkspaceAction), + ] + ]); + } +} + export function getContextMenuActions(mcpServer: IWorkbenchMcpServer, isEditorAction: boolean, instantiationService: IInstantiationService): IAction[][] { return instantiationService.invokeFunction(accessor => { const workspaceService = accessor.get(IWorkspaceContextService); @@ -524,6 +695,12 @@ export function getContextMenuActions(mcpServer: IWorkbenchMcpServer, isEditorAc instantiationService.createInstance(StopServerAction), instantiationService.createInstance(RestartServerAction), ]); + groups.push([ + instantiationService.createInstance(EnableMcpServerGloballyAction), + instantiationService.createInstance(EnableMcpServerForWorkspaceAction), + instantiationService.createInstance(DisableMcpServerGloballyAction), + instantiationService.createInstance(DisableMcpServerForWorkspaceAction), + ]); groups.push([ instantiationService.createInstance(AuthServerAction), ]); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index 034985bdf44..1f0f633be66 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -38,7 +38,7 @@ import { IExtensionService } from '../../../services/extensions/common/extension import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IMcpServerContainer, IMcpServerEditorOptions, IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers, McpServerInstallState } from '../common/mcpTypes.js'; import { StarredWidget, McpServerIconWidget, McpServerStatusWidget, McpServerWidget, onClick, PublisherWidget, McpServerScopeBadgeWidget, LicenseWidget } from './mcpServerWidgets.js'; -import { ButtonWithDropDownExtensionAction, ButtonWithDropdownExtensionActionViewItem, DropDownAction, InstallAction, InstallingLabelAction, InstallInRemoteAction, InstallInWorkspaceAction, ManageMcpServerAction, McpServerStatusAction, UninstallAction } from './mcpServerActions.js'; +import { ButtonWithDropDownExtensionAction, ButtonWithDropdownExtensionActionViewItem, DisableMcpDropDownAction, DropDownAction, EnableMcpDropDownAction, InstallAction, InstallingLabelAction, InstallInRemoteAction, InstallInWorkspaceAction, ManageMcpServerAction, McpServerStatusAction, UninstallAction } from './mcpServerActions.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; import { ILocalMcpServer, IGalleryMcpServerConfiguration, IMcpServerPackage, IMcpServerKeyValueInput, RegistryType } from '../../../../platform/mcp/common/mcpManagement.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; @@ -251,6 +251,8 @@ export class McpServerEditor extends EditorPane { this.instantiationService.createInstance(InstallInRemoteAction, false) ] ]), + this.instantiationService.createInstance(EnableMcpDropDownAction), + this.instantiationService.createInstance(DisableMcpDropDownAction), this.instantiationService.createInstance(ManageMcpServerAction, true), ]; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index a87f5984006..342092dbd72 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -37,11 +37,12 @@ import { IRemoteAgentService } from '../../../services/remote/common/remoteAgent import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; import { McpServerInstallData, McpServerInstallClassification } from '../common/mcpServer.js'; import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServerEnablementStatus, McpServersGalleryStatusContext } from '../common/mcpTypes.js'; +import { ContributionEnablementState } from '../../chat/common/enablement.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; import { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js'; import { IIterativePager, IIterativePage } from '../../../../base/common/paging.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { runOnChange } from '../../../../base/common/observable.js'; +import { autorun, runOnChange } from '../../../../base/common/observable.js'; import Severity from '../../../../base/common/severity.js'; import { Queue } from '../../../../base/common/async.js'; @@ -221,6 +222,14 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this._local = this.sort(this._local); this._onChange.fire(undefined); })); + + // React to enablement changes on individual servers + this._register(autorun(reader => { + for (const server of mcpService.servers.read(reader)) { + server.enablement.read(reader); + } + this._onChange.fire(undefined); + })); } private async onDidChangeProfile() { @@ -743,10 +752,31 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return enablementStatus; } - if (!this.mcpService.servers.get().find(s => s.definition.id === mcpServer.id)) { + const server = this.mcpService.servers.get().find(s => s.definition.id === mcpServer.id); + if (!server) { return { state: McpServerEnablementState.Disabled }; } + const enablement = server.enablement.get(); + if (enablement === ContributionEnablementState.DisabledProfile) { + return { + state: McpServerEnablementState.DisabledProfile, + message: { + severity: Severity.Info, + text: new MarkdownString(localize('disabled globally', "This MCP server is disabled.")) + } + }; + } + if (enablement === ContributionEnablementState.DisabledWorkspace) { + return { + state: McpServerEnablementState.DisabledWorkspace, + message: { + severity: Severity.Info, + text: new MarkdownString(localize('disabled in workspace', "This MCP server is disabled for this workspace.")) + } + }; + } + return undefined; } diff --git a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts index 371bd107215..7cb6a6efebd 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts @@ -18,6 +18,7 @@ import { IAgentPluginMcpServerDefinition, IAgentPluginService } from '../../../chat/common/plugins/agentPluginService.js'; +import { isContributionEnabled } from '../../../chat/common/enablement.js'; import { IMcpRegistry } from '../mcpRegistryTypes.js'; import { McpCollectionSortOrder, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js'; import { IMcpDiscovery } from './mcpDiscovery.js'; @@ -39,6 +40,9 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { const plugins = this._agentPluginService.plugins.read(reader); const seen = new ResourceSet(); for (const plugin of plugins) { + if (!isContributionEnabled(plugin.enablement.read(reader))) { + continue; + } seen.add(plugin.uri); let collectionState = this._collections.get(plugin.uri); diff --git a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts index 9ef7cf37e5f..28400d12acf 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts @@ -23,6 +23,7 @@ import { mcpAppsEnabledConfig } from '../../../../platform/mcp/common/mcpManagem import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { isContributionEnabled } from '../../chat/common/enablement.js'; import { ChatResponseResource, getAttachableImageExtension } from '../../chat/common/model/chatModel.js'; import { LanguageModelPartAudience } from '../../chat/common/languageModels.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/tools/languageModelToolsService.js'; @@ -59,6 +60,11 @@ export class McpLanguageModelToolContribution extends Disposable implements IWor const toDelete = new Set(previous.keys()); for (const server of servers) { + // Skip disabled servers — don't register their tools. + if (!isContributionEnabled(server.enablement.read(reader))) { + continue; + } + const previousRec = previous.get(server); if (previousRec) { toDelete.delete(server); diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 3a1f490eb07..9f93c186e58 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -41,6 +41,7 @@ import { IMcpSandboxService } from './mcpSandboxService.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { McpTaskManager } from './mcpTaskManager.js'; import { ElicitationKind, extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPotentialSandboxBlock, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, McpToolVisibility, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js'; +import { ContributionEnablementState, IEnablementModel } from '../../chat/common/enablement.js'; import { MCP } from './modelContextProtocol.js'; import { McpApps } from './modelContextProtocolApps.js'; import { UriTemplate } from './uriTemplate.js'; @@ -424,6 +425,8 @@ export class McpServer extends Disposable implements IMcpServer { /** Count of running tool calls, used to detect if sampling is during an LM call */ public runningToolCalls = new Set(); + public readonly enablement: IObservable; + constructor( initialCollection: McpCollectionDefinition, public readonly definition: McpDefinitionReference, @@ -431,6 +434,7 @@ export class McpServer extends Disposable implements IMcpServer { private readonly _requiresExtensionActivation: boolean | undefined, private readonly _primitiveCache: McpServerMetadataCache, toolPrefix: string, + enablementModel: IEnablementModel, @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, @IWorkspaceContextService workspacesService: IWorkspaceContextService, @IExtensionService private readonly _extensionService: IExtensionService, @@ -451,6 +455,7 @@ export class McpServer extends Disposable implements IMcpServer { this.collection = initialCollection; this._fullDefinitions = this._mcpRegistry.getServerDefinition(this.collection, this.definition); + this.enablement = derived(r => enablementModel.readEnabled(definition.id, r)); this._loggerId = `mcpServer.${definition.id}`; this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` })); diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index f994ad463e1..940704772ae 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -11,7 +11,8 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js'; -import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; +import { EnablementModel, isContributionEnabled } from '../../chat/common/enablement.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServer, McpServerMetadataCache } from './mcpServer.js'; import { IAutostartResult, IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, McpToolName, UserInteractionRequiredError } from './mcpTypes.js'; @@ -29,6 +30,8 @@ export class McpService extends Disposable implements IMcpService { public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; } + public readonly enablementModel: EnablementModel; + protected readonly userCache: McpServerMetadataCache; protected readonly workspaceCache: McpServerMetadataCache; @@ -36,10 +39,13 @@ export class McpService extends Disposable implements IMcpService { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, @ILogService private readonly _logService: ILogService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, ) { super(); + this.enablementModel = this._register(new EnablementModel('mcp.enablement', storageService)); + this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE)); this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE)); @@ -96,8 +102,11 @@ export class McpService extends Disposable implements IMcpService { return; } - // don't try re-running errored servers, let the user choose if they want that - const candidates = this.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error); + // don't try re-running errored servers or disabled servers + const candidates = this.servers.get().filter(s => + s.connectionState.get().state !== McpConnectionState.Kind.Error + && isContributionEnabled(s.enablement.get()) + ); let todo = new Set(); if (autoStartConfig === McpAutoStartValue.OnlyNew) { @@ -203,6 +212,7 @@ export class McpService extends Disposable implements IMcpService { !!def.collectionDefinition.lazy, def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache, def.toolPrefix, + this.enablementModel, ); nextServers.push({ object, toolPrefix: def.toolPrefix }); diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 4c77b0a2f54..4877aa5d706 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -23,11 +23,12 @@ import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { McpGalleryManifestStatus } from '../../../../platform/mcp/common/mcpGalleryManifest.js'; -import { IGalleryMcpServer, IInstallableMcpServer, IGalleryMcpServerConfiguration, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js'; +import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js'; import { IMcpDevModeConfig, IMcpSandboxConfiguration, IMcpServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolder, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchLocalMcpServer, IWorkbencMcpServerInstallOptions } from '../../../services/mcp/common/mcpWorkbenchManagementService.js'; +import { ContributionEnablementState, IEnablementModel } from '../../chat/common/enablement.js'; import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js'; import { IMcpServerSamplingConfiguration } from './mcpConfiguration.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; @@ -245,6 +246,9 @@ export interface IMcpService { _serviceBrand: undefined; readonly servers: IObservable; + /** The enablement model for MCP servers. */ + readonly enablementModel: IEnablementModel; + /** Resets the cached tools. */ resetCaches(): void; @@ -329,6 +333,7 @@ export namespace McpServerTrust { export interface IMcpServer extends IDisposable { readonly collection: McpCollectionReference; readonly definition: McpDefinitionReference; + readonly enablement: IObservable; readonly connection: IObservable; readonly connectionState: IObservable; readonly serverMetadata: IObservable<{ @@ -742,6 +747,8 @@ export interface IMcpServerEditorOptions extends IEditorOptions { export const enum McpServerEnablementState { Disabled, DisabledByAccess, + DisabledProfile, + DisabledWorkspace, Enabled, } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index 7035bc3aa9b..e0dcab8d7f1 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -8,6 +8,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ContributionEnablementState } from '../../../chat/common/enablement.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IGatewayCallToolResult } from '../../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../common/modelContextProtocol.js'; @@ -268,6 +269,7 @@ function createServer( definition: { id: definitionId, label: definitionId }, connection: observableValue(owner, undefined), connectionState, + enablement: observableValue(owner, ContributionEnablementState.EnabledProfile), serverMetadata: observableValue(owner, undefined), readDefinitions: () => observableValue(owner, { server: undefined, collection: undefined }), showOutput: async () => { }, @@ -306,6 +308,7 @@ function createNeverStartingServer( definition: { id: definitionId, label: definitionId }, connection: observableValue(owner, undefined), connectionState, + enablement: observableValue(owner, ContributionEnablementState.EnabledProfile), serverMetadata: observableValue(owner, undefined), readDefinitions: () => observableValue(owner, { server: undefined, collection: undefined }), showOutput: async () => { }, diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test.ts index 543ab09965b..e546b5f5153 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test.ts @@ -35,9 +35,10 @@ suite('Workbench - MCP - ResourceFilesystem', () => { let fs: McpResourceFilesystem; setup(() => { + const storageService = ds.add(new TestStorageService()); const services = new ServiceCollection( [IFileService, { registerProvider: () => { } }], - [IStorageService, ds.add(new TestStorageService())], + [IStorageService, storageService], [ILoggerService, ds.add(new TestLoggerService())], [IWorkspaceContextService, new TestContextService()], [IWorkbenchEnvironmentService, {}], @@ -49,7 +50,7 @@ suite('Workbench - MCP - ResourceFilesystem', () => { const registry = new TestMcpRegistry(parentInsta1); const parentInsta2 = ds.add(parentInsta1.createChild(new ServiceCollection([IMcpRegistry, registry]))); - const mcpService = ds.add(new McpService(parentInsta2, registry, new NullLogService(), new TestConfigurationService())); + const mcpService = ds.add(new McpService(parentInsta2, registry, new NullLogService(), new TestConfigurationService(), storageService)); mcpService.updateCollectedServers(); const instaService = ds.add(parentInsta2.createChild(new ServiceCollection( diff --git a/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts b/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts index 8479338a5b3..004220e5876 100644 --- a/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts +++ b/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts @@ -4,11 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { observableValue } from '../../../../../base/common/observable.js'; +import { ContributionEnablementState, IEnablementModel } from '../../../chat/common/enablement.js'; import { IAutostartResult, IMcpServer, IMcpService, LazyCollectionState } from '../../common/mcpTypes.js'; +export class TestEnablementModel implements IEnablementModel { + readEnabled(_key: string): ContributionEnablementState { + return ContributionEnablementState.EnabledProfile; + } + setEnabled(_key: string, _state: ContributionEnablementState): void { } +} + export class TestMcpService implements IMcpService { declare readonly _serviceBrand: undefined; public servers = observableValue(this, []); + public readonly enablementModel: IEnablementModel = new TestEnablementModel(); resetCaches(): void { } From fed1cfdff23c51ef1df365e25b960ae199fea264 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 16:35:25 -0700 Subject: [PATCH 377/448] Use ResourceLabels for attachment pill icons Replace plain spans with getIconClasses() with proper ResourceLabels and label.setFile() for attachment pills, matching how core chat attachments render file icons via the monaco-icon-label system. Add CSS for .monaco-icon-label inside pills to ensure correct sizing and layout of file icon theme ::before pseudo-elements. Keep getIconClasses for QuickPick file items where the QuickPick's internal IconLabel handles rendering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/media/chatWidget.css | 23 ++++++++++++++++ .../chat/browser/newChatContextAttachments.ts | 26 +++++++++++++------ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index ef3a5d80a79..b9e7bbe6a02 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -232,6 +232,29 @@ padding: 0 3px; } +.sessions-chat-attachment-pill .monaco-icon-label { + gap: 4px; +} + +.sessions-chat-attachment-pill .monaco-icon-label::before { + height: auto; + padding: 0 0 0 2px; + line-height: 100% !important; + align-self: center; +} + +.sessions-chat-attachment-pill .monaco-icon-label .monaco-icon-label-container { + display: flex; +} + +.sessions-chat-attachment-pill .monaco-icon-label .monaco-icon-label-container .monaco-highlighted-label { + display: inline-flex; + align-items: center; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .sessions-chat-attachment-remove { display: flex; align-items: center; diff --git a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts index 5b5234c961e..9acac072639 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts @@ -23,11 +23,13 @@ import { IClipboardService } from '../../../../platform/clipboard/common/clipboa import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; import { basename } from '../../../../base/common/resources.js'; import { Schemas } from '../../../../base/common/network.js'; +import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../../../workbench/browser/labels.js'; import { IChatRequestVariableEntry, OmittedState } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { isLocation } from '../../../../editor/common/languages.js'; @@ -66,6 +68,8 @@ export class NewChatContextAttachments extends Disposable { this._onDidChangeContext.fire(); } + private readonly _resourceLabels: ResourceLabels; + constructor( @IQuickInputService private readonly quickInputService: IQuickInputService, @ITextModelService private readonly textModelService: ITextModelService, @@ -76,10 +80,12 @@ export class NewChatContextAttachments extends Disposable { @ISearchService private readonly searchService: ISearchService, @IConfigurationService private readonly configurationService: IConfigurationService, @IOpenerService private readonly openerService: IOpenerService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @ILanguageService private readonly languageService: ILanguageService, ) { super(); + this._resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); } // --- Rendering --- @@ -95,6 +101,7 @@ export class NewChatContextAttachments extends Disposable { } this._renderDisposables.clear(); + this._resourceLabels.clear(); dom.clearNode(this._container); if (this._attachedContext.length === 0) { @@ -112,16 +119,19 @@ export class NewChatContextAttachments extends Disposable { const resource = URI.isUri(entry.value) ? entry.value : isLocation(entry.value) ? entry.value.uri : undefined; if (entry.kind === 'image') { dom.append(pill, renderIcon(Codicon.fileMedia)); - } else if (entry.kind === 'directory') { - const iconSpan = dom.$('span'); - iconSpan.classList.add(...getIconClasses(this.modelService, this.languageService, resource, FileKind.FOLDER)); - dom.append(pill, iconSpan); + dom.append(pill, dom.$('span.sessions-chat-attachment-name', undefined, entry.name)); } else { - const iconSpan = dom.$('span'); - iconSpan.classList.add(...getIconClasses(this.modelService, this.languageService, resource, FileKind.FILE)); - dom.append(pill, iconSpan); + const label = this._resourceLabels.create(pill, { supportIcons: true }); + this._renderDisposables.add(label); + if (resource) { + label.setFile(resource, { + fileKind: entry.kind === 'directory' ? FileKind.FOLDER : FileKind.FILE, + hidePath: true, + }); + } else { + label.setLabel(entry.name); + } } - dom.append(pill, dom.$('span.sessions-chat-attachment-name', undefined, entry.name)); // Click to open the resource if (resource) { From f8c6fd31e93cb443f58fddb0d264c2b5df973995 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 16:39:45 -0700 Subject: [PATCH 378/448] feat: enhance session grouping by adding archived sessions and repository name extraction --- .../agentSessions/agentSessionsViewer.ts | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index ec954f53bcc..cacd112f809 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -856,17 +856,17 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou private groupSessionsByRepository(sortedSessions: IAgentSession[]): AgentSessionListItem[] { const repoMap = new Map(); + const archivedSessions: IAgentSession[] = []; const noRepoLabel = localize('agentSessions.noRepository', "Other"); for (const session of sortedSessions) { - const badge = session.badge; - let repoName: string; - if (badge) { - repoName = typeof badge === 'string' ? badge : renderAsPlaintext(new MarkdownString(badge.value)); - } else { - repoName = noRepoLabel; + if (session.isArchived()) { + archivedSessions.push(session); + continue; } + const repoName = this.getRepositoryName(session) ?? noRepoLabel; + let group = repoMap.get(repoName); if (!group) { group = []; @@ -884,8 +884,71 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou }); } + if (archivedSessions.length > 0) { + result.push({ + section: AgentSessionSection.Archived, + label: AgentSessionSectionLabels[AgentSessionSection.Archived], + sessions: archivedSessions, + }); + } + return result; } + + private getRepositoryName(session: IAgentSession): string | undefined { + const metadata = session.metadata; + if (metadata) { + // repositoryNwo: "owner/repo" + const nwo = metadata.repositoryNwo as string | undefined; + if (nwo && nwo.includes('/')) { + return nwo.split('/').pop()!; + } + + // repository: could be "owner/repo" or a URL + const repository = metadata.repository as string | undefined; + if (repository) { + if (repository.includes('/') && !repository.includes(':')) { + return repository.split('/').pop()!; + } + // Try to extract from URL like "https://github.com/owner/repo" + try { + const url = new URL(repository); + const parts = url.pathname.split('/').filter(Boolean); + if (parts.length >= 2) { + return parts[1]; + } + } catch { + // not a URL + } + } + + // repositoryUrl: "https://github.com/owner/repo" + const repositoryUrl = metadata.repositoryUrl as string | undefined; + if (repositoryUrl) { + try { + const url = new URL(repositoryUrl); + const parts = url.pathname.split('/').filter(Boolean); + if (parts.length >= 2) { + return parts[1]; + } + } catch { + // not a URL + } + } + } + + // Fallback: extract from badge (strip codicon syntax) + const badge = session.badge; + if (badge) { + const raw = typeof badge === 'string' ? badge : renderAsPlaintext(new MarkdownString(badge.value)); + const cleaned = raw.replace(/\$\([^)]+\)\s*/g, '').trim(); + if (cleaned) { + return cleaned; + } + } + + return undefined; + } } export const AgentSessionSectionLabels = { From 01669c223340ccd144e6161c8d6cdc0845b79f5e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:47:31 +0100 Subject: [PATCH 379/448] Agent feedback improvements (#300202) * agent feedback improvements * fix test --- .../browser/agentFeedbackEditorActions.ts | 8 +- .../agentFeedbackEditorWidgetContribution.ts | 33 +- .../browser/agentFeedbackService.ts | 17 +- .../media/agentFeedbackEditorOverlay.css | 2 +- .../media/agentFeedbackEditorWidget.css | 30 +- .../agentFeedbackEditorWidget.fixture.ts | 17 +- .../contrib/changes/browser/changesView.ts | 23 ++ .../changes/browser/media/changesView.css | 6 + .../browser/codeReview.contributions.ts | 17 +- .../codeReview/browser/codeReviewService.ts | 133 ++++++++ .../test/browser/codeReviewService.test.ts | 296 +++++++++++++++++- 11 files changed, 527 insertions(+), 55 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts index 2fd5134bb04..eb9a50f5b97 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts @@ -13,6 +13,7 @@ import { URI } from '../../../../base/common/uri.js'; import { isEqual } from '../../../../base/common/resources.js'; import { EditorsOrder, IEditorIdentifier } from '../../../../workbench/common/editor.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { GroupsOrder, IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; @@ -48,7 +49,12 @@ abstract class AgentFeedbackEditorAction extends Action2 { const agentSessionsService = accessor.get(IAgentSessionsService); const codeReviewService = accessor.get(ICodeReviewService); - const candidates = getActiveResourceCandidates(editorService.activeEditorPane?.input); + const editorGroupsService = accessor.get(IEditorGroupsService); + + const activePane = editorService.activeEditorPane + ?? editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(g => g.activeEditorPane)?.activeEditorPane + ?? editorService.visibleEditorPanes[0]; + const candidates = getActiveResourceCandidates(activePane?.input); for (const candidate of candidates) { const sessionResource = getSessionForResource(candidate, chatEditingService, agentSessionsService) ?? agentFeedbackService.getMostRecentSessionForResource(candidate); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index 987c39acfef..ad4b69371fc 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -31,6 +31,9 @@ import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.j import { getResourceEditorComments, getSessionEditorComments, groupNearbySessionEditorComments, ISessionEditorComment, SessionEditorCommentSource, toSessionEditorCommentId } from './sessionEditorComments.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { isEqual } from '../../../../base/common/resources.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; /** * Widget that displays agent feedback comments for a group of nearby feedback items. @@ -44,7 +47,6 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid private readonly _domNode: HTMLElement; private readonly _headerNode: HTMLElement; private readonly _titleNode: HTMLElement; - private readonly _dismissButton: HTMLElement; private readonly _toggleButton: HTMLElement; private readonly _bodyNode: HTMLElement; private readonly _itemElements = new Map(); @@ -63,6 +65,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid private readonly _sessionResource: URI, @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, @ICodeReviewService private readonly _codeReviewService: ICodeReviewService, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, ) { super(); @@ -88,12 +91,6 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid this._updateToggleButton(); this._headerNode.appendChild(this._toggleButton); - // Dismiss button - this._dismissButton = $('div.agent-feedback-widget-dismiss'); - this._dismissButton.appendChild(renderIcon(Codicon.close)); - this._dismissButton.title = nls.localize('dismiss', "Dismiss"); - this._headerNode.appendChild(this._dismissButton); - this._domNode.appendChild(this._headerNode); // Body (collapsible) — starts collapsed @@ -128,11 +125,6 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid this._toggleExpanded(); })); - // Dismiss button click - this._eventStore.add(addDisposableListener(this._dismissButton, 'click', (e) => { - e.stopPropagation(); - this._dismiss(); - })); } private _toggleExpanded(): void { @@ -143,12 +135,6 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid } } - private _dismiss(): void { - for (const comment of this._commentItems) { - this._removeComment(comment); - } - } - private _updateTitle(): void { const count = this._commentItems.length; if (count === 1) { @@ -206,7 +192,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid actionBar.push(new Action( 'agentFeedback.widget.convert', nls.localize('convertComment', "Convert to Agent Feedback"), - ThemeIcon.asClassName(Codicon.comment), + ThemeIcon.asClassName(Codicon.check), true, () => this._convertToAgentFeedback(comment), ), { icon: true, label: false }); @@ -221,8 +207,10 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid itemHeader.appendChild(actionBarContainer); item.appendChild(itemHeader); - const text = $('span.agent-feedback-widget-text'); - text.textContent = comment.text; + const text = $('div.agent-feedback-widget-text'); + const rendered = this._markdownRendererService.render(new MarkdownString(comment.text)); + this._eventStore.add(rendered); + text.appendChild(rendered.element); item.appendChild(text); if (comment.suggestion?.edits.length) { @@ -505,6 +493,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @ICodeReviewService private readonly _codeReviewService: ICodeReviewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -572,7 +561,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito const groups = groupNearbySessionEditorComments(fileComments, 5); for (const group of groups) { - const widget = new AgentFeedbackEditorWidget(this._editor, group, this._sessionResource, this._agentFeedbackService, this._codeReviewService); + const widget = this._instantiationService.createInstance(AgentFeedbackEditorWidget, this._editor, group, this._sessionResource); this._widgets.push(widget); widget.layout(group[0].range.startLineNumber); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index 9d99b64cada..d4f30fd99e5 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -79,6 +79,12 @@ export interface IAgentFeedbackService { */ revealFeedback(sessionResource: URI, feedbackId: string): Promise; + /** + * Open an editor for the given session comment (feedback or code-review) at its range + * and set it as the navigation anchor. + */ + revealSessionComment(sessionResource: URI, commentId: string, resourceUri: URI, range: IRange): Promise; + /** * Navigate to next/previous feedback item in a session. */ @@ -271,16 +277,19 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe if (!feedback) { return; } + await this.revealSessionComment(sessionResource, feedbackId, feedback.resourceUri, feedback.range); + } + + async revealSessionComment(sessionResource: URI, commentId: string, resourceUri: URI, range: IRange): Promise { await this._editorService.openEditor({ - resource: feedback.resourceUri, + resource: resourceUri, options: { preserveFocus: false, revealIfVisible: true, + selection: { startLineNumber: range.startLineNumber, startColumn: range.startColumn }, } }); - setTimeout(() => { - this.setNavigationAnchor(sessionResource, feedbackId); - }, 50); // delay to ensure editor has revealed the correct position before firing navigation event + this.setNavigationAnchor(sessionResource, commentId); } getNextFeedback(sessionResource: URI, next: boolean): IAgentFeedback | undefined { diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css index 0de61925c9e..766e481b9eb 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css @@ -8,7 +8,7 @@ color: var(--vscode-foreground); background-color: var(--vscode-editorWidget-background); border-radius: 6px; - border: 1px solid var(--vscode-contrastBorder); + border: 1px solid var(--vscode-editorHoverWidget-border); display: flex; align-items: center; justify-content: center; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css index 8eed23bc26c..608af1249b1 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -113,24 +113,6 @@ } /* Dismiss button */ -.agent-feedback-widget-dismiss { - display: flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - border-radius: 4px; - cursor: pointer; - color: var(--vscode-foreground); - opacity: 0.7; - transition: opacity 0.1s; -} - -.agent-feedback-widget-dismiss:hover { - opacity: 1; - background-color: var(--vscode-toolbar-hoverBackground); -} - /* Body - collapsible */ .agent-feedback-widget-body { transition: max-height 0.2s ease-in-out, padding 0.2s ease-in-out; @@ -233,6 +215,18 @@ word-wrap: break-word; } +.agent-feedback-widget-text .rendered-markdown p { + margin: 0; +} + +.agent-feedback-widget-text .rendered-markdown code { + font-family: var(--monaco-monospace-font); + font-size: 11px; + padding: 1px 4px; + border-radius: 3px; + background: color-mix(in srgb, var(--vscode-editorWidget-border, var(--vscode-widget-border)) 25%, transparent); +} + .agent-feedback-widget-suggestion { display: flex; flex-direction: column; diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts index 818c87c9a36..b8b75bec923 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts @@ -6,6 +6,7 @@ import { Event } from '../../../../../base/common/event.js'; import { Color } from '../../../../../base/common/color.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { IMarkdownRendererService, MarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { observableValue } from '../../../../../base/common/observable.js'; @@ -182,7 +183,16 @@ function renderWidget(context: ComponentFixtureContext, options: IFixtureOptions ensureTokenColorMap(); - const instantiationService = createEditorServices(scopedDisposables, { colorTheme: context.theme }); + const agentFeedbackService = createMockAgentFeedbackService(); + const codeReviewService = createMockCodeReviewService(); + const instantiationService = createEditorServices(scopedDisposables, { + colorTheme: context.theme, + additionalServices: reg => { + reg.defineInstance(IAgentFeedbackService, agentFeedbackService); + reg.defineInstance(ICodeReviewService, codeReviewService); + reg.define(IMarkdownRendererService, MarkdownRendererService); + }, + }); const model = scopedDisposables.add(createTextModel(instantiationService, sampleCode, fileResource, 'typescript')); const editorOptions: ICodeEditorWidgetOptions = { @@ -205,12 +215,11 @@ function renderWidget(context: ComponentFixtureContext, options: IFixtureOptions editor.setModel(model); - const widget = scopedDisposables.add(new AgentFeedbackEditorWidget( + const widget = scopedDisposables.add(instantiationService.createInstance( + AgentFeedbackEditorWidget, editor, options.commentItems, sessionResource, - createMockAgentFeedbackService(), - createMockCodeReviewService(), )); widget.layout(options.commentItems[0].range.startLineNumber); diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index efd27ab3a77..9c30d453409 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -749,6 +749,23 @@ export class ChangesViewPane extends ViewPane { sessionsChangedSignal.read(reader); // Re-evaluate when session metadata changes (e.g. pullRequestUrl) const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; + // Read code review state to update the button label dynamically + let codeReviewCommentCount: number | undefined; + let codeReviewLoading = false; + if (sessionResource) { + const sessionChanges = this.agentSessionsService.getSession(sessionResource)?.changes; + if (sessionChanges instanceof Array && sessionChanges.length > 0) { + const reviewFiles = getCodeReviewFilesFromSessionChanges(sessionChanges as readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]); + const reviewVersion = getCodeReviewVersion(reviewFiles); + const reviewState = this.codeReviewService.getReviewState(sessionResource).read(reader); + if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === reviewVersion) { + codeReviewLoading = true; + } else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === reviewVersion && reviewState.comments.length > 0) { + codeReviewCommentCount = reviewState.comments.length; + } + } + } + reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, @@ -768,6 +785,12 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel }; } if (action.id === RUN_SESSION_CODE_REVIEW_ACTION_ID) { + if (codeReviewLoading) { + return { showIcon: true, showLabel: true, isSecondary: true, customLabel: '$(loading~spin)', customClass: 'code-review-loading' }; + } + if (codeReviewCommentCount !== undefined) { + return { showIcon: true, showLabel: true, isSecondary: true, customLabel: String(codeReviewCommentCount), customClass: 'code-review-comments' }; + } return { showIcon: true, showLabel: false, isSecondary: true }; } if (action.id === 'chatEditing.synchronizeChanges') { diff --git a/src/vs/sessions/contrib/changes/browser/media/changesView.css b/src/vs/sessions/contrib/changes/browser/media/changesView.css index 948b1a898bc..1b187233832 100644 --- a/src/vs/sessions/contrib/changes/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changes/browser/media/changesView.css @@ -274,3 +274,9 @@ .changes-view-body .chat-editing-session-actions .monaco-button .working-set-lines-removed { color: var(--vscode-chat-linesRemovedForeground); } + +.changes-view-body .chat-editing-session-actions .monaco-button.code-review-comments, +.changes-view-body .chat-editing-session-actions .monaco-button.code-review-loading { + padding-left: 4px; + padding-right: 4px; +} diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts index 805ad59cca0..bb3818e8a46 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts @@ -20,6 +20,8 @@ import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/action import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from './codeReviewService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js'; +import { SessionEditorCommentSource, toSessionEditorCommentId } from '../../agentFeedback/browser/sessionEditorComments.js'; registerSingleton(ICodeReviewService, CodeReviewService, InstantiationType.Delayed); @@ -55,6 +57,7 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp const sessionManagementService = accessor.get(ISessionsManagementService); const agentSessionsService = accessor.get(IAgentSessionsService); const codeReviewService = accessor.get(ICodeReviewService); + const agentFeedbackService = accessor.get(IAgentFeedbackService); const resource = URI.isUri(sessionResource) ? sessionResource @@ -72,6 +75,15 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp const files = getCodeReviewFilesFromSessionChanges(session.changes); const version = getCodeReviewVersion(files); + // If a review already exists with comments, navigate to the first comment + const reviewState = codeReviewService.getReviewState(resource).get(); + if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version && reviewState.comments.length > 0) { + const firstComment = reviewState.comments[0]; + const commentId = toSessionEditorCommentId(SessionEditorCommentSource.CodeReview, firstComment.id); + await agentFeedbackService.revealSessionComment(resource, commentId, firstComment.uri, firstComment.range); + return; + } + codeReviewService.requestReview(resource, version, files); } } @@ -126,13 +138,14 @@ class CodeReviewToolbarContribution extends Disposable implements IWorkbenchCont if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === version) { canRunCodeReview = false; tooltip = localize('sessions.runCodeReview.tooltip.loading', "Creating code review..."); - icon = Codicon.commentDraft; + icon = Codicon.codeReview; } else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version) { - canRunCodeReview = false; if (reviewState.comments.length === 0) { + canRunCodeReview = false; tooltip = localize('sessions.runCodeReview.tooltip.allResolved', "All review comments have been addressed."); icon = Codicon.comment; } else { + canRunCodeReview = true; icon = Codicon.commentUnresolved; tooltip = reviewState.comments.length === 1 ? localize('sessions.runCodeReview.tooltip.oneUnresolved', "1 review comment unresolved.") diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts index acd401cf4e3..8230f01c789 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts @@ -9,6 +9,8 @@ import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { hash } from '../../../../base/common/hash.js'; import { hasKey } from '../../../../base/common/types.js'; @@ -156,6 +158,23 @@ export interface ICodeReviewService { dismissReview(sessionResource: URI): void; } +// --- Storage Types ----------------------------------------------------------- + +interface IStoredCodeReview { + readonly version: string; + readonly comments: readonly IStoredCodeReviewComment[]; +} + +interface IStoredCodeReviewComment { + readonly id: string; + readonly uri: UriComponents; + readonly range: IRange; + readonly body: string; + readonly kind: string; + readonly severity: string; + readonly suggestion?: ICodeReviewSuggestion; +} + // --- Implementation ---------------------------------------------------------- interface ISessionReviewData { @@ -225,12 +244,18 @@ export class CodeReviewService extends Disposable implements ICodeReviewService declare readonly _serviceBrand: undefined; + private static readonly _STORAGE_KEY = 'codeReview.reviews'; + private readonly _reviewsBySession = new Map(); constructor( @ICommandService private readonly _commandService: ICommandService, + @IStorageService private readonly _storageService: IStorageService, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, ) { super(); + this._loadFromStorage(); + this._registerSessionListeners(); } getReviewState(sessionResource: URI): IObservable { @@ -276,12 +301,14 @@ export class CodeReviewService extends Disposable implements ICodeReviewService const filtered = state.comments.filter(c => c.id !== commentId); data.state.set({ kind: CodeReviewStateKind.Result, version: state.version, comments: filtered }, undefined); + this._saveToStorage(); } dismissReview(sessionResource: URI): void { const data = this._reviewsBySession.get(sessionResource.toString()); if (data) { data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + this._saveToStorage(); } } @@ -342,6 +369,7 @@ export class CodeReviewService extends Disposable implements ICodeReviewService transaction(tx => { data.state.set({ kind: CodeReviewStateKind.Result, version, comments }, tx); }); + this._saveToStorage(); } } catch (err) { const currentState = data.state.get(); @@ -350,4 +378,109 @@ export class CodeReviewService extends Disposable implements ICodeReviewService } } } + + private _loadFromStorage(): void { + const raw = this._storageService.get(CodeReviewService._STORAGE_KEY, StorageScope.WORKSPACE); + if (!raw) { + return; + } + + try { + const stored: Record = JSON.parse(raw); + for (const [key, review] of Object.entries(stored)) { + const comments: ICodeReviewComment[] = review.comments.map(c => ({ + id: c.id, + uri: URI.revive(c.uri), + range: c.range, + body: c.body, + kind: c.kind, + severity: c.severity, + suggestion: c.suggestion, + })); + const data = this._getOrCreateData(URI.parse(key)); + data.state.set({ kind: CodeReviewStateKind.Result, version: review.version, comments }, undefined); + } + } catch { + // Corrupted storage data — ignore + } + } + + private _saveToStorage(): void { + const stored: Record = {}; + for (const [key, data] of this._reviewsBySession) { + const state = data.state.get(); + if (state.kind === CodeReviewStateKind.Result) { + stored[key] = { + version: state.version, + comments: state.comments.map(c => ({ + id: c.id, + uri: c.uri.toJSON(), + range: c.range, + body: c.body, + kind: c.kind, + severity: c.severity, + suggestion: c.suggestion, + })), + }; + } + } + + if (Object.keys(stored).length === 0) { + this._storageService.remove(CodeReviewService._STORAGE_KEY, StorageScope.WORKSPACE); + } else { + this._storageService.store(CodeReviewService._STORAGE_KEY, JSON.stringify(stored), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + } + + private _registerSessionListeners(): void { + // Clean up when a session is archived + this._register(this._agentSessionsService.onDidChangeSessionArchivedState(session => { + if (session.isArchived()) { + const key = session.resource.toString(); + const data = this._reviewsBySession.get(key); + if (data) { + data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + this._saveToStorage(); + } + } + })); + + // Clean up when session changes make a review version outdated + this._register(this._agentSessionsService.model.onDidChangeSessions(() => { + let changed = false; + for (const [key, data] of this._reviewsBySession) { + const state = data.state.get(); + if (state.kind !== CodeReviewStateKind.Result) { + continue; + } + + const session = this._agentSessionsService.getSession(URI.parse(key)); + if (!session) { + // Session no longer exists — clean up + data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + changed = true; + continue; + } + + if (!(session.changes instanceof Array) || session.changes.length === 0) { + // Session has no file-level changes — clean up + data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + changed = true; + continue; + } + + const files = getCodeReviewFilesFromSessionChanges(session.changes); + const currentVersion = getCodeReviewVersion(files); + if (state.version !== currentVersion) { + // Version mismatch — review is stale + data.state.set({ kind: CodeReviewStateKind.Idle }, undefined); + changed = true; + } + } + + if (changed) { + this._saveToStorage(); + } + })); + } } diff --git a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts index 76dcc8c1385..000bf528d12 100644 --- a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts +++ b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts @@ -10,14 +10,21 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { Event } from '../../../../../base/common/event.js'; -import { CodeReviewService, CodeReviewStateKind, ICodeReviewService } from '../../browser/codeReviewService.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { InMemoryStorageService, IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { IAgentSession, IAgentSessionsModel } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IChatSessionFileChange2 } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../browser/codeReviewService.js'; suite('CodeReviewService', () => { const store = new DisposableStore(); + let instantiationService: TestInstantiationService; let service: ICodeReviewService; let commandService: MockCommandService; + let storageService: InMemoryStorageService; + let agentSessionsService: MockAgentSessionsService; let session: URI; let fileA: URI; @@ -87,12 +94,81 @@ suite('CodeReviewService', () => { } } + class MockAgentSessionsService { + declare readonly _serviceBrand: undefined; + + private readonly _onDidChangeSessionArchivedState: Emitter; + readonly onDidChangeSessionArchivedState: Event; + private readonly _onDidChangeSessions: Emitter; + readonly model: IAgentSessionsModel; + private readonly _sessions = new Map(); + + constructor(disposables: DisposableStore) { + this._onDidChangeSessionArchivedState = disposables.add(new Emitter()); + this.onDidChangeSessionArchivedState = this._onDidChangeSessionArchivedState.event; + this._onDidChangeSessions = disposables.add(new Emitter()); + this.model = { + onWillResolve: Event.None, + onDidResolve: Event.None, + onDidChangeSessions: this._onDidChangeSessions.event, + onDidChangeSessionArchivedState: this._onDidChangeSessionArchivedState.event, + resolved: true, + sessions: [], + getSession: (resource: URI) => this._sessions.get(resource.toString()), + resolve: async () => { }, + }; + } + + getSession(resource: URI): IAgentSession | undefined { + return this._sessions.get(resource.toString()); + } + + setSession(resource: URI, changes?: readonly IChatSessionFileChange2[], archived = false): IAgentSession { + let _archived = archived; + const session = { + resource, + changes, + isArchived: () => _archived, + setArchived: (v: boolean) => { _archived = v; }, + isRead: () => true, + setRead: () => { }, + } as unknown as IAgentSession; + this._sessions.set(resource.toString(), session); + return session; + } + + updateSessionChanges(resource: URI, changes: readonly IChatSessionFileChange2[] | undefined): void { + const session = this._sessions.get(resource.toString()) as Record | undefined; + if (session) { + session.changes = changes; + } + } + + removeSession(resource: URI): void { + this._sessions.delete(resource.toString()); + } + + fireSessionArchivedState(session: IAgentSession): void { + this._onDidChangeSessionArchivedState.fire(session); + } + + fireSessionsChanged(): void { + this._onDidChangeSessions.fire(); + } + } + setup(() => { - const instantiationService = store.add(new TestInstantiationService()); + instantiationService = store.add(new TestInstantiationService()); commandService = new MockCommandService(); instantiationService.stub(ICommandService, commandService); + storageService = store.add(new InMemoryStorageService()); + instantiationService.stub(IStorageService, storageService); + + agentSessionsService = new MockAgentSessionsService(store); + instantiationService.stub(IAgentSessionsService, agentSessionsService); + service = store.add(instantiationService.createInstance(CodeReviewService)); session = URI.parse('test://session/1'); fileA = URI.parse('file:///a.ts'); @@ -654,6 +730,220 @@ suite('CodeReviewService', () => { CodeReviewStateKind.Idle, ]); }); + + // --- Storage persistence --- + + test('review results are persisted to storage', async () => { + commandService.result = { + type: 'success', + comments: [{ uri: fileA, range: new Range(1, 1, 5, 1), body: 'Persisted comment', kind: 'bug', severity: 'high' }], + }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const raw = storageService.get('codeReview.reviews', StorageScope.WORKSPACE); + assert.ok(raw, 'Storage should contain review data'); + const stored = JSON.parse(raw!); + const reviewData = stored[session.toString()]; + assert.ok(reviewData); + assert.strictEqual(reviewData.version, 'v1'); + assert.strictEqual(reviewData.comments.length, 1); + assert.strictEqual(reviewData.comments[0].body, 'Persisted comment'); + }); + + test('reviews are restored from storage on service creation', async () => { + commandService.result = { + type: 'success', + comments: [{ uri: fileA, range: new Range(1, 1, 5, 1), body: 'Restored comment', kind: 'bug', severity: 'high' }], + }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + // Create a second service with the same storage + const service2 = store.add(instantiationService.createInstance(CodeReviewService)); + const state = service2.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.version, 'v1'); + assert.strictEqual(state.comments.length, 1); + assert.strictEqual(state.comments[0].body, 'Restored comment'); + assert.strictEqual(state.comments[0].uri.toString(), fileA.toString()); + assert.deepStrictEqual(state.comments[0].range, { startLineNumber: 1, startColumn: 1, endLineNumber: 5, endColumn: 1 }); + } + }); + + test('suggestions are persisted and restored correctly', async () => { + commandService.result = { + type: 'success', + comments: [{ + uri: fileA, + range: new Range(1, 1, 5, 1), + body: 'suggestion comment', + suggestion: { + edits: [{ + range: new Range(2, 1, 3, 10), + oldText: 'let x = 1;', + newText: 'const x = 1;', + }], + }, + }], + }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const service2 = store.add(instantiationService.createInstance(CodeReviewService)); + const state = service2.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.comments[0].suggestion?.edits.length, 1); + assert.strictEqual(state.comments[0].suggestion?.edits[0].oldText, 'let x = 1;'); + assert.strictEqual(state.comments[0].suggestion?.edits[0].newText, 'const x = 1;'); + } + }); + + test('removeComment updates storage', async () => { + commandService.result = { + type: 'success', + comments: [ + { uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment1' }, + { uri: fileA, range: new Range(5, 1, 5, 1), body: 'comment2' }, + ], + }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const state = service.getReviewState(session).get(); + if (state.kind !== CodeReviewStateKind.Result) { return; } + + service.removeComment(session, state.comments[0].id); + + const raw = storageService.get('codeReview.reviews', StorageScope.WORKSPACE); + const stored = JSON.parse(raw!); + assert.strictEqual(stored[session.toString()].comments.length, 1); + assert.strictEqual(stored[session.toString()].comments[0].body, 'comment2'); + }); + + test('dismissReview removes session from storage', async () => { + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'c' }] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + assert.ok(storageService.get('codeReview.reviews', StorageScope.WORKSPACE)); + + service.dismissReview(session); + + assert.strictEqual(storageService.get('codeReview.reviews', StorageScope.WORKSPACE), undefined); + }); + + test('corrupted storage is handled gracefully', () => { + storageService.store('codeReview.reviews', 'not-valid-json{{{', StorageScope.WORKSPACE, StorageTarget.MACHINE); + + const service2 = store.add(instantiationService.createInstance(CodeReviewService)); + const state = service2.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Idle); + }); + + // --- Session lifecycle cleanup --- + + test('archived session reviews are cleaned up', async () => { + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment' }] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Result); + + const mockSession = agentSessionsService.setSession(session, undefined, true); + agentSessionsService.fireSessionArchivedState(mockSession); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + assert.strictEqual(storageService.get('codeReview.reviews', StorageScope.WORKSPACE), undefined); + }); + + test('non-archived session change does not clean up review', async () => { + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment' }] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + const mockSession = agentSessionsService.setSession(session, undefined, false); + agentSessionsService.fireSessionArchivedState(mockSession); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Result); + }); + + test('session with changed version has review cleaned up', async () => { + const changes: IChatSessionFileChange2[] = [ + { uri: fileA, modifiedUri: fileA, insertions: 1, deletions: 0 }, + ]; + agentSessionsService.setSession(session, changes); + + const files = getCodeReviewFilesFromSessionChanges(changes); + const version = getCodeReviewVersion(files); + + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'stale comment' }] }; + service.requestReview(session, version, files); + await tick(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Result); + + const newChanges: IChatSessionFileChange2[] = [ + { uri: fileA, modifiedUri: fileA, insertions: 1, deletions: 0 }, + { uri: fileB, modifiedUri: fileB, insertions: 2, deletions: 0 }, + ]; + agentSessionsService.updateSessionChanges(session, newChanges); + agentSessionsService.fireSessionsChanged(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + assert.strictEqual(storageService.get('codeReview.reviews', StorageScope.WORKSPACE), undefined); + }); + + test('session that no longer exists has review cleaned up', async () => { + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'orphaned comment' }] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Result); + + agentSessionsService.fireSessionsChanged(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + }); + + test('session with no changes has review cleaned up', async () => { + agentSessionsService.setSession(session, [ + { uri: fileA, modifiedUri: fileA, insertions: 1, deletions: 0 }, + ]); + + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'comment' }] }; + service.requestReview(session, 'v1', [{ currentUri: fileA }]); + await tick(); + + agentSessionsService.updateSessionChanges(session, undefined); + agentSessionsService.fireSessionsChanged(); + + assert.strictEqual(service.getReviewState(session).get().kind, CodeReviewStateKind.Idle); + }); + + test('session with matching version keeps review intact', async () => { + const changes: IChatSessionFileChange2[] = [ + { uri: fileA, modifiedUri: fileA, insertions: 1, deletions: 0 }, + ]; + agentSessionsService.setSession(session, changes); + + const files = getCodeReviewFilesFromSessionChanges(changes); + const version = getCodeReviewVersion(files); + + commandService.result = { type: 'success', comments: [{ uri: fileA, range: new Range(1, 1, 1, 1), body: 'valid comment' }] }; + service.requestReview(session, version, files); + await tick(); + + agentSessionsService.fireSessionsChanged(); + + const state = service.getReviewState(session).get(); + assert.strictEqual(state.kind, CodeReviewStateKind.Result); + if (state.kind === CodeReviewStateKind.Result) { + assert.strictEqual(state.comments[0].body, 'valid comment'); + } + }); }); function tick(): Promise { From 3b84d87a3bd4b2085cce5c4b62b2c57df39cd47f Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 16:50:00 -0700 Subject: [PATCH 380/448] feat: add repository name extraction and grouping toggle for agent sessions --- .../sessions/browser/sessionsViewPane.ts | 50 ++++++++++++++++++- .../agentSessions/agentSessionsFilter.ts | 45 ++--------------- .../agentSessions/agentSessionsViewer.ts | 20 ++++---- 3 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index fbec4187720..93fe120ceca 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -8,7 +8,7 @@ import * as DOM from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { autorun } from '../../../../base/common/observable.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorsVisibleContext } from '../../../../workbench/common/contextkeys.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -40,12 +40,14 @@ import { IHostService } from '../../../../workbench/services/host/browser/host.j const $ = DOM.$; export const SessionsViewId = 'agentic.workbench.view.sessionsView'; const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu'); +const IsGroupedByRepositoryContext = new RawContextKey('sessionsView.isGroupedByRepository', false); export class AgenticSessionsViewPane extends ViewPane { private viewPaneContainer: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; sessionsControl: AgentSessionsControl | undefined; + private sessionsFilter: AgentSessionsFilter | undefined; constructor( options: IViewPaneOptions, @@ -90,7 +92,7 @@ export class AgenticSessionsViewPane extends ViewPane { 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, { + const sessionsFilter = this.sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { filterMenuId: SessionsViewFilterSubMenu, groupResults: () => AgentSessionsGrouping.Date, allowedProviders: [AgentSessionProviders.Background, AgentSessionProviders.Cloud], @@ -99,6 +101,13 @@ export class AgenticSessionsViewPane extends ViewPane { ]), })); + // Track grouping state via context key for the toggle button + const isGroupedByRepoKey = IsGroupedByRepositoryContext.bindTo(this.contextKeyService); + isGroupedByRepoKey.set(sessionsFilter.groupResults() === AgentSessionsGrouping.Repository); + this._register(sessionsFilter.onDidChange(() => { + isGroupedByRepoKey.set(sessionsFilter.groupResults() === AgentSessionsGrouping.Repository); + })); + // Sessions section (top, fills available space) const sessionsSection = DOM.append(sessionsContainer, $('.agent-sessions-section')); @@ -213,6 +222,19 @@ export class AgenticSessionsViewPane extends ViewPane { openFind(): void { this.sessionsControl?.openFind(); } + + toggleGroupByRepository(): void { + if (!this.sessionsFilter) { + return; + } + + const current = this.sessionsFilter.groupResults(); + if (current === AgentSessionsGrouping.Repository) { + this.sessionsFilter.setGrouping(undefined); // back to default (Date) + } else { + this.sessionsFilter.setGrouping(AgentSessionsGrouping.Repository); + } + } } // Register Cmd+N / Ctrl+N keybinding for new session in the agent sessions window @@ -258,6 +280,30 @@ MenuRegistry.appendMenuItem(MenuId.ViewTitle, { when: ContextKeyExpr.equals('view', SessionsViewId) } satisfies ISubmenuItem); +registerAction2(class ToggleGroupByRepositoryAction extends Action2 { + constructor() { + super({ + id: 'sessionsView.toggleGroupByRepository', + title: localize2('groupByRepository', "Group by Repository"), + icon: Codicon.groupByRefType, + category: SessionsCategories.Sessions, + toggled: IsGroupedByRepositoryContext, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 1, + when: ContextKeyExpr.equals('view', SessionsViewId), + }] + }); + } + + override run(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const view = viewsService.getViewWithId(SessionsViewId); + view?.toggleGroupByRepository(); + } +}); + registerAction2(class RefreshAgentSessionsViewerAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 8c464b098d8..e5d437de570 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -127,6 +127,11 @@ export class AgentSessionsFilter extends Disposable implements Required Date: Mon, 9 Mar 2026 16:54:38 -0700 Subject: [PATCH 381/448] feat: enhance repository name extraction in AgentSessionsDataSource --- .../agentSessions/agentSessionsViewer.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 591c0595d21..c1cfe5b399a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -898,21 +898,18 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou private getRepositoryName(session: IAgentSession): string | undefined { const metadata = session.metadata; if (metadata) { + // Cloud sessions: metadata.name is the repo name + const name = metadata.name as string | undefined; + if (name && typeof name === 'string') { + return name; + } + // repositoryNwo: "owner/repo" const nwo = metadata.repositoryNwo as string | undefined; if (nwo && nwo.includes('/')) { return nwo.split('/').pop()!; } - // repositoryPath: "/Users/user/Projects/vscode" — the actual repo root - const repositoryPath = metadata.repositoryPath as string | undefined; - if (repositoryPath) { - const name = repositoryPath.replace(/[\\/]+$/, '').split(/[\\/]/).pop(); - if (name) { - return name; - } - } - // repository: could be "owner/repo" or a URL const repository = metadata.repository as string | undefined; if (repository) { @@ -945,6 +942,16 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou } } + // Fallback: extract repo name from badge if it uses the $(repo) icon + const badge = session.badge; + if (badge) { + const raw = typeof badge === 'string' ? badge : badge.value; + const repoMatch = raw.match(/\$\(repo\)\s*(.+)/); + if (repoMatch) { + return repoMatch[1].trim(); + } + } + return undefined; } } From 0da24326a1dcb32fc6b7260234f36b17c73e325c Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 17:00:11 -0700 Subject: [PATCH 382/448] feat: implement grouping actions for sessions by repository and date --- .../sessions/browser/sessionsViewPane.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 93fe120ceca..f0873e47f9f 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -280,19 +280,41 @@ MenuRegistry.appendMenuItem(MenuId.ViewTitle, { when: ContextKeyExpr.equals('view', SessionsViewId) } satisfies ISubmenuItem); -registerAction2(class ToggleGroupByRepositoryAction extends Action2 { +registerAction2(class GroupByRepositoryAction extends Action2 { constructor() { super({ - id: 'sessionsView.toggleGroupByRepository', + id: 'sessionsView.groupByRepository', title: localize2('groupByRepository', "Group by Repository"), - icon: Codicon.groupByRefType, + icon: Codicon.repo, category: SessionsCategories.Sessions, - toggled: IsGroupedByRepositoryContext, menu: [{ id: MenuId.ViewTitle, group: 'navigation', order: 1, - when: ContextKeyExpr.equals('view', SessionsViewId), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', SessionsViewId), IsGroupedByRepositoryContext.negate()), + }] + }); + } + + override run(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const view = viewsService.getViewWithId(SessionsViewId); + view?.toggleGroupByRepository(); + } +}); + +registerAction2(class GroupByDateAction extends Action2 { + constructor() { + super({ + id: 'sessionsView.groupByDate', + title: localize2('groupByDate', "Group by Date"), + icon: Codicon.history, + category: SessionsCategories.Sessions, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', SessionsViewId), IsGroupedByRepositoryContext), }] }); } From 944b9d37f7b12f4b6973a8b23e659a2aa7916d7c Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:10:20 -0700 Subject: [PATCH 383/448] Normalize URLs in browser tool calls (#299891) --- .../tools/navigateBrowserTool.ts | 22 ++++++++++------- .../electron-browser/tools/openBrowserTool.ts | 24 +++++++++---------- .../tools/openBrowserToolNonAgentic.ts | 23 +++++++++--------- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/navigateBrowserTool.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/navigateBrowserTool.ts index aabb81819aa..aea84f61e5e 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/navigateBrowserTool.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/navigateBrowserTool.ts @@ -69,16 +69,25 @@ export class NavigateBrowserTool implements IToolImpl { invocationMessage: localize('browser.goForward.invocation', "Going forward in browser history"), pastTenseMessage: localize('browser.goForward.past', "Went forward in browser history"), }; - default: + default: { + if (!params.url) { + throw new Error('The "url" parameter is required when type is "url".'); + } + const parsed = URL.parse(params.url); + if (!parsed) { + throw new Error('You must provide a complete, valid URL.'); + } + return { - invocationMessage: localize('browser.navigate.invocation', "Navigating browser to {0}", params.url), - pastTenseMessage: localize('browser.navigate.past', "Navigated browser to {0}", params.url), + invocationMessage: localize('browser.navigate.invocation', "Navigating browser to {0}", parsed.href), + pastTenseMessage: localize('browser.navigate.past', "Navigated browser to {0}", parsed.href), confirmationMessages: { title: localize('browser.navigate.confirmTitle', 'Navigate Browser?'), - message: localize('browser.navigate.confirmMessage', 'This will navigate the browser to {0} and allow the agent to access its contents.', params.url), + message: localize('browser.navigate.confirmMessage', 'This will navigate the browser to {0} and allow the agent to access its contents.', parsed.href), allowAutoConfirm: true, }, }; + } } } @@ -97,12 +106,9 @@ export class NavigateBrowserTool implements IToolImpl { case 'forward': return playwrightInvoke(this.playwrightService, params.pageId, (page) => page.goForward({ waitUntil: 'domcontentloaded' })); default: { - if (!params.url) { - return errorResult('The "url" parameter is required when type is "url".'); - } return playwrightInvoke(this.playwrightService, params.pageId, (page, url) => { return page.goto(url, { waitUntil: 'domcontentloaded' }); - }, params.url); + }, params.url!); } } } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts index a4090c87c4c..97e764cda78 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserTool.ts @@ -8,7 +8,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { localize } from '../../../../../nls.js'; import { IPlaywrightService } from '../../../../../platform/browserView/common/playwrightService.js'; import { ToolDataSource, type CountTokensCallback, type IPreparedToolInvocation, type IToolData, type IToolImpl, type IToolInvocation, type IToolInvocationPreparationContext, type IToolResult, type ToolProgress } from '../../../chat/common/tools/languageModelToolsService.js'; -import { errorResult } from './browserToolHelpers.js'; export const OpenPageToolId = 'open_browser_page'; @@ -43,12 +42,21 @@ export class OpenBrowserTool implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const params = context.parameters as IOpenBrowserToolParams; + + if (!params.url) { + throw new Error('The "url" parameter is required.'); + } + const parsed = URL.parse(params.url); + if (!parsed) { + throw new Error('You must provide a complete, valid URL.'); + } + return { - invocationMessage: localize('browser.open.invocation', "Opening browser page at {0}", params.url ?? 'about:blank'), - pastTenseMessage: localize('browser.open.past', "Opened browser page at {0}", params.url ?? 'about:blank'), + invocationMessage: localize('browser.open.invocation', "Opening browser page at {0}", parsed.href), + pastTenseMessage: localize('browser.open.past', "Opened browser page at {0}", parsed.href), confirmationMessages: { title: localize('browser.open.confirmTitle', 'Open Browser Page?'), - message: localize('browser.open.confirmMessage', 'This will open {0} in the integrated browser. The agent will be able to read and interact with its contents.', params.url ?? 'about:blank'), + message: localize('browser.open.confirmMessage', 'This will open {0} in the integrated browser. The agent will be able to read and interact with its contents.', parsed.href), allowAutoConfirm: true, }, }; @@ -56,14 +64,6 @@ export class OpenBrowserTool implements IToolImpl { async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { const params = invocation.parameters as IOpenBrowserToolParams; - - if (!params.url) { - return errorResult('The "url" parameter is required.'); - } - if (!URL.parse(params.url)) { - return errorResult('You must provide a complete, valid URL.'); - } - const { pageId, summary } = await this.playwrightService.openPage(params.url); return { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts index 40e6abcf62b..d9c819f7ca9 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts @@ -10,7 +10,6 @@ import { BrowserViewUri } from '../../../../../platform/browserView/common/brows import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { type CountTokensCallback, type IPreparedToolInvocation, type IToolData, type IToolImpl, type IToolInvocation, type IToolInvocationPreparationContext, type IToolResult, type ToolProgress } from '../../../chat/common/tools/languageModelToolsService.js'; -import { errorResult } from './browserToolHelpers.js'; import { IOpenBrowserToolParams, OpenBrowserToolData } from './openBrowserTool.js'; export const OpenBrowserToolNonAgenticData: IToolData = { @@ -26,12 +25,21 @@ export class OpenBrowserToolNonAgentic implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const params = context.parameters as IOpenBrowserToolParams; + + if (!params.url) { + throw new Error('The "url" parameter is required.'); + } + const parsed = URL.parse(params.url); + if (!parsed) { + throw new Error('You must provide a complete, valid URL.'); + } + return { - invocationMessage: localize('browser.open.nonAgentic.invocation', "Opening browser page at {0}", params.url ?? 'about:blank'), - pastTenseMessage: localize('browser.open.nonAgentic.past', "Opened browser page at {0}", params.url ?? 'about:blank'), + invocationMessage: localize('browser.open.nonAgentic.invocation', "Opening browser page at {0}", parsed.href), + pastTenseMessage: localize('browser.open.nonAgentic.past', "Opened browser page at {0}", parsed.href), confirmationMessages: { title: localize('browser.open.nonAgentic.confirmTitle', 'Open Browser Page?'), - message: localize('browser.open.nonAgentic.confirmMessage', 'This will open {0} in the integrated browser. The agent will not be able to read its contents.', params.url ?? 'about:blank'), + message: localize('browser.open.nonAgentic.confirmMessage', 'This will open {0} in the integrated browser. The agent will not be able to read its contents.', parsed.href), allowAutoConfirm: true, }, }; @@ -40,13 +48,6 @@ export class OpenBrowserToolNonAgentic implements IToolImpl { async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { const params = invocation.parameters as IOpenBrowserToolParams; - if (!params.url) { - return errorResult('The "url" parameter is required.'); - } - if (!URL.parse(params.url)) { - return errorResult('You must provide a complete, valid URL.'); - } - logBrowserOpen(this.telemetryService, 'chatTool'); const browserUri = BrowserViewUri.forUrl(params.url); From 35d87bb110ba3080d82cb2359aea19329a2238e8 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 17:28:40 -0700 Subject: [PATCH 384/448] fix: address PR review feedback - Scope grouping storage key per filter instance (filterMenuId) to avoid cross-instance interference; only enable grouping override when a default grouping is configured - Add isStoringGrouping guard to prevent duplicate onDidChange events when setGrouping writes to storage - Use full owner/repo as section ID for unique grouping; display short repo name as section label Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentSessions/agentSessionsFilter.ts | 33 +++++++++++--- .../agentSessions/agentSessionsViewer.ts | 43 +++++++++++-------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index e5d437de570..87227715de6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -55,7 +55,7 @@ const DEFAULT_EXCLUDES: IAgentSessionsFilterExcludes = Object.freeze({ export class AgentSessionsFilter extends Disposable implements Required { private readonly STORAGE_KEY = `agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu`; - private readonly GROUPING_STORAGE_KEY = `agentSessions.grouping`; + private readonly GROUPING_STORAGE_KEY: string | undefined; private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; @@ -64,6 +64,7 @@ export class AgentSessionsFilter extends Disposable implements Required this.groupingOverride ?? this.options.groupResults?.(); private groupingOverride: AgentSessionsGrouping | undefined; + private isStoringGrouping = false; private excludes = DEFAULT_EXCLUDES; private isStoringExcludes = false; @@ -77,6 +78,11 @@ export class AgentSessionsFilter extends Disposable implements Required this.updateFilterActions())); this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.STORAGE_KEY, this._store)(() => this.updateExcludes(true))); - this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.GROUPING_STORAGE_KEY, this._store)(() => this.updateGrouping(true))); + if (this.GROUPING_STORAGE_KEY) { + this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.GROUPING_STORAGE_KEY, this._store)(() => this.updateGrouping(true))); + } } private updateExcludes(fromEvent: boolean): void { @@ -113,6 +121,10 @@ export class AgentSessionsFilter extends Disposable implements Required(); + const repoMap = new Map(); const archivedSessions: IAgentSession[] = []; + const noRepoId = 'other'; const noRepoLabel = localize('agentSessions.noRepository', "Other"); for (const session of sortedSessions) { @@ -865,21 +866,23 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou continue; } - const repoName = this.getRepositoryName(session) ?? noRepoLabel; + const repo = this.getRepositoryInfo(session); + const repoId = repo?.id ?? noRepoId; + const repoLabel = repo?.label ?? noRepoLabel; - let group = repoMap.get(repoName); + let group = repoMap.get(repoId); if (!group) { - group = []; - repoMap.set(repoName, group); + group = { label: repoLabel, sessions: [] }; + repoMap.set(repoId, group); } - group.push(session); + group.sessions.push(session); } const result: AgentSessionListItem[] = []; - for (const [repoName, sessions] of repoMap) { + for (const [repoId, { label, sessions }] of repoMap) { result.push({ - section: `repo-${repoName}`, - label: repoName, + section: `repo-${repoId}`, + label, sessions, }); } @@ -895,32 +898,34 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou return result; } - private getRepositoryName(session: IAgentSession): string | undefined { + private getRepositoryInfo(session: IAgentSession): { id: string; label: string } | undefined { const metadata = session.metadata; if (metadata) { - // Cloud sessions: metadata.name is the repo name + // Cloud sessions: metadata.owner + metadata.name + const owner = metadata.owner as string | undefined; const name = metadata.name as string | undefined; - if (name && typeof name === 'string') { - return name; + if (owner && name) { + return { id: `${owner}/${name}`, label: name }; } // repositoryNwo: "owner/repo" const nwo = metadata.repositoryNwo as string | undefined; if (nwo && nwo.includes('/')) { - return nwo.split('/').pop()!; + return { id: nwo, label: nwo.split('/').pop()! }; } // repository: could be "owner/repo" or a URL const repository = metadata.repository as string | undefined; if (repository) { if (repository.includes('/') && !repository.includes(':')) { - return repository.split('/').pop()!; + return { id: repository, label: repository.split('/').pop()! }; } try { const url = new URL(repository); const parts = url.pathname.split('/').filter(Boolean); if (parts.length >= 2) { - return parts[1]; + const id = `${parts[0]}/${parts[1]}`; + return { id, label: parts[1] }; } } catch { // not a URL @@ -934,7 +939,8 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou const url = new URL(repositoryUrl); const parts = url.pathname.split('/').filter(Boolean); if (parts.length >= 2) { - return parts[1]; + const id = `${parts[0]}/${parts[1]}`; + return { id, label: parts[1] }; } } catch { // not a URL @@ -948,7 +954,8 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou const raw = typeof badge === 'string' ? badge : badge.value; const repoMatch = raw.match(/\$\(repo\)\s*(.+)/); if (repoMatch) { - return repoMatch[1].trim(); + const label = repoMatch[1].trim(); + return { id: label, label }; } } From 975cdcf8fe15139610ec8c1c6ae9885b31fd2b7c Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:38:04 -0700 Subject: [PATCH 385/448] policy: `DeprecatedEditModeHidden` (#300290) * Add 'DeprecatedEditModeHidden' policy * Add 'add-policy' skill --- .github/skills/add-policy/SKILL.md | 139 ++++++++++++++++++ build/lib/policies/policyData.jsonc | 14 ++ .../contrib/chat/browser/chat.contribution.ts | 11 ++ 3 files changed, 164 insertions(+) create mode 100644 .github/skills/add-policy/SKILL.md diff --git a/.github/skills/add-policy/SKILL.md b/.github/skills/add-policy/SKILL.md new file mode 100644 index 00000000000..64119b056ba --- /dev/null +++ b/.github/skills/add-policy/SKILL.md @@ -0,0 +1,139 @@ +--- +name: add-policy +description: Use when adding, modifying, or reviewing VS Code configuration policies. Covers the full policy lifecycle from registration to export to platform-specific artifacts. Run on ANY change that adds a `policy:` field to a configuration property. +--- + +# Adding a Configuration Policy + +Policies allow enterprise administrators to lock configuration settings via OS-level mechanisms (Windows Group Policy, macOS managed preferences, Linux config files) or via Copilot account-level policy data. This skill covers the complete procedure. + +## When to Use + +- Adding a new `policy:` field to any configuration property +- Modifying an existing policy (rename, category change, etc.) +- Reviewing a PR that touches policy registration +- Adding account-based policy support via `IPolicyData` + +## Architecture Overview + +### Policy Sources (layered, last writer wins) + +| Source | Implementation | How it reads policies | +|--------|---------------|----------------------| +| **OS-level** (Windows registry, macOS plist) | `NativePolicyService` via `@vscode/policy-watcher` | Watches `Software\Policies\Microsoft\{productName}` (Windows) or bundle identifier prefs (macOS) | +| **Linux file** | `FilePolicyService` | Reads `/etc/vscode/policy.json` | +| **Account/GitHub** | `AccountPolicyService` | Reads `IPolicyData` from `IDefaultAccountService.policyData`, applies `value()` function | +| **Multiplex** | `MultiplexPolicyService` | Combines OS-level + account policy services; used in desktop main | + +### Key Files + +| File | Purpose | +|------|---------| +| `src/vs/base/common/policy.ts` | `PolicyCategory` enum, `IPolicy` interface | +| `src/vs/platform/policy/common/policy.ts` | `IPolicyService`, `AbstractPolicyService`, `PolicyDefinition` | +| `src/vs/platform/configuration/common/configurations.ts` | `PolicyConfiguration` — bridges policies to configuration values | +| `src/vs/workbench/services/policies/common/accountPolicyService.ts` | Account/GitHub-based policy evaluation | +| `src/vs/workbench/services/policies/common/multiplexPolicyService.ts` | Combines multiple policy services | +| `src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts` | `--export-policy-data` CLI handler | +| `src/vs/base/common/defaultAccount.ts` | `IPolicyData` interface for account-level policy fields | +| `build/lib/policies/policyData.jsonc` | Auto-generated policy catalog (DO NOT edit manually) | +| `build/lib/policies/policyGenerator.ts` | Generates ADMX/ADML (Windows), plist (macOS), JSON (Linux) | +| `build/lib/test/policyConversion.test.ts` | Tests for policy artifact generation | + +## Procedure + +### Step 1 — Add the `policy` field to the configuration property + +Find the configuration registration (typically in a `*.contribution.ts` file) and add a `policy` object to the property schema. + +**Required fields:** + +**Determining `minimumVersion`:** Always read `version` from the root `package.json` and use the `major.minor` portion. For example, if `package.json` has `"version": "1.112.0"`, use `minimumVersion: '1.112'`. Never hardcode an old version like `'1.99'`. + +```typescript +policy: { + name: 'MyPolicyName', // PascalCase, unique across all policies + category: PolicyCategory.InteractiveSession, // From PolicyCategory enum + minimumVersion: '1.112', // Use major.minor from package.json version + localization: { + description: { + key: 'my.config.key', // NLS key for the description + value: nls.localize('my.config.key', "Human-readable description."), + } + } +} +``` + +**Optional: `value` function for account-based policy:** + +If this policy should also be controllable via Copilot account policy data (from `IPolicyData`), add a `value` function: + +```typescript +policy: { + name: 'MyPolicyName', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.112', // Use major.minor from package.json version + value: (policyData) => policyData.my_field === false ? false : undefined, + localization: { /* ... */ } +} +``` + +The `value` function receives `IPolicyData` (from `src/vs/base/common/defaultAccount.ts`) and should: +- Return a concrete value to **override** the user's setting +- Return `undefined` to **not apply** any account-level override (falls through to OS policy or user setting) + +If you need a new field on `IPolicyData`, add it to the interface in `src/vs/base/common/defaultAccount.ts`. + +**Optional: `enumDescriptions` for enum/string policies:** + +```typescript +localization: { + description: { key: '...', value: nls.localize('...', "...") }, + enumDescriptions: [ + { key: 'opt.none', value: nls.localize('opt.none', "No access.") }, + { key: 'opt.all', value: nls.localize('opt.all', "Full access.") }, + ] +} +``` + +### Step 2 — Ensure `PolicyCategory` is imported + +```typescript +import { PolicyCategory } from '../../../../base/common/policy.js'; +``` + +Existing categories in the `PolicyCategory` enum: +- `Extensions` +- `IntegratedTerminal` +- `InteractiveSession` (used for all chat/Copilot policies) +- `Telemetry` +- `Update` + +If you need a new category, add it to `PolicyCategory` in `src/vs/base/common/policy.ts` and add corresponding `PolicyCategoryData` localization. + +### Step 3 — Validate TypeScript compilation + +Check the `VS Code - Build` watch task output, or run: + +```bash +npm run compile-check-ts-native +``` + +### Step 4 — Export the policy data + +Regenerate the auto-generated policy catalog: + +```bash +npm run transpile-client && ./scripts/code.sh --export-policy-data +``` + +This updates `build/lib/policies/policyData.jsonc`. **Never edit this file manually.** Verify your new policy appears in the output. You will need code review from a codeowner to merge the change to main. + + +## Policy for extension-provided settings + +For an extension author to provide policies for their extension's settings, a change must be made in `vscode-distro` to the `product.json`. + +## Examples + +Search the codebase for `policy:` to find all the examples of different policy configurations. diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 2ea27460de9..f58dda70afa 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -169,6 +169,20 @@ "type": "boolean", "default": true }, + { + "key": "chat.editMode.hidden", + "name": "DeprecatedEditModeHidden", + "category": "InteractiveSession", + "minimumVersion": "1.112", + "localization": { + "description": { + "key": "chat.editMode.hidden", + "value": "When enabled, hides the Edit mode from the chat mode picker." + } + }, + "type": "boolean", + "default": true + }, { "key": "chat.useHooks", "name": "ChatHooks", diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4fbdb207fa0..93251f797e3 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -728,6 +728,17 @@ configurationRegistry.registerConfiguration({ tags: ['experimental'], experiment: { mode: 'auto' + }, + policy: { + name: 'DeprecatedEditModeHidden', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.112', + localization: { + description: { + key: 'chat.editMode.hidden', + value: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."), + } + } } }, [ChatConfiguration.EnableMath]: { From 7c249057557d6ff89254ae3f37c2124d21cd4e48 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:49:02 -0700 Subject: [PATCH 386/448] bypass approvals and toolbar in cli (#300228) * bypass approvals and toolbar in cli * new picker in new chat state for sessions --- .../chat/browser/newChatPermissionPicker.ts | 201 ++++++++++++++++++ .../contrib/chat/browser/newChatViewPane.ts | 11 +- .../browser/sessionsManagementService.ts | 14 +- .../browser/actions/chatExecuteActions.ts | 21 +- .../contrib/chat/browser/widget/chatWidget.ts | 4 + .../browser/widget/input/chatInputPart.ts | 2 +- .../input/permissionPickerActionItem.ts | 3 +- .../chat/common/actions/chatContextKeys.ts | 1 + 8 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts diff --git a/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts b/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts new file mode 100644 index 00000000000..b5c8ee1730a --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { ChatConfiguration, ChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; +import Severity from '../../../../base/common/severity.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; + +// Track whether warnings have been shown this VS Code session +const shownWarnings = new Set(); + +interface IPermissionItem { + readonly level: ChatPermissionLevel; + readonly label: string; + readonly icon: ThemeIcon; + readonly checked: boolean; +} + +/** + * A permission picker for the new-session welcome view. + * Shows Default Approvals and Bypass Approvals options (no Autopilot for CLI sessions). + */ +export class NewChatPermissionPicker extends Disposable { + + private readonly _onDidChangeLevel = this._register(new Emitter()); + readonly onDidChangeLevel: Event = this._onDidChangeLevel.event; + + private _currentLevel: ChatPermissionLevel = ChatPermissionLevel.Default; + private _triggerElement: HTMLElement | undefined; + private _container: HTMLElement | undefined; + private readonly _renderDisposables = this._register(new DisposableStore()); + + get permissionLevel(): ChatPermissionLevel { + return this._currentLevel; + } + + constructor( + @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService, + ) { + super(); + } + + render(container: HTMLElement): HTMLElement { + this._renderDisposables.clear(); + + const slot = dom.append(container, dom.$('.sessions-chat-picker-slot')); + this._container = slot; + this._renderDisposables.add({ dispose: () => slot.remove() }); + + const trigger = dom.append(slot, dom.$('a.action-label')); + trigger.tabIndex = 0; + trigger.role = 'button'; + this._triggerElement = trigger; + + this._updateTriggerLabel(trigger); + + this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.CLICK, (e) => { + dom.EventHelper.stop(e, true); + this.showPicker(); + })); + + this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + dom.EventHelper.stop(e, true); + this.showPicker(); + } + })); + + return slot; + } + + setVisible(visible: boolean): void { + if (this._container) { + this._container.style.display = visible ? '' : 'none'; + } + } + + showPicker(): void { + if (!this._triggerElement || this.actionWidgetService.isVisible) { + return; + } + + const policyRestricted = this.configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; + + const items: IActionListItem[] = [ + { + kind: ActionListItemKind.Action, + group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.shield }, + item: { + level: ChatPermissionLevel.Default, + label: localize('permissions.default', "Default Approvals"), + icon: Codicon.shield, + checked: this._currentLevel === ChatPermissionLevel.Default, + }, + label: localize('permissions.default', "Default Approvals"), + description: localize('permissions.default.subtext', "Copilot uses your configured settings"), + disabled: false, + }, + { + kind: ActionListItemKind.Action, + group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.warning }, + item: { + level: ChatPermissionLevel.AutoApprove, + label: localize('permissions.autoApprove', "Bypass Approvals"), + icon: Codicon.warning, + checked: this._currentLevel === ChatPermissionLevel.AutoApprove, + }, + label: localize('permissions.autoApprove', "Bypass Approvals"), + description: localize('permissions.autoApprove.subtext', "All tool calls are auto-approved"), + disabled: policyRestricted, + }, + ]; + + const triggerElement = this._triggerElement; + const delegate: IActionListDelegate = { + onSelect: async (item) => { + this.actionWidgetService.hide(); + await this._selectLevel(item.level); + }, + onHide: () => { triggerElement.focus(); }, + }; + + this.actionWidgetService.show( + 'permissionPicker', + false, + items, + delegate, + this._triggerElement, + undefined, + [], + { + getAriaLabel: (item) => item.label ?? '', + getWidgetAriaLabel: () => localize('permissionPicker.ariaLabel', "Permission Picker"), + }, + ); + } + + private async _selectLevel(level: ChatPermissionLevel): Promise { + if (level === ChatPermissionLevel.AutoApprove && !shownWarnings.has(ChatPermissionLevel.AutoApprove)) { + const result = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('permissions.autoApprove.warning.title', "Enable Bypass Approvals?"), + buttons: [ + { + label: localize('permissions.autoApprove.warning.confirm', "Enable"), + run: () => true + }, + { + label: localize('permissions.autoApprove.warning.cancel', "Cancel"), + run: () => false + }, + ], + custom: { + icon: Codicon.warning, + markdownDetails: [{ + markdown: new MarkdownString(localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.")), + }], + }, + }); + if (result.result !== true) { + return; + } + shownWarnings.add(ChatPermissionLevel.AutoApprove); + } + + this._currentLevel = level; + this._updateTriggerLabel(this._triggerElement); + this._onDidChangeLevel.fire(level); + } + + private _updateTriggerLabel(trigger: HTMLElement | undefined): void { + if (!trigger) { + return; + } + + dom.clearNode(trigger); + const icon = this._currentLevel === ChatPermissionLevel.AutoApprove ? Codicon.warning : Codicon.shield; + const label = this._currentLevel === ChatPermissionLevel.AutoApprove + ? localize('permissions.autoApprove.label', "Bypass Approvals") + : localize('permissions.default.label', "Default Approvals"); + + dom.append(trigger, renderIcon(icon)); + const labelSpan = dom.append(trigger, dom.$('span.sessions-chat-dropdown-label')); + labelSpan.textContent = label; + dom.append(trigger, renderIcon(Codicon.chevronDown)); + } +} diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 274cf54a4f5..f4b76e4ffac 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -70,6 +70,7 @@ import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/co import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; import { ChatHistoryNavigator } from '../../../../workbench/contrib/chat/common/widget/chatWidgetHistoryService.js'; import { IHistoryNavigationWidget } from '../../../../base/browser/history.js'; +import { NewChatPermissionPicker } from './newChatPermissionPicker.js'; import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; const STORAGE_KEY_DRAFT_STATE = 'sessions.draftState'; @@ -147,6 +148,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private _inputSlot: HTMLElement | undefined; private readonly _folderPicker: FolderPicker; private _folderPickerContainer: HTMLElement | undefined; + private readonly _permissionPicker: NewChatPermissionPicker; private readonly _repoPicker: RepoPicker; private _repoPickerContainer: HTMLElement | undefined; private readonly _cloudModelPicker: CloudModelPicker; @@ -194,6 +196,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments)); this._folderPicker = this._register(this.instantiationService.createInstance(FolderPicker)); + this._permissionPicker = this._register(this.instantiationService.createInstance(NewChatPermissionPicker)); this._repoPicker = this._register(this.instantiationService.createInstance(RepoPicker)); this._cloudModelPicker = this._register(this.instantiationService.createInstance(CloudModelPicker)); this._targetPicker = this._register(new SessionTargetPicker(options.allowedTargets, this._resolveDefaultTarget(options))); @@ -207,6 +210,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._createNewSession(); const isLocal = target === AgentSessionProviders.Background; this._isolationModePicker.setVisible(isLocal); + this._permissionPicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal); this._syncIndicator.setVisible(isLocal); this._updateDraftState(); @@ -305,6 +309,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { const isolationContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-local-mode')); this._isolationModePicker.render(isolationContainer); dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-spacer')); + this._permissionPicker.render(isolationContainer); const branchContainer = dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-right')); this._branchPicker.render(branchContainer); this._syncIndicator.render(branchContainer); @@ -313,6 +318,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { const isLocal = this._targetPicker.selectedTarget === AgentSessionProviders.Background; const isWorktree = this._isolationModePicker.isolationMode === 'worktree'; this._isolationModePicker.setVisible(isLocal); + this._permissionPicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal && isWorktree); this._syncIndicator.setVisible(isLocal && isWorktree); @@ -1021,7 +1027,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { try { await this.sessionsManagementService.sendRequestForNewSession( session.resource, - options?.openNewAfterSend ? { openNewSessionView: true } : undefined + { + ...options?.openNewAfterSend ? { openNewSessionView: true } : {}, + permissionLevel: this._permissionPicker.permissionLevel, + } ); this._newSessionListener.clear(); this._contextAttachments.clear(); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index be6b61aa821..8f365a6eb87 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -16,7 +16,7 @@ import { ISessionOpenOptions, openSession as openSessionDefault } from '../../.. import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -92,7 +92,7 @@ export interface ISessionsManagementService { * When `openNewSessionView` is true, opens a new session view after sending * instead of navigating to the newly created session. */ - sendRequestForNewSession(sessionResource: URI, options?: { openNewSessionView?: boolean }): Promise; + sendRequestForNewSession(sessionResource: URI, options?: { openNewSessionView?: boolean; permissionLevel?: ChatPermissionLevel }): Promise; /** * Commit files in a worktree and refresh the agent sessions model @@ -306,7 +306,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.logService.info(`[ActiveSessionService] Active session changed (new): ${sessionResource.toString()}, repository: ${repository?.toString() ?? 'none'}`); } - async sendRequestForNewSession(sessionResource: URI, options?: { openNewSessionView?: boolean }): Promise { + async sendRequestForNewSession(sessionResource: URI, options?: { openNewSessionView?: boolean; permissionLevel?: ChatPermissionLevel }): Promise { const session = this._newSession.value; if (!session) { this.logService.error(`[SessionsManagementService] No new session found for resource: ${sessionResource.toString()}`); @@ -334,6 +334,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa modeInstructions: undefined, modeId: 'agent', applyCodeBlockSuggestionId: undefined, + permissionLevel: options?.permissionLevel ?? ChatPermissionLevel.Default, }, agentIdSilent: contribution?.type, attachedContext: session.attachedContext, @@ -353,6 +354,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.openNewSessionView(); } + // Sync the permission level from the welcome picker to the ChatWidget's input part + const permissionLevel = sendOptions.modeInfo?.permissionLevel; + if (permissionLevel) { + const chatWidget = this.chatWidgetService.getWidgetBySessionResource(session.resource); + chatWidget?.input.setPermissionLevel(permissionLevel); + } + // 2. Apply selected model and options to the session const modelRef = this.chatService.acquireExistingSession(session.resource); if (modelRef) { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 26f26818255..c49e5c69c32 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -475,8 +475,10 @@ export class OpenPermissionPickerAction extends Action2 { ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.lockedToCodingAgent.negate(), - IsSessionsWindowContext.negate(), + ContextKeyExpr.or( + ChatContextKeys.lockedToCodingAgent.negate(), + ChatContextKeys.lockedCodingAgentId.isEqualTo(AgentSessionProviders.Background), + ), ) } }); @@ -599,17 +601,6 @@ export class OpenDelegationPickerAction extends Action2 { f1: false, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ - { - id: MenuId.ChatInput, - order: 0.5, - when: ContextKeyExpr.and( - ChatContextKeys.enabled, - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), - ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.chatSessionIsEmpty.negate(), - IsSessionsWindowContext), - group: 'navigation', - }, { id: MenuId.ChatInputSecondary, order: 0.5, @@ -617,8 +608,8 @@ export class OpenDelegationPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.chatSessionIsEmpty.negate(), - IsSessionsWindowContext.negate()), + ChatContextKeys.chatSessionIsEmpty.negate() + ), group: 'navigation', }, ] diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 0cae54e9130..88cf0e53f6f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -286,6 +286,7 @@ export class ChatWidget extends Disposable implements IChatWidget { displayName: string; }; private readonly _lockedToCodingAgentContextKey: IContextKey; + private readonly _lockedCodingAgentIdContextKey: IContextKey; private readonly _agentSupportsAttachmentsContextKey: IContextKey; private readonly _sessionIsEmptyContextKey: IContextKey; private readonly _hasPendingRequestsContextKey: IContextKey; @@ -400,6 +401,7 @@ export class ChatWidget extends Disposable implements IChatWidget { super(); this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService); + this._lockedCodingAgentIdContextKey = ChatContextKeys.lockedCodingAgentId.bindTo(this.contextKeyService); this._agentSupportsAttachmentsContextKey = ChatContextKeys.agentSupportsAttachments.bindTo(this.contextKeyService); this._sessionIsEmptyContextKey = ChatContextKeys.chatSessionIsEmpty.bindTo(this.contextKeyService); this._hasPendingRequestsContextKey = ChatContextKeys.hasPendingRequests.bindTo(this.contextKeyService); @@ -2095,6 +2097,7 @@ export class ChatWidget extends Disposable implements IChatWidget { displayName }; this._lockedToCodingAgentContextKey.set(true); + this._lockedCodingAgentIdContextKey.set(agentId); this.renderWelcomeViewContentIfNeeded(); // Update capabilities for the locked agent const agent = this.chatAgentService.getAgent(agentId); @@ -2109,6 +2112,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // Clear all state related to locking this._lockedAgent = undefined; this._lockedToCodingAgentContextKey.set(false); + this._lockedCodingAgentIdContextKey.set(''); this._updateAgentCapabilitiesContextKeys(undefined); // Explicitly update the DOM to reflect unlocked state diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 206e6f5d4b8..cf7862b0d25 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2026,7 +2026,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; this.secondaryToolbarContainer = elements.secondaryToolbar; - if (this.options.isSessionsWindow || this.options.renderStyle === 'compact') { + if (this.options.renderStyle === 'compact') { this.secondaryToolbarContainer.style.display = 'none'; } this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 46161115022..b3c674193bc 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -57,6 +57,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ) { const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; const isAutopilotEnabled = () => configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; + const isBackgroundProvider = contextKeyService.getContextKeyValue('lockedCodingAgentId') === 'copilotcli'; const actionProvider: IActionWidgetDropdownActionProvider = { getActions: () => { const currentLevel = delegate.currentPermissionLevel.get(); @@ -130,7 +131,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { }, } satisfies IActionWidgetDropdownAction, ]; - if (isAutopilotEnabled()) { + if (isAutopilotEnabled() && !isBackgroundProvider) { actions.push({ ...action, id: 'chat.permissions.autopilot', diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 7b5018b7f44..b249c635261 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -58,6 +58,7 @@ export namespace ChatContextKeys { * True when the chat widget is locked to the coding agent session. */ export const lockedToCodingAgent = new RawContextKey('lockedToCodingAgent', false, { type: 'boolean', description: localize('lockedToCodingAgent', "True when the chat widget is locked to the coding agent session.") }); + export const lockedCodingAgentId = new RawContextKey('lockedCodingAgentId', '', { type: 'string', description: localize('lockedCodingAgentId', "The agent ID when the chat widget is locked to a coding agent session.") }); /** * True when the chat session has a customAgentTarget defined in its contribution, * which means the mode picker should be shown with filtered custom agents. From 74c765017eaff5d7654ae6a140b14fbdfc8f1d59 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 10 Mar 2026 01:52:12 +0100 Subject: [PATCH 387/448] Disable screenshots until time out bug is fixed (#300293) --- .github/workflows/screenshot-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index 9c91702bccb..c3107065279 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -18,6 +18,7 @@ concurrency: jobs: screenshots: + if: false # temporarily disabled name: Checking Component Screenshots runs-on: ubuntu-latest steps: From 21d682f1854d9b0a5085c7909e274cb592e676e3 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 9 Mar 2026 18:00:21 -0700 Subject: [PATCH 388/448] Allow local customizations (#300298) --- .gitignore | 18 ++++++++++++++++++ .vscode/settings.json | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/.gitignore b/.gitignore index 9a9fdcadff9..2ffd5536d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,21 @@ test-output.json test/componentFixtures/.screenshots/* !test/componentFixtures/.screenshots/baseline/ dist +.agents/agents/*.local.md +.claude/agents/*.local.md +.github/agents/*.local.md +.agents/agents/*.local.agent.md +.claude/agents/*.local.agent.md +.github/agents/*.local.agent.md +.agents/hooks/*.local.json +.claude/hooks/*.local.json +.github/hooks/*.local.json +.agents/instructions/*.local.instructions.md +.claude/instructions/*.local.instructions.md +.github/instructions/*.local.instructions.md +.agents/prompts/*.local.prompt.md +.claude/prompts/*.local.prompt.md +.github/prompts/*.local.prompt.md +.agents/skills/.local/ +.claude/skills/.local/ +.github/skills/.local/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 74343459e02..bd45f6441fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -209,4 +209,9 @@ "azureMcp.serverMode": "all", "azureMcp.readOnly": true, "debug.breakpointsView.presentation": "tree", + "chat.agentSkillsLocations": { + ".github/skills/.local": true, + ".agents/skills/.local": true, + ".claude/skills/.local": true, + } } From 0c354be564b5eea68c7750a856b3ff381fc46de8 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 18:19:22 -0700 Subject: [PATCH 389/448] fix: use single static grouping storage key Simplify GROUPING_STORAGE_KEY to a static constant instead of per-instance scoped key. The grouping override is guarded by supportsGroupingOverride (only enabled when options.groupResults is configured), which is sufficient to prevent unintended interference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentSessions/agentSessionsFilter.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 87227715de6..407ef46ac7a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -55,7 +55,7 @@ const DEFAULT_EXCLUDES: IAgentSessionsFilterExcludes = Object.freeze({ export class AgentSessionsFilter extends Disposable implements Required { private readonly STORAGE_KEY = `agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu`; - private readonly GROUPING_STORAGE_KEY: string | undefined; + private static readonly GROUPING_STORAGE_KEY = `agentSessions.grouping`; private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; @@ -65,6 +65,7 @@ export class AgentSessionsFilter extends Disposable implements Required this.updateFilterActions())); this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.STORAGE_KEY, this._store)(() => this.updateExcludes(true))); - if (this.GROUPING_STORAGE_KEY) { - this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.GROUPING_STORAGE_KEY, this._store)(() => this.updateGrouping(true))); + if (this.supportsGroupingOverride) { + this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, AgentSessionsFilter.GROUPING_STORAGE_KEY, this._store)(() => this.updateGrouping(true))); } } @@ -121,11 +119,11 @@ export class AgentSessionsFilter extends Disposable implements Required Date: Mon, 9 Mar 2026 18:23:37 -0700 Subject: [PATCH 390/448] refactor: move grouping state out of AgentSessionsFilter The filter should work the same regardless of grouping mode. Moved all grouping state management (storage, context key, toggle) to the sessions view pane, which owns it. The filter just receives the current grouping via its existing groupResults option callback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../sessions/browser/sessionsViewPane.ts | 42 +++++++------ .../agentSessions/agentSessionsFilter.ts | 59 +------------------ 2 files changed, 26 insertions(+), 75 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index f0873e47f9f..147d4a719f2 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -35,19 +35,22 @@ import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keyb import { ACTION_ID_NEW_CHAT } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { AICustomizationShortcutsWidget } from './aiCustomizationShortcutsWidget.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IHostService } from '../../../../workbench/services/host/browser/host.js'; const $ = DOM.$; export const SessionsViewId = 'agentic.workbench.view.sessionsView'; const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu'); const IsGroupedByRepositoryContext = new RawContextKey('sessionsView.isGroupedByRepository', false); +const GROUPING_STORAGE_KEY = 'agentSessions.grouping'; export class AgenticSessionsViewPane extends ViewPane { private viewPaneContainer: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; sessionsControl: AgentSessionsControl | undefined; - private sessionsFilter: AgentSessionsFilter | undefined; + private currentGrouping: AgentSessionsGrouping = AgentSessionsGrouping.Date; + private isGroupedByRepoKey: ReturnType | undefined; constructor( options: IViewPaneOptions, @@ -63,8 +66,15 @@ export class AgenticSessionsViewPane extends ViewPane { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IHostService private readonly hostService: IHostService, + @IStorageService private readonly storageService: IStorageService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); + + // Restore persisted grouping + const stored = this.storageService.get(GROUPING_STORAGE_KEY, StorageScope.PROFILE); + if (stored && Object.values(AgentSessionsGrouping).includes(stored as AgentSessionsGrouping)) { + this.currentGrouping = stored as AgentSessionsGrouping; + } } protected override renderBody(parent: HTMLElement): void { @@ -91,23 +101,20 @@ export class AgenticSessionsViewPane extends ViewPane { private createControls(parent: HTMLElement): void { const sessionsContainer = DOM.append(parent, $('.agent-sessions-container')); + // Track grouping state via context key for the toggle button + const isGroupedByRepoKey = this.isGroupedByRepoKey = IsGroupedByRepositoryContext.bindTo(this.contextKeyService); + isGroupedByRepoKey.set(this.currentGrouping === AgentSessionsGrouping.Repository); + // Sessions Filter (actions go to view title bar via menu registration) - const sessionsFilter = this.sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { + const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { filterMenuId: SessionsViewFilterSubMenu, - groupResults: () => AgentSessionsGrouping.Date, + groupResults: () => this.currentGrouping, allowedProviders: [AgentSessionProviders.Background, AgentSessionProviders.Cloud], providerLabelOverrides: new Map([ [AgentSessionProviders.Background, localize('chat.session.providerLabel.local', "Local")], ]), })); - // Track grouping state via context key for the toggle button - const isGroupedByRepoKey = IsGroupedByRepositoryContext.bindTo(this.contextKeyService); - isGroupedByRepoKey.set(sessionsFilter.groupResults() === AgentSessionsGrouping.Repository); - this._register(sessionsFilter.onDidChange(() => { - isGroupedByRepoKey.set(sessionsFilter.groupResults() === AgentSessionsGrouping.Repository); - })); - // Sessions section (top, fills available space) const sessionsSection = DOM.append(sessionsContainer, $('.agent-sessions-section')); @@ -224,16 +231,15 @@ export class AgenticSessionsViewPane extends ViewPane { } toggleGroupByRepository(): void { - if (!this.sessionsFilter) { - return; + if (this.currentGrouping === AgentSessionsGrouping.Repository) { + this.currentGrouping = AgentSessionsGrouping.Date; + } else { + this.currentGrouping = AgentSessionsGrouping.Repository; } - const current = this.sessionsFilter.groupResults(); - if (current === AgentSessionsGrouping.Repository) { - this.sessionsFilter.setGrouping(undefined); // back to default (Date) - } else { - this.sessionsFilter.setGrouping(AgentSessionsGrouping.Repository); - } + this.storageService.store(GROUPING_STORAGE_KEY, this.currentGrouping, StorageScope.PROFILE, StorageTarget.USER); + this.isGroupedByRepoKey?.set(this.currentGrouping === AgentSessionsGrouping.Repository); + this.sessionsControl?.refresh(); } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 407ef46ac7a..5cb399a6c87 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -55,17 +55,12 @@ const DEFAULT_EXCLUDES: IAgentSessionsFilterExcludes = Object.freeze({ export class AgentSessionsFilter extends Disposable implements Required { private readonly STORAGE_KEY = `agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu`; - private static readonly GROUPING_STORAGE_KEY = `agentSessions.grouping`; private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; readonly limitResults = () => this.options.limitResults?.(); - readonly groupResults = () => this.groupingOverride ?? this.options.groupResults?.(); - - private groupingOverride: AgentSessionsGrouping | undefined; - private isStoringGrouping = false; - private readonly supportsGroupingOverride: boolean; + readonly groupResults = () => this.options.groupResults?.(); private excludes = DEFAULT_EXCLUDES; private isStoringExcludes = false; @@ -79,10 +74,7 @@ export class AgentSessionsFilter extends Disposable implements Required this.updateFilterActions())); this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, this.STORAGE_KEY, this._store)(() => this.updateExcludes(true))); - if (this.supportsGroupingOverride) { - this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, AgentSessionsFilter.GROUPING_STORAGE_KEY, this._store)(() => this.updateGrouping(true))); - } } private updateExcludes(fromEvent: boolean): void { @@ -118,49 +107,6 @@ export class AgentSessionsFilter extends Disposable implements Required Date: Mon, 9 Mar 2026 18:27:03 -0700 Subject: [PATCH 391/448] fix: revert type widening of AgentSessionSection Keep IAgentSessionSection.section as AgentSessionSection (not string). Revert all changes to agentSessionsControl.ts and agentSessionsModel.ts. Use type assertion for dynamic repo section IDs in the viewer instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../chat/browser/agentSessions/agentSessionsControl.ts | 6 +++--- .../chat/browser/agentSessions/agentSessionsModel.ts | 2 +- .../chat/browser/agentSessions/agentSessionsViewer.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index d1c1457baa6..9fba2ec928c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -179,7 +179,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo private static readonly SECTION_COLLAPSE_STATE_KEY = 'agentSessions.sectionCollapseState'; - private getSavedCollapseState(section: string): boolean | undefined { + private getSavedCollapseState(section: AgentSessionSection): boolean | undefined { const raw = this.storageService.get(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, StorageScope.PROFILE); if (raw) { try { @@ -194,7 +194,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo return undefined; } - private saveSectionCollapseState(section: string, collapsed: boolean): void { + private saveSectionCollapseState(section: AgentSessionSection, collapsed: boolean): void { let state: Record = {}; const raw = this.storageService.get(AgentSessionsControl.SECTION_COLLAPSE_STATE_KEY, StorageScope.PROFILE); if (raw) { @@ -227,7 +227,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo return true; // Archived section is collapsed when archived are excluded } if (this.options.collapseOlderSections?.()) { - const olderSections: string[] = [AgentSessionSection.Week, AgentSessionSection.Older, AgentSessionSection.Archived]; + const olderSections = [AgentSessionSection.Week, AgentSessionSection.Older, AgentSessionSection.Archived]; if (olderSections.includes(element.section)) { return true; // Collapse older time sections if option is enabled } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 0ae2ed5543e..2d40294cfeb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -165,7 +165,7 @@ export const enum AgentSessionSection { } export interface IAgentSessionSection { - readonly section: string; + readonly section: AgentSessionSection; readonly label: string; readonly sessions: IAgentSession[]; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 1d530dfdd15..f229fa60402 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -881,7 +881,7 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou const result: AgentSessionListItem[] = []; for (const [repoId, { label, sessions }] of repoMap) { result.push({ - section: `repo-${repoId}`, + section: `repo-${repoId}` as AgentSessionSection, label, sessions, }); From 862667adee419cd186d4dd4b3396ccfd14571dde Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 9 Mar 2026 18:29:40 -0700 Subject: [PATCH 392/448] fix: avoid type assertion for repo section IDs Add AgentSessionSection.Repository enum value and use it for all repo group sections. Differentiate repo sections via the identity provider which now includes the label in the section ID. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../chat/browser/agentSessions/agentSessionsModel.ts | 3 +++ .../chat/browser/agentSessions/agentSessionsViewer.ts | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 2d40294cfeb..25571a1fbb6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -162,6 +162,9 @@ export const enum AgentSessionSection { // Capped Grouping More = 'more', + + // Repository Grouping + Repository = 'repository', } export interface IAgentSessionSection { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index f229fa60402..d6dc9c07382 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -879,9 +879,9 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou } const result: AgentSessionListItem[] = []; - for (const [repoId, { label, sessions }] of repoMap) { + for (const [, { label, sessions }] of repoMap) { result.push({ - section: `repo-${repoId}` as AgentSessionSection, + section: AgentSessionSection.Repository, label, sessions, }); @@ -1040,7 +1040,7 @@ export class AgentSessionsIdentityProvider implements IIdentityProvider Date: Mon, 9 Mar 2026 18:32:46 -0700 Subject: [PATCH 393/448] perf: use update() instead of refresh() when toggling grouping refresh() re-resolves all sessions from providers (network calls). update() just re-renders the tree with existing data, which is all that is needed when changing the grouping mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 147d4a719f2..cb9a8794025 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -239,7 +239,7 @@ export class AgenticSessionsViewPane extends ViewPane { this.storageService.store(GROUPING_STORAGE_KEY, this.currentGrouping, StorageScope.PROFILE, StorageTarget.USER); this.isGroupedByRepoKey?.set(this.currentGrouping === AgentSessionsGrouping.Repository); - this.sessionsControl?.refresh(); + this.sessionsControl?.update(); } } From 5c842594811efeb339c22bafe5e9fb7ce27bfa51 Mon Sep 17 00:00:00 2001 From: Vijay Upadya <41652029+vijayupadya@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:44:34 -0700 Subject: [PATCH 394/448] Debug Panel: oTel data source support and Import/export (#299256) * otel data source and Import/export * Handle chat customization events in import/export * PR feedback updates * Fix reopen issue * Simplify and pass core events for export * Perf optimizations and label changes * add session title to export/import --- .../common/extensionsApiProposals.ts | 2 +- .../api/browser/mainThreadChatDebug.ts | 47 ++++++++ .../workbench/api/common/extHost.protocol.ts | 2 + .../workbench/api/common/extHostChatDebug.ts | 103 ++++++++++++++++- .../actions/chatOpenAgentDebugPanelAction.ts | 105 +++++++++++++++++- .../chat/browser/chatDebug/chatDebugEditor.ts | 47 ++------ .../browser/chatDebug/chatDebugFlowGraph.ts | 53 ++++----- .../browser/chatDebug/chatDebugFlowLayout.ts | 10 +- .../browser/chatDebug/chatDebugHomeView.ts | 49 ++++---- .../browser/chatDebug/chatDebugLogsView.ts | 34 +++++- .../contrib/chat/common/chatDebugService.ts | 30 +++++ .../chat/common/chatDebugServiceImpl.ts | 55 ++++++++- src/vscode-dts/vscode.proposed.chatDebug.d.ts | 65 ++++++++++- 13 files changed, 502 insertions(+), 100 deletions(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index a9bc2d2fa10..00e09a016ac 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -45,7 +45,7 @@ const _allApiProposals = { }, chatDebug: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatDebug.d.ts', - version: 2 + version: 3 }, chatHooks: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatHooks.d.ts', diff --git a/src/vs/workbench/api/browser/mainThreadChatDebug.ts b/src/vs/workbench/api/browser/mainThreadChatDebug.ts index 82594dcb038..169324d37e7 100644 --- a/src/vs/workbench/api/browser/mainThreadChatDebug.ts +++ b/src/vs/workbench/api/browser/mainThreadChatDebug.ts @@ -5,7 +5,9 @@ import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; +import { VSBuffer } from '../../../base/common/buffer.js'; import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugService } from '../../contrib/chat/common/chatDebugService.js'; +import { IChatService } from '../../contrib/chat/common/chatService/chatService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostChatDebugShape, ExtHostContext, IChatDebugEventDto, MainContext, MainThreadChatDebugShape } from '../common/extHost.protocol.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; @@ -19,6 +21,7 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb constructor( extHostContext: IExtHostContext, @IChatDebugService private readonly _chatDebugService: IChatDebugService, + @IChatService private readonly _chatService: IChatService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatDebug); @@ -36,6 +39,26 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb }, resolveChatDebugLogEvent: async (eventId, token) => { return this._proxy.$resolveChatDebugLogEvent(handle, eventId, token); + }, + provideChatDebugLogExport: async (sessionResource, token) => { + // Gather core events and session title to pass to the extension. + const coreEventDtos = this._chatDebugService.getEvents(sessionResource) + .filter(e => this._chatDebugService.isCoreEvent(e)) + .map(e => this._serializeEvent(e)); + const sessionTitle = this._chatService.getSessionTitle(sessionResource); + const result = await this._proxy.$exportChatDebugLog(handle, sessionResource, coreEventDtos, sessionTitle, token); + return result?.buffer; + }, + resolveChatDebugLogImport: async (data, token) => { + const result = await this._proxy.$importChatDebugLog(handle, VSBuffer.wrap(data), token); + if (!result) { + return undefined; + } + const uri = URI.revive(result.uri); + if (result.sessionTitle) { + this._chatDebugService.setImportedSessionTitle(uri, result.sessionTitle); + } + return uri; } })); } @@ -58,6 +81,30 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb this._chatDebugService.addProviderEvent(revived); } + private _serializeEvent(event: IChatDebugEvent): IChatDebugEventDto { + const base = { + id: event.id, + sessionResource: event.sessionResource, + created: event.created.getTime(), + parentEventId: event.parentEventId, + }; + + switch (event.kind) { + case 'toolCall': + return { ...base, kind: 'toolCall', toolName: event.toolName, toolCallId: event.toolCallId, input: event.input, output: event.output, result: event.result, durationInMillis: event.durationInMillis }; + case 'modelTurn': + return { ...base, kind: 'modelTurn', model: event.model, requestName: event.requestName, inputTokens: event.inputTokens, outputTokens: event.outputTokens, totalTokens: event.totalTokens, durationInMillis: event.durationInMillis }; + case 'generic': + return { ...base, kind: 'generic', name: event.name, details: event.details, level: event.level, category: event.category }; + case 'subagentInvocation': + return { ...base, kind: 'subagentInvocation', agentName: event.agentName, description: event.description, status: event.status, durationInMillis: event.durationInMillis, toolCallCount: event.toolCallCount, modelTurnCount: event.modelTurnCount }; + case 'userMessage': + return { ...base, kind: 'userMessage', message: event.message, sections: event.sections.map(s => ({ name: s.name, content: s.content })) }; + case 'agentResponse': + return { ...base, kind: 'agentResponse', message: event.message, sections: event.sections.map(s => ({ name: s.name, content: s.content })) }; + } + } + private _reviveEvent(dto: IChatDebugEventDto, sessionResource: URI): IChatDebugEvent { const base = { id: dto.id, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index eb65735e58a..6f5d4d775a1 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1502,6 +1502,8 @@ export type IChatDebugResolvedEventContentDto = IChatDebugEventTextContentDto | export interface ExtHostChatDebugShape { $provideChatDebugLog(handle: number, sessionResource: UriComponents, token: CancellationToken): Promise; $resolveChatDebugLogEvent(handle: number, eventId: string, token: CancellationToken): Promise; + $exportChatDebugLog(handle: number, sessionResource: UriComponents, coreEvents: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise; + $importChatDebugLog(handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined>; } export interface MainThreadChatDebugShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostChatDebug.ts b/src/vs/workbench/api/common/extHostChatDebug.ts index 04125f3e551..b83bb2f3cf5 100644 --- a/src/vs/workbench/api/common/extHostChatDebug.ts +++ b/src/vs/workbench/api/common/extHostChatDebug.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; +import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtHostChatDebugShape, IChatDebugEventDto, IChatDebugResolvedEventContentDto, MainContext, MainThreadChatDebugShape } from './extHost.protocol.js'; -import { ChatDebugMessageContentType, ChatDebugSubagentStatus, ChatDebugToolCallResult } from './extHostTypes.js'; +import { ChatDebugGenericEvent, ChatDebugLogLevel, ChatDebugMessageContentType, ChatDebugMessageSection, ChatDebugModelTurnEvent, ChatDebugSubagentInvocationEvent, ChatDebugSubagentStatus, ChatDebugToolCallEvent, ChatDebugToolCallResult, ChatDebugUserMessageEvent, ChatDebugAgentResponseEvent } from './extHostTypes.js'; import { IExtHostRpcService } from './extHostRpcService.js'; export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShape { @@ -291,6 +292,106 @@ export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShap } } + private _deserializeEvent(dto: IChatDebugEventDto): vscode.ChatDebugEvent | undefined { + const created = new Date(dto.created); + const sessionResource = dto.sessionResource ? URI.revive(dto.sessionResource) : undefined; + switch (dto.kind) { + case 'toolCall': { + const evt = new ChatDebugToolCallEvent(dto.toolName, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.toolCallId = dto.toolCallId; + evt.input = dto.input; + evt.output = dto.output; + evt.result = dto.result === 'success' ? ChatDebugToolCallResult.Success + : dto.result === 'error' ? ChatDebugToolCallResult.Error + : undefined; + evt.durationInMillis = dto.durationInMillis; + return evt; + } + case 'modelTurn': { + const evt = new ChatDebugModelTurnEvent(created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.model = dto.model; + evt.inputTokens = dto.inputTokens; + evt.outputTokens = dto.outputTokens; + evt.totalTokens = dto.totalTokens; + evt.durationInMillis = dto.durationInMillis; + return evt; + } + case 'generic': { + const evt = new ChatDebugGenericEvent(dto.name, dto.level as ChatDebugLogLevel, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.details = dto.details; + evt.category = dto.category; + return evt; + } + case 'subagentInvocation': { + const evt = new ChatDebugSubagentInvocationEvent(dto.agentName, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.description = dto.description; + evt.status = dto.status === 'running' ? ChatDebugSubagentStatus.Running + : dto.status === 'completed' ? ChatDebugSubagentStatus.Completed + : dto.status === 'failed' ? ChatDebugSubagentStatus.Failed + : undefined; + evt.durationInMillis = dto.durationInMillis; + evt.toolCallCount = dto.toolCallCount; + evt.modelTurnCount = dto.modelTurnCount; + return evt; + } + case 'userMessage': { + const evt = new ChatDebugUserMessageEvent(dto.message, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content)); + return evt; + } + case 'agentResponse': { + const evt = new ChatDebugAgentResponseEvent(dto.message, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content)); + return evt; + } + default: + return undefined; + } + } + + async $exportChatDebugLog(_handle: number, sessionResource: UriComponents, coreEventDtos: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise { + if (!this._provider?.provideChatDebugLogExport) { + return undefined; + } + const sessionUri = URI.revive(sessionResource); + const coreEvents = coreEventDtos.map(dto => this._deserializeEvent(dto)).filter((e): e is vscode.ChatDebugEvent => e !== undefined); + const options: vscode.ChatDebugLogExportOptions = { coreEvents, sessionTitle }; + const result = await this._provider.provideChatDebugLogExport(sessionUri, options, token); + if (!result) { + return undefined; + } + return VSBuffer.wrap(result); + } + + async $importChatDebugLog(_handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined> { + if (!this._provider?.resolveChatDebugLogImport) { + return undefined; + } + const result = await this._provider.resolveChatDebugLogImport(data.buffer, token); + if (!result) { + return undefined; + } + return { uri: result.uri, sessionTitle: result.sessionTitle }; + } + override dispose(): void { for (const store of this._activeProgress.values()) { store.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts index 860559d64e3..685ebc58948 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts @@ -3,12 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; +import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; +import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -16,6 +22,7 @@ import { IChatDebugService } from '../../common/chatDebugService.js'; import { ChatViewId, IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js'; import { ChatDebugEditorInput } from '../chatDebug/chatDebugEditorInput.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { IChatDebugEditorOptions } from '../chatDebug/chatDebugTypes.js'; /** @@ -92,4 +99,100 @@ export function registerChatOpenAgentDebugPanelAction() { await editorService.openEditor(ChatDebugEditorInput.instance, options); } }); + + const defaultDebugLogFileName = 'agent-debug-log.json'; + const debugLogFilters = [{ name: localize('chatDebugLog.file.label', "Agent Debug Log"), extensions: ['json'] }]; + + registerAction2(class ExportAgentDebugLogAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.exportAgentDebugLog', + title: localize2('chat.exportAgentDebugLog.label', "Export Agent Debug Log..."), + icon: Codicon.desktopDownload, + f1: true, + category: Categories.Developer, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID), + order: 10 + }], + }); + } + + async run(accessor: ServicesAccessor): Promise { + const chatDebugService = accessor.get(IChatDebugService); + const fileDialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + + const sessionResource = chatDebugService.activeSessionResource; + if (!sessionResource) { + notificationService.notify({ severity: Severity.Info, message: localize('chatDebugLog.noSession', "No active debug session to export. Navigate to a session first.") }); + return; + } + + const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName); + const outputPath = await fileDialogService.showSaveDialog({ defaultUri, filters: debugLogFilters }); + if (!outputPath) { + return; + } + + const data = await chatDebugService.exportLog(sessionResource); + if (!data) { + notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.exportFailed', "Export is not supported by the current provider.") }); + return; + } + + await fileService.writeFile(outputPath, VSBuffer.wrap(data)); + } + }); + + registerAction2(class ImportAgentDebugLogAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.importAgentDebugLog', + title: localize2('chat.importAgentDebugLog.label', "Import Agent Debug Log..."), + icon: Codicon.cloudUpload, + f1: true, + category: Categories.Developer, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID), + order: 11 + }], + }); + } + + async run(accessor: ServicesAccessor): Promise { + const chatDebugService = accessor.get(IChatDebugService); + const editorService = accessor.get(IEditorService); + const fileDialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + + const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName); + const result = await fileDialogService.showOpenDialog({ + defaultUri, + canSelectFiles: true, + filters: debugLogFilters + }); + if (!result) { + return; + } + + const content = await fileService.readFile(result[0]); + const sessionUri = await chatDebugService.importLog(content.value.buffer); + if (!sessionUri) { + notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.importFailed', "Import is not supported by the current provider.") }); + return; + } + + const options: IChatDebugEditorOptions = { pinned: true, sessionResource: sessionUri, viewHint: 'overview' }; + await editorService.openEditor(ChatDebugEditorInput.instance, options); + } + }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index 8276d701b2a..867053d97ac 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -65,9 +65,6 @@ export class ChatDebugEditor extends EditorPane { private readonly sessionModelListener = this._register(new MutableDisposable()); private readonly modelChangeListeners = this._register(new DisposableMap()); - /** Saved session resource so we can restore it after the editor is re-shown. */ - private savedSessionResource: URI | undefined; - /** * Stops the streaming pipeline and clears cached events for the * active session. Called when navigating away from a session or @@ -175,7 +172,10 @@ export class ChatDebugEditor extends EditorPane { this._register(this.chatService.onDidCreateModel(model => { if (this.viewState === ViewState.Home) { - this.homeView?.render(); + // Auto-navigate to the new session when the debug panel is + // already open on the home view. This avoids the user having to + // wait for the title to resolve and manually clicking the session. + this.navigateToSession(model.sessionResource); } // Track title changes per model, disposing the previous listener @@ -307,40 +307,11 @@ export class ChatDebugEditor extends EditorPane { super.setEditorVisible(visible); if (visible) { this.telemetryService.publicLog2<{}, ChatDebugPanelOpenedClassification>('chatDebugPanelOpened'); - // Note: do NOT read this.options here. When the editor becomes - // visible via openEditor(), setEditorVisible fires before - // setOptions, so this.options still contains stale values from - // the previous openEditor() call. Navigation from new options - // is handled entirely by setOptions → _applyNavigationOptions. - // Here we only restore the previous state when the editor is - // re-shown without a new openEditor() call (e.g., tab switch). - if (this.viewState === ViewState.Home) { - const sessionResource = this.chatDebugService.activeSessionResource ?? this.savedSessionResource; - this.savedSessionResource = undefined; - if (sessionResource) { - this.navigateToSession(sessionResource, 'overview'); - } else { - this.showView(ViewState.Home); - } - } else { - // Re-activate the streaming pipeline for the current session, - // restoring the saved session resource if the editor was temporarily hidden. - const sessionResource = this.chatDebugService.activeSessionResource ?? this.savedSessionResource; - this.savedSessionResource = undefined; - if (sessionResource) { - this.chatDebugService.activeSessionResource = sessionResource; - if (!this.chatDebugService.hasInvokedProviders(sessionResource)) { - this.chatDebugService.invokeProviders(sessionResource); - } - } else { - this.showView(ViewState.Home); - } - } - } else { - // Remember the active session so we can restore when re-shown - this.savedSessionResource = this.chatDebugService.activeSessionResource; - // Stop the streaming pipeline when the editor is hidden - this.endActiveSession(); + // Re-show the current view so it reloads events from scratch, + // ensuring correct ordering and no stale duplicates. + // Navigation from new openEditor() options is handled by + // setOptions → _applyNavigationOptions (fires after this). + this.showView(this.viewState); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts index 442c56360b1..48e5ce0dced 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts @@ -179,13 +179,18 @@ export function buildFlowGraph(events: readonly IChatDebugEvent[]): FlowNode[] { // For subagent invocations, enrich with description from the // filtered-out completion sibling, or fall back to the event's own field. - let sublabel = getEventSublabel(event, effectiveKind); + let label = getEventLabel(event, effectiveKind); + const sublabel = getEventSublabel(event, effectiveKind); let tooltip = getEventTooltip(event); let description: string | undefined; if (effectiveKind === 'subagentInvocation') { description = getSubagentDescription(event); + // Show "Subagent: " as the label so users can identify + // these nodes and see what task they perform. + label = description + ? localize('subagentWithDesc', "Subagent: {0}", truncateLabel(description, 30)) + : localize('subagentLabel', "Subagent"); if (description) { - sublabel = truncateLabel(description, 30) + (sublabel ? ` \u00b7 ${sublabel}` : ''); // Ensure description appears in tooltip if not already present if (tooltip && !tooltip.includes(description)) { const lines = tooltip.split('\n'); @@ -199,7 +204,7 @@ export function buildFlowGraph(events: readonly IChatDebugEvent[]): FlowNode[] { id: event.id ?? `event-${events.indexOf(event)}`, kind: effectiveKind, category: event.kind === 'generic' ? event.category : undefined, - label: getEventLabel(event, effectiveKind), + label, sublabel, description, tooltip, @@ -524,29 +529,17 @@ function getEventLabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEvent[' const kind = effectiveKind ?? event.kind; switch (kind) { case 'userMessage': - return localize('userLabel', "User"); + return localize('userLabel', "User Message"); case 'modelTurn': return event.kind === 'modelTurn' ? (event.model ?? localize('modelTurnLabel', "Model Turn")) : localize('modelTurnLabel', "Model Turn"); case 'toolCall': - return event.kind === 'toolCall' ? event.toolName : event.kind === 'generic' ? event.name : ''; + return event.kind === 'toolCall' ? event.toolName : event.kind === 'generic' ? event.name : localize('toolCallLabel', "Tool Call"); case 'subagentInvocation': - return event.kind === 'subagentInvocation' ? event.agentName : ''; - case 'agentResponse': { - if (event.kind === 'agentResponse') { - return event.message || localize('responseLabel', "Response"); - } - // Remapped generic event — extract model name from parenthesized suffix - // e.g. "Agent response (claude-opus-4.5)" → "claude-opus-4.5" - if (event.kind === 'generic') { - const match = /\(([^)]+)\)\s*$/.exec(event.name); - if (match) { - return match[1]; - } - } - return localize('responseLabel', "Response"); - } + return event.kind === 'subagentInvocation' ? event.agentName : localize('subagentFallback', "Subagent"); + case 'agentResponse': + return localize('agentResponseLabel', "Agent Response"); case 'generic': - return event.kind === 'generic' ? event.name : ''; + return event.kind === 'generic' ? event.name : localize('genericLabel', "Event"); } } @@ -588,30 +581,32 @@ function getEventSublabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEven } case 'userMessage': case 'agentResponse': { - // For proper typed events, prefer the first section's content - // (which has the actual message text) over the `message` field - // (which is a short summary/name). Fall back to `message` when - // no sections are available. For remapped generic events, use - // the details property. + // Use the message summary as the sublabel. For remapped generic + // events, use the details property. let text: string | undefined; if (event.kind === 'userMessage' || event.kind === 'agentResponse') { - text = event.sections[0]?.content || event.message; + text = event.message; } else if (event.kind === 'generic') { text = event.details; } if (!text) { return undefined; } - // Find the first non-empty line (content may start with newlines) + // Find the first meaningful line, skipping trivial lines like + // lone brackets/braces that appear when the message is JSON. const lines = text.split('\n'); let firstLine = ''; for (const line of lines) { const trimmed = line.trim(); - if (trimmed) { + if (trimmed && trimmed.length > 2) { firstLine = trimmed; break; } } + if (!firstLine) { + // Fall back to the full text collapsed to a single line + firstLine = text.replace(/\s+/g, ' ').trim(); + } if (!firstLine) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts index cf9dd80103e..403003e62bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts @@ -159,7 +159,15 @@ function measureNodeWidth(label: string, sublabel?: string): number { } function subgraphHeaderLabel(node: FlowNode): string { - return node.description ? `${node.label}: ${node.description}` : node.label; + // For subagent nodes, the label already includes the description + // (e.g. "Subagent: Count markdown files"), so don't append it again. + if (node.kind === 'subagentInvocation') { + return node.label; + } + if (node.description && node.description !== node.label) { + return `${node.label}: ${node.description}`; + } + return node.label; } function measureSubgraphHeaderWidth(headerLabel: string): number { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts index f167776e078..0492768b660 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts @@ -85,7 +85,24 @@ export class ChatDebugHomeView extends Disposable { const items: HTMLButtonElement[] = []; for (const sessionResource of sessionResources) { - const sessionTitle = this.chatService.getSessionTitle(sessionResource) || LocalChatSessionUri.parseLocalSessionId(sessionResource) || sessionResource.toString(); + const rawTitle = this.chatService.getSessionTitle(sessionResource); + let sessionTitle: string; + if (rawTitle && !isUUID(rawTitle)) { + sessionTitle = rawTitle; + } else if (LocalChatSessionUri.isLocalSession(sessionResource)) { + sessionTitle = localize('chatDebug.newSession', "New Chat"); + } else { + // For imported/external sessions, use the stored title if available + const importedTitle = this.chatDebugService.getImportedSessionTitle(sessionResource); + if (importedTitle) { + sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", importedTitle); + } else { + // Fall back to URI segment + const uriLabel = sessionResource.path || sessionResource.fragment || sessionResource.toString(); + const segment = uriLabel.replace(/^\/+/, '').split('/').pop() || uriLabel; + sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", segment); + } + } const isActive = activeSessionResource !== undefined && sessionResource.toString() === activeSessionResource.toString(); const item = DOM.append(sessionList, $('button.chat-debug-home-session-item')); @@ -98,32 +115,20 @@ export class ChatDebugHomeView extends Disposable { DOM.append(item, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`)); const titleSpan = DOM.append(item, $('span.chat-debug-home-session-item-title')); - // Show shimmer when the title is still a UUID — the session is - // either not yet loaded or hasn't produced a real title yet. - const isShimmering = isUUID(sessionTitle); - if (isShimmering) { - titleSpan.classList.add('chat-debug-home-session-item-shimmer'); - item.disabled = true; - item.setAttribute('aria-busy', 'true'); - item.setAttribute('aria-label', localize('chatDebug.loadingSession', "Loading session…")); - } else { - titleSpan.textContent = sessionTitle; - const ariaLabel = isActive - ? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle) - : sessionTitle; - item.setAttribute('aria-label', ariaLabel); - } + titleSpan.textContent = sessionTitle; + const ariaLabel = isActive + ? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle) + : sessionTitle; + item.setAttribute('aria-label', ariaLabel); if (isActive) { DOM.append(item, $('span.chat-debug-home-session-badge', undefined, localize('chatDebug.active', "Active"))); } - if (!isShimmering) { - this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => { - this._onNavigateToSession.fire(sessionResource); - })); - items.push(item); - } + this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => { + this._onNavigateToSession.fire(sessionResource); + })); + items.push(item); } // Arrow key navigation between session items diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index 8cbf8c1a90d..550fa005b81 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -12,6 +12,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; +import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -63,6 +64,7 @@ export class ChatDebugLogsView extends Disposable { private currentDimension: Dimension | undefined; private readonly eventListener = this._register(new MutableDisposable()); private readonly sessionStateDisposable = this._register(new MutableDisposable()); + private readonly refreshScheduler: RunOnceScheduler; private shimmerRow!: HTMLElement; constructor( @@ -75,6 +77,7 @@ export class ChatDebugLogsView extends Disposable { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); + this.refreshScheduler = this._register(new RunOnceScheduler(() => this.refreshList(), 50)); this.container = DOM.append(parent, $('.chat-debug-logs')); DOM.hide(this.container); @@ -383,8 +386,32 @@ export class ChatDebugLogsView extends Disposable { } addEvent(event: IChatDebugEvent): void { - this.events.push(event); - this.refreshList(); + // Binary-insert to maintain chronological order without a full sort. + // Events almost always arrive in order, so the insertion point is + // typically at the end (O(log n) comparison, O(1) splice). + const time = event.created.getTime(); + let lo = 0; + let hi = this.events.length; + while (lo < hi) { + const mid = (lo + hi) >>> 1; + if (this.events[mid].created.getTime() <= time) { + lo = mid + 1; + } else { + hi = mid; + } + } + if (lo === this.events.length) { + this.events.push(event); + } else { + this.events.splice(lo, 0, event); + } + this.scheduleRefresh(); + } + + private scheduleRefresh(): void { + if (!this.refreshScheduler.isScheduled()) { + this.refreshScheduler.schedule(); + } } private loadEvents(): void { @@ -392,8 +419,7 @@ export class ChatDebugLogsView extends Disposable { const addEventDisposable = this.chatDebugService.onDidAddEvent(e => { if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) { - this.events.push(e); - this.refreshList(); + this.addEvent(e); } }); diff --git a/src/vs/workbench/contrib/chat/common/chatDebugService.ts b/src/vs/workbench/contrib/chat/common/chatDebugService.ts index a059f0aa836..889e496e9b1 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugService.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugService.ts @@ -196,6 +196,34 @@ export interface IChatDebugService extends IDisposable { */ resolveEvent(eventId: string): Promise; + /** + /** + * Export the debug log for a session via the registered provider. + */ + exportLog(sessionResource: URI): Promise; + + /** + * Import a previously exported debug log via the registered provider. + * Returns the session URI for the imported data. + */ + importLog(data: Uint8Array): Promise; + + /** + * Returns true if the event was logged by VS Code core + * (not sourced from an external provider). + */ + isCoreEvent(event: IChatDebugEvent): boolean; + + /** + * Store a human-readable title for an imported session. + */ + setImportedSessionTitle(sessionResource: URI, title: string): void; + + /** + * Get the stored title for an imported session, if available. + */ + getImportedSessionTitle(sessionResource: URI): string | undefined; + /** * Fired when debug data is attached to a session. */ @@ -317,4 +345,6 @@ export type IChatDebugResolvedEventContent = IChatDebugEventTextContent | IChatD export interface IChatDebugLogProvider { provideChatDebugLog(sessionResource: URI, token: CancellationToken): Promise; resolveChatDebugLogEvent?(eventId: string, token: CancellationToken): Promise; + provideChatDebugLogExport?(sessionResource: URI, token: CancellationToken): Promise; + resolveChatDebugLogImport?(data: Uint8Array, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts index 9cdd711a311..c80186d968d 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts @@ -39,6 +39,12 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic /** Events that were returned by providers (not internally logged). */ private readonly _providerEvents = new WeakSet(); + /** Session URIs created via import, allowed through the invokeProviders guard. */ + private readonly _importedSessions = new ResourceMap(); + + /** Human-readable titles for imported sessions. */ + private readonly _importedSessionTitles = new ResourceMap(); + activeSessionResource: URI | undefined; log(sessionResource: URI, name: string, details?: string, level: ChatDebugLogLevel = ChatDebugLogLevel.Info, options?: { id?: string; category?: string; parentEventId?: string }): void { @@ -135,10 +141,10 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic } async invokeProviders(sessionResource: URI): Promise { - if (!LocalChatSessionUri.isLocalSession(sessionResource)) { + + if (!LocalChatSessionUri.isLocalSession(sessionResource) && !this._importedSessions.has(sessionResource)) { return; } - // Cancel only the previous invocation for THIS session, not others. // Each session has its own pipeline so events from multiple sessions // can be streamed concurrently. @@ -247,6 +253,51 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic return undefined; } + isCoreEvent(event: IChatDebugEvent): boolean { + return !this._providerEvents.has(event); + } + + setImportedSessionTitle(sessionResource: URI, title: string): void { + this._importedSessionTitles.set(sessionResource, title); + } + + getImportedSessionTitle(sessionResource: URI): string | undefined { + return this._importedSessionTitles.get(sessionResource); + } + + async exportLog(sessionResource: URI): Promise { + for (const provider of this._providers) { + if (provider.provideChatDebugLogExport) { + try { + const data = await provider.provideChatDebugLogExport(sessionResource, CancellationToken.None); + if (data !== undefined) { + return data; + } + } catch (err) { + onUnexpectedError(err); + } + } + } + return undefined; + } + + async importLog(data: Uint8Array): Promise { + for (const provider of this._providers) { + if (provider.resolveChatDebugLogImport) { + try { + const sessionUri = await provider.resolveChatDebugLogImport(data, CancellationToken.None); + if (sessionUri !== undefined) { + this._importedSessions.set(sessionUri, true); + return sessionUri; + } + } catch (err) { + onUnexpectedError(err); + } + } + } + return undefined; + } + override dispose(): void { for (const cts of this._invocationCts.values()) { cts.cancel(); diff --git a/src/vscode-dts/vscode.proposed.chatDebug.d.ts b/src/vscode-dts/vscode.proposed.chatDebug.d.ts index f74f4e7ba11..3fe781d29fc 100644 --- a/src/vscode-dts/vscode.proposed.chatDebug.d.ts +++ b/src/vscode-dts/vscode.proposed.chatDebug.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 2 +// version: 3 declare module 'vscode' { /** @@ -642,6 +642,37 @@ declare module 'vscode' { eventId: string, token: CancellationToken ): ProviderResult; + + /** + * Export the debug log for a chat session as a serialized byte array. + * The extension controls the format (e.g., OTLP JSON with Copilot extensions). + * Core provides the save dialog and writes the returned bytes to disk. + * + * @param sessionResource The resource URI of the chat session to export. + * @param options Export options including core events and session metadata. + * @param token A cancellation token. + * @returns The serialized debug log data, or undefined if export is not available. + */ + provideChatDebugLogExport?( + sessionResource: Uri, + options: ChatDebugLogExportOptions, + token: CancellationToken + ): ProviderResult; + + /** + * Import a previously exported debug log from a serialized byte array. + * Core provides the open dialog and reads the file bytes. + * The extension deserializes the data and returns a session URI that can be + * opened in the debug panel via {@link provideChatDebugLog}. + * + * @param data The serialized debug log data (as returned by {@link provideChatDebugLogExport}). + * @param token A cancellation token. + * @returns The imported session info, or undefined if import failed. + */ + resolveChatDebugLogImport?( + data: Uint8Array, + token: CancellationToken + ): ProviderResult; } export namespace chat { @@ -654,4 +685,36 @@ declare module 'vscode' { */ export function registerChatDebugLogProvider(provider: ChatDebugLogProvider): Disposable; } + + /** + * Options passed to {@link ChatDebugLogProvider.provideChatDebugLogExport}. + */ + export interface ChatDebugLogExportOptions { + /** + * Core-originated debug events (prompt discovery, skill loading, etc.) + * for the session. The extension may include these in the export alongside its own data. + */ + readonly coreEvents: readonly ChatDebugEvent[]; + + /** + * Session title, if available. + * Used to provide a human-readable label in the exported file. + */ + readonly sessionTitle?: string; + } + + /** + * Result of importing a debug log via {@link ChatDebugLogProvider.resolveChatDebugLogImport}. + */ + export interface ChatDebugLogImportResult { + /** + * The session resource URI for the imported session. + */ + readonly uri: Uri; + + /** + * The session title from the imported file, if available. + */ + readonly sessionTitle?: string; + } } From eb7e1c7cb89fc44d19bc3d1004ce847869795a38 Mon Sep 17 00:00:00 2001 From: Jamie Cansdale Date: Tue, 10 Mar 2026 04:05:33 +0000 Subject: [PATCH 395/448] fix: chunk multiline PTY writes on macOS to avoid 1024-byte buffer corruption (#298993) * test: add multiline PTY write test for macOS 1024-byte buffer bug Adds a test that sends multiline commands of varying sizes (10, 20, 30 lines) through TerminalProcess.input() and verifies the data arrives intact at the shell. On macOS, multiline commands exceeding ~1024 bytes corrupt due to PTY canonical-mode input buffer backpressure. Reproduces: #296955 * fix: chunk multiline PTY writes on macOS to avoid 1024-byte buffer corruption macOS PTY has a ~1024-byte canonical-mode input buffer. When multiline data (containing CR characters) exceeds this threshold, the shell's line editor echoes characters back, creating backpressure that corrupts the write. Write multiline PTY input in 512-byte chunks with 5ms pauses between them to allow the echo buffer to drain. Non-macOS platforms and single-line writes are unaffected. Fixes #296955 * test: increase large multiline test to 500 lines (~32KB) --- .../platform/terminal/node/terminalProcess.ts | 21 +++ .../test/node/terminalProcess.test.ts | 139 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 src/vs/platform/terminal/test/node/terminalProcess.test.ts diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 60563ff6487..e684180e35d 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -110,6 +110,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _isPtyPaused: boolean = false; private _unacknowledgedCharCount: number = 0; + private _writeQueue: Promise = Promise.resolve(); get exitMessage(): string | undefined { return this._exitMessage; } get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } @@ -468,12 +469,32 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._logService.trace('node-pty.IPty#write', data, isBinary); if (isBinary) { this._ptyProcess!.write(Buffer.from(data, 'binary')); + } else if (isMacintosh && data.length > 512 && data.includes('\r')) { + // macOS PTY has a ~1024-byte canonical-mode input buffer. Multiline + // input exceeding this causes writes to block or corrupt due to + // backpressure from the shell's line editor echoing characters. + // https://github.com/microsoft/vscode/issues/296955 + this._writeChunked(data); } else { this._ptyProcess!.write(data); } this._childProcessMonitor?.handleInput(); } + private _writeChunked(data: string): void { + this._writeQueue = this._writeQueue.then(async () => { + for (let i = 0; i < data.length; i += 512) { + if (this._store.isDisposed) { + return; + } + this._ptyProcess!.write(data.slice(i, i + 512)); + if (i + 512 < data.length) { + await timeout(5); + } + } + }); + } + sendSignal(signal: string): void { if (this._store.isDisposed || !this._ptyProcess) { return; diff --git a/src/vs/platform/terminal/test/node/terminalProcess.test.ts b/src/vs/platform/terminal/test/node/terminalProcess.test.ts new file mode 100644 index 00000000000..07e1dd16ae8 --- /dev/null +++ b/src/vs/platform/terminal/test/node/terminalProcess.test.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual } from 'assert'; +import { tmpdir } from 'os'; +import * as path from '../../../../base/common/path.js'; +import * as fs from 'fs'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../log/common/log.js'; +import { IProductService } from '../../../product/common/productService.js'; +import { ITerminalProcessOptions, ITerminalLaunchError } from '../../common/terminal.js'; +import { TerminalProcess } from '../../node/terminalProcess.js'; +import { isWindows } from '../../../../base/common/platform.js'; + +const processOptions: ITerminalProcessOptions = { + shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, + windowsUseConptyDll: false, + environmentVariableCollections: undefined, + workspaceFolder: undefined, + isScreenReaderOptimized: false +}; + +/** + * Build a multiline shell command that writes its content to a file. + * The command writes numbered lines to a temp file so we can verify + * the entire payload was received intact by the shell. + */ +function buildMultilineCommand(lineCount: number, outputFile: string): { command: string; expectedLines: string[] } { + const lines: string[] = []; + for (let i = 1; i <= lineCount; i++) { + // Pad line number, add filler to make each line ~55 chars + const line = `L${String(i).padStart(2, '0')} ${'a'.repeat(51)}`; + lines.push(line); + } + // Use cat heredoc to write content to a file — this exercises multiline PTY input + const command = `cat > ${outputFile} << 'TESTEOF'\n${lines.join('\n')}\nTESTEOF\n`; + return { command, expectedLines: lines }; +} + +// These tests spawn real PTY processes and are macOS/Linux only +(isWindows ? suite.skip : suite)('TerminalProcess - multiline write', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let outputDir: string; + + setup(() => { + outputDir = fs.mkdtempSync(path.join(tmpdir(), 'vscode-pty-test-')); + }); + + teardown(() => { + fs.rmSync(outputDir, { recursive: true, force: true }); + }); + + async function runMultilineTest(lineCount: number): Promise { + const outputFile = path.join(outputDir, `output-${lineCount}.txt`); + const { command, expectedLines } = buildMultilineCommand(lineCount, outputFile); + + const terminalProcess = store.add(new TerminalProcess( + { executable: '/bin/bash', args: ['--norc', '--noprofile', '-i'] }, + outputDir, + 80, + 24, + { ...process.env } as Record, + { ...process.env } as Record, + processOptions, + new NullLogService(), + { applicationName: 'vscode' } as IProductService + )); + + const result = await terminalProcess.start(); + const error = result as ITerminalLaunchError | undefined; + if (error?.message) { + throw new Error(`Failed to start terminal: ${error.message}`); + } + + // Wait for shell to produce output (prompt), indicating it's ready for input + await new Promise(resolve => { + const timeout = setTimeout(() => { + listener.dispose(); + resolve(); + }, 10000); + const listener = terminalProcess.onProcessData(() => { + clearTimeout(timeout); + listener.dispose(); + resolve(); + }); + }); + + // Send the multiline command — newlines are converted to \r for PTY + const ptyData = command.replace(/\n/g, '\r'); + terminalProcess.input(ptyData); + + // Wait for the command to execute and write the file + const maxWait = 10000; + const start = Date.now(); + while (Date.now() - start < maxWait) { + await new Promise(resolve => setTimeout(resolve, 200)); + if (fs.existsSync(outputFile)) { + // Give a moment for the write to flush + await new Promise(resolve => setTimeout(resolve, 200)); + break; + } + } + + // Shut down and wait for the process to exit + const exitPromise = new Promise(resolve => { + const listener = terminalProcess.onProcessExit(() => { + listener.dispose(); + resolve(); + }); + }); + terminalProcess.shutdown(true); + await exitPromise; + + if (!fs.existsSync(outputFile)) { + throw new Error(`Output file was not created — terminal likely got stuck (command was ${command.length} bytes)`); + } + + const actualContent = fs.readFileSync(outputFile, 'utf-8'); + const actualLines = actualContent.trimEnd().split('\n'); + deepStrictEqual(actualLines, expectedLines); + } + + test('small multiline command (10 lines, ~700 bytes)', async function () { + this.timeout(15000); + await runMultilineTest(10); + }); + + test('medium multiline command (20 lines, ~1300 bytes)', async function () { + this.timeout(15000); + await runMultilineTest(20); + }); + + test('large multiline command (500 lines, ~32KB)', async function () { + this.timeout(30000); + await runMultilineTest(500); + }); +}); From 018f88abe82bb8ffcfc4bdde7596c8d0292b3dad Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:24:34 -0700 Subject: [PATCH 396/448] Remove unused import --- src/vs/workbench/contrib/chat/common/chatSessionsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index d693e5b34b7..d584e8295e0 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -14,7 +14,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './participants/chatAgents.js'; import { IChatEditingSession } from './editing/chatEditingService.js'; import { IChatModel, IChatRequestVariableData, ISerializableChatModelInputState } from './model/chatModel.js'; -import { IChatProgress, IChatService, IChatSessionTiming } from './chatService/chatService.js'; +import { IChatProgress, IChatSessionTiming } from './chatService/chatService.js'; import { Target } from './promptSyntax/promptTypes.js'; export const enum ChatSessionStatus { From e2db4495b34a5bd799b29824fc8d12eb5082545f Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 9 Mar 2026 22:25:06 -0700 Subject: [PATCH 397/448] Improve slash command render (#300287) --- .../contrib/chat/browser/widget/chatWidget.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 88cf0e53f6f..208dd704892 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -23,7 +23,7 @@ import { Schemas } from '../../../../../base/common/network.js'; import { IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { filter } from '../../../../../base/common/objects.js'; import { autorun, derived, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; -import { basename, extUri, isEqual } from '../../../../../base/common/resources.js'; +import { extUri, isEqual } from '../../../../../base/common/resources.js'; import { MicrotaskDelay } from '../../../../../base/common/symbols.js'; import { isDefined } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -2187,9 +2187,6 @@ export class ChatWidget extends Disposable implements IChatWidget { const toolReferences = this.toolsService.toToolReferences(refs); requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences)); - // remove the slash command from the input - requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); - const promptPath = slashCommand.promptPath; const promptRunEvent: ChatPromptRunEvent = { storage: promptPath.storage, @@ -2202,12 +2199,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.telemetryService.publicLog2('chat.promptRun', promptRunEvent); - const input = requestInput.input.trim(); - requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`; - if (input) { - // if the input is not empty, append it to the prompt - requestInput.input += `\n${input}`; - } if (parseResult.header) { await this._applyPromptMetadata(parseResult.header, requestInput); } From b44b56dfa8d0a9e1dd1a7294c97b88b80f273a0c Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:43:39 -0700 Subject: [PATCH 398/448] Add deprecation message on registerChatSessionItemProvider Let's see how many callers are out there for this deprecated api --- src/vs/workbench/api/common/extHost.api.impl.ts | 3 +++ .../api/common/extHostApiDeprecationService.ts | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 37251c6f672..d1de91ab992 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1626,6 +1626,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerChatSessionItemProvider: (chatSessionType: string, provider: vscode.ChatSessionItemProvider) => { checkProposedApiEnabled(extension, 'chatSessionsProvider'); + extHostApiDeprecation.report('chat.registerChatSessionItemProvider', extension, `Please migrate to the new chat session controller API`, { + usageId: chatSessionType + }); return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider); }, createChatSessionItemController: (chatSessionType: string, refreshHandler: (token: vscode.CancellationToken) => Thenable) => { diff --git a/src/vs/workbench/api/common/extHostApiDeprecationService.ts b/src/vs/workbench/api/common/extHostApiDeprecationService.ts index 9a72ce444c0..9d6597fb947 100644 --- a/src/vs/workbench/api/common/extHostApiDeprecationService.ts +++ b/src/vs/workbench/api/common/extHostApiDeprecationService.ts @@ -12,7 +12,7 @@ import { IExtHostRpcService } from './extHostRpcService.js'; export interface IExtHostApiDeprecationService { readonly _serviceBrand: undefined; - report(apiId: string, extension: IExtensionDescription, migrationSuggestion: string): void; + report(apiId: string, extension: IExtensionDescription, migrationSuggestion: string, options?: { usageId?: string }): void; } export const IExtHostApiDeprecationService = createDecorator('IExtHostApiDeprecationService'); @@ -31,8 +31,8 @@ export class ExtHostApiDeprecationService implements IExtHostApiDeprecationServi this._telemetryShape = rpc.getProxy(extHostProtocol.MainContext.MainThreadTelemetry); } - public report(apiId: string, extension: IExtensionDescription, migrationSuggestion: string): void { - const key = this.getUsageKey(apiId, extension); + public report(apiId: string, extension: IExtensionDescription, migrationSuggestion: string, options?: { usageId?: string }): void { + const key = this.getUsageKey(apiId, extension, options?.usageId); if (this._reportedUsages.has(key)) { return; } @@ -45,21 +45,24 @@ export class ExtHostApiDeprecationService implements IExtHostApiDeprecationServi type DeprecationTelemetry = { extensionId: string; apiId: string; + usageId: string; }; type DeprecationTelemetryMeta = { extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The id of the extension that is using the deprecated API' }; apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The id of the deprecated API' }; + usageId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Id identifying the specific usage of the deprecated API' }; owner: 'mjbvz'; comment: 'Helps us gain insights on extensions using deprecated API so we can assist in migration to new API'; }; this._telemetryShape.$publicLog2('extHostDeprecatedApiUsage', { extensionId: extension.identifier.value, apiId: apiId, + usageId: options?.usageId ?? '', }); } - private getUsageKey(apiId: string, extension: IExtensionDescription): string { - return `${apiId}-${extension.identifier.value}`; + private getUsageKey(apiId: string, extension: IExtensionDescription, usageId?: string): string { + return `${apiId}-${extension.identifier.value}-${usageId ?? 'default'}`; } } From a303f977d349fb66335f4839e6c7dcb1d75b820a Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:51:06 -0700 Subject: [PATCH 399/448] Don't use 'default' --- src/vs/workbench/api/common/extHostApiDeprecationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostApiDeprecationService.ts b/src/vs/workbench/api/common/extHostApiDeprecationService.ts index 9d6597fb947..da3efe1bad3 100644 --- a/src/vs/workbench/api/common/extHostApiDeprecationService.ts +++ b/src/vs/workbench/api/common/extHostApiDeprecationService.ts @@ -62,7 +62,8 @@ export class ExtHostApiDeprecationService implements IExtHostApiDeprecationServi } private getUsageKey(apiId: string, extension: IExtensionDescription, usageId?: string): string { - return `${apiId}-${extension.identifier.value}-${usageId ?? 'default'}`; + const rootKey = `${apiId}-${extension.identifier.value}`; + return usageId ? `${rootKey}-${usageId}` : rootKey; } } From 79c14564df64d607bc0adca9132a370c2f2c9997 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:14:30 -0700 Subject: [PATCH 400/448] Reduce any usage in consoleForwarder For #269213 --- src/vs/workbench/api/common/extHostConsoleForwarder.ts | 10 +++++----- src/vs/workbench/api/node/extHostConsoleForwarder.ts | 5 ++--- src/vs/workbench/api/worker/extHostConsoleForwarder.ts | 5 ++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/api/common/extHostConsoleForwarder.ts b/src/vs/workbench/api/common/extHostConsoleForwarder.ts index 02559149900..8bd7fc9a912 100644 --- a/src/vs/workbench/api/common/extHostConsoleForwarder.ts +++ b/src/vs/workbench/api/common/extHostConsoleForwarder.ts @@ -46,13 +46,13 @@ export abstract class AbstractExtHostConsoleForwarder { Object.defineProperty(console, method, { set: () => { }, - get: () => function () { - that._handleConsoleCall(method, severity, original, arguments); + get: () => (...args: unknown[]) => { + that._handleConsoleCall(method, severity, original, args); }, }); } - private _handleConsoleCall(method: 'log' | 'info' | 'warn' | 'error' | 'debug', severity: 'log' | 'warn' | 'error' | 'debug', original: (...args: any[]) => void, args: IArguments): void { + private _handleConsoleCall(method: 'log' | 'info' | 'warn' | 'error' | 'debug', severity: 'log' | 'warn' | 'error' | 'debug', original: (...args: unknown[]) => void, args: unknown[]): void { this._mainThreadConsole.$logExtensionHostMessage({ type: '__$console', severity, @@ -63,7 +63,7 @@ export abstract class AbstractExtHostConsoleForwarder { } } - protected abstract _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error' | 'debug', original: (...args: any[]) => void, args: IArguments): void; + protected abstract _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error' | 'debug', original: (...args: unknown[]) => void, args: unknown[]): void; } @@ -72,7 +72,7 @@ const MAX_LENGTH = 100000; /** * Prevent circular stringify and convert arguments to real array */ -function safeStringifyArgumentsToArray(args: IArguments, includeStack: boolean): string { +function safeStringifyArgumentsToArray(args: unknown[], includeStack: boolean): string { const argsArray = []; // Massage some arguments with special treatment diff --git a/src/vs/workbench/api/node/extHostConsoleForwarder.ts b/src/vs/workbench/api/node/extHostConsoleForwarder.ts index d1568f28642..69ce4029ca0 100644 --- a/src/vs/workbench/api/node/extHostConsoleForwarder.ts +++ b/src/vs/workbench/api/node/extHostConsoleForwarder.ts @@ -24,12 +24,11 @@ export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder { this._wrapStream('stdout', 'log'); } - protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error' | 'debug', original: (...args: any[]) => void, args: IArguments) { + protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error' | 'debug', original: (...args: unknown[]) => void, args: unknown[]): void { const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout; this._isMakingConsoleCall = true; stream.write(`\n${NativeLogMarkers.Start}\n`); - // eslint-disable-next-line local/code-no-any-casts - original.apply(console, args as any); + original.apply(console, args); stream.write(`\n${NativeLogMarkers.End}\n`); this._isMakingConsoleCall = false; } diff --git a/src/vs/workbench/api/worker/extHostConsoleForwarder.ts b/src/vs/workbench/api/worker/extHostConsoleForwarder.ts index 01814a7d969..26eec2f9ad3 100644 --- a/src/vs/workbench/api/worker/extHostConsoleForwarder.ts +++ b/src/vs/workbench/api/worker/extHostConsoleForwarder.ts @@ -16,8 +16,7 @@ export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder { super(extHostRpc, initData); } - protected override _nativeConsoleLogMessage(_method: unknown, original: (...args: any[]) => void, args: IArguments) { - // eslint-disable-next-line local/code-no-any-casts - original.apply(console, args as any); + protected override _nativeConsoleLogMessage(_method: unknown, original: (...args: unknown[]) => void, args: unknown[]) { + original.apply(console, args); } } From 9ac9ae8a1009d33ac05f8aaaaeb1e43bb0c75b82 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:17:36 -0700 Subject: [PATCH 401/448] Reduce `any` usage in a few more tests For #269213 --- .../test/browser/quickinput.test.ts | 3 +- .../cdpAccessibilityDomain.test.ts | 7 +- .../requestParser/chatRequestParser.test.ts | 166 +++++++----------- 3 files changed, 71 insertions(+), 105 deletions(-) diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 217fef39906..db57dedbffa 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -66,8 +66,7 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); instantiationService.stub(IListService, store.add(new ListService())); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(ILayoutService, { activeContainer: fixture, onDidLayoutContainer: Event.None } as any); + instantiationService.stub(ILayoutService, { _serviceBrand: undefined, activeContainer: fixture, onDidLayoutContainer: Event.None }); instantiationService.stub(IContextViewService, store.add(instantiationService.createInstance(ContextViewService))); instantiationService.stub(IContextKeyService, store.add(instantiationService.createInstance(ContextKeyService))); instantiationService.stub(IKeybindingService, { diff --git a/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts b/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts index 50859223195..f4ec49d643b 100644 --- a/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts +++ b/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; -import { AXNode, AXProperty, AXValueType, convertAXTreeToMarkdown } from '../../electron-main/cdpAccessibilityDomain.js'; +import { AXNode, AXProperty, AXPropertyName, AXValueType, convertAXTreeToMarkdown } from '../../electron-main/cdpAccessibilityDomain.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; suite('CDP Accessibility Domain', () => { @@ -17,10 +17,9 @@ suite('CDP Accessibility Domain', () => { return { type, value }; } - function createAXProperty(name: string, value: any, type: AXValueType = 'string'): AXProperty { + function createAXProperty(name: AXPropertyName, value: any, type: AXValueType = 'string'): AXProperty { return { - // eslint-disable-next-line local/code-no-any-casts - name: name as any, + name, value: createAXValue(type, value) }; } diff --git a/src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts index 96e754d3688..fa6cc7ac065 100644 --- a/src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts @@ -6,6 +6,7 @@ import { mockObject } from '../../../../../../base/test/common/mock.js'; import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Event } from '../../../../../../base/common/event.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; @@ -70,10 +71,9 @@ suite('ChatRequestParser', () => { }); test('slash command', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = '/fix this'; @@ -82,10 +82,9 @@ suite('ChatRequestParser', () => { }); test('invalid slash command', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = '/explain this'; @@ -94,10 +93,9 @@ suite('ChatRequestParser', () => { }); test('multiple slash commands', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = '/fix /fix'; @@ -106,10 +104,9 @@ suite('ChatRequestParser', () => { }); test('slash command not first', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = 'Hello /fix'; @@ -118,10 +115,9 @@ suite('ChatRequestParser', () => { }); test('slash command after whitespace', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = ' /fix'; @@ -130,17 +126,15 @@ suite('ChatRequestParser', () => { }); test('prompt slash command', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = ' /prompt'; @@ -149,17 +143,15 @@ suite('ChatRequestParser', () => { }); test('prompt slash command after text', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = 'handle the / route and the request of /search-option'; @@ -168,18 +160,16 @@ suite('ChatRequestParser', () => { }); test('prompt slash command after slash', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = '/ route and the request of /search-option'; @@ -188,17 +178,15 @@ suite('ChatRequestParser', () => { }); test('prompt slash command with numbers', async () => { - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const text = '/001-sample this is a test'; @@ -240,10 +228,9 @@ suite('ChatRequestParser', () => { }; test('agent with subcommand after text', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent Please do /subCommand thanks'); @@ -251,10 +238,9 @@ suite('ChatRequestParser', () => { }); test('agents, subCommand', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent /subCommand Please do thanks'); @@ -262,10 +248,9 @@ suite('ChatRequestParser', () => { }); test('agent but edit mode', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent hello', undefined, { mode: ChatModeKind.Edit }); @@ -273,10 +258,9 @@ suite('ChatRequestParser', () => { }); test('agent with question mark', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent? Are you there'); @@ -284,10 +268,9 @@ suite('ChatRequestParser', () => { }); test('agent and subcommand with leading whitespace', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, ' \r\n\t @agent \r\n\t /subCommand Thanks'); @@ -295,10 +278,9 @@ suite('ChatRequestParser', () => { }); test('agent and subcommand after newline', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, ' \n@agent\n/subCommand Thanks'); @@ -306,10 +288,9 @@ suite('ChatRequestParser', () => { }); test('agent not first', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, 'Hello Mr. @agent'); @@ -317,10 +298,9 @@ suite('ChatRequestParser', () => { }); test('agents and tools and multiline', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); variableService.setSelectedToolAndToolSets(testSessionUri, new Map([ [{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: ToolDataSource.Internal }, true], @@ -333,10 +313,9 @@ suite('ChatRequestParser', () => { }); test('agents and tools and multiline, part2', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); variableService.setSelectedToolAndToolSets(testSessionUri, new Map([ [{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: ToolDataSource.Internal }, true], @@ -349,22 +328,19 @@ suite('ChatRequestParser', () => { }); test('prompt slash command with agent and supportsPromptAttachments', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent /myPrompt do something', undefined, { @@ -374,22 +350,19 @@ suite('ChatRequestParser', () => { }); test('prompt slash command with agent but no supportsPromptAttachments', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent /myPrompt do something', undefined, { @@ -399,22 +372,19 @@ suite('ChatRequestParser', () => { }); test('agent subcommand still takes priority with supportsPromptAttachments', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); - const promptSlashCommandService = mockObject()({}); + const promptSlashCommandService = mockObject()({ _serviceBrand: undefined }); promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { return !!command.match(/^[\w_\-\.]+$/); }); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IPromptsService, promptSlashCommandService as any); + instantiationService.stub(IPromptsService, promptSlashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent /subCommand do something', undefined, { @@ -424,15 +394,13 @@ suite('ChatRequestParser', () => { }); test('slash command with agent and supportsPromptAttachments', async () => { - const agentsService = mockObject()({}); + const agentsService = mockObject()({ _serviceBrand: undefined, hasToolsAgent: false, onDidChangeAgents: Event.None }); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatAgentService, agentsService as any); + instantiationService.stub(IChatAgentService, agentsService); - const slashCommandService = mockObject()({}); + const slashCommandService = mockObject()({ _serviceBrand: undefined }); slashCommandService.getCommands.returns([{ command: 'fix' }]); - // eslint-disable-next-line local/code-no-any-casts - instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + instantiationService.stub(IChatSlashCommandService, slashCommandService); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest(testSessionUri, '@agent /fix this', undefined, { From f2bd744896d37055ef376efc7801411c40f23947 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:19:55 -0700 Subject: [PATCH 402/448] Keep `.md` file extension as default extension Fixes #300239 --- extensions/markdown-basics/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index 434c7e7c667..58cc459ce3f 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -8,7 +8,9 @@ "engines": { "vscode": "^1.20.0" }, - "categories": ["Programming Languages"], + "categories": [ + "Programming Languages" + ], "contributes": { "languages": [ { @@ -18,7 +20,6 @@ "markdown" ], "extensions": [ - ".litcoffee", ".md", ".mkd", ".mkdn", @@ -28,6 +29,7 @@ ".markdn", ".mdtxt", ".mdtext", + ".litcoffee", ".ron", ".ronn", ".workbook" From f0df3843589fe49043dedd7acd027cb2805435c0 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:01:29 -0700 Subject: [PATCH 403/448] Reduce `....args: any[]` usage For #269213 --- src/typings/base-common.d.ts | 2 +- src/vs/base/browser/ui/list/listView.ts | 4 ++-- src/vs/base/common/async.ts | 8 ++++---- src/vs/base/common/decorators.ts | 5 ++--- src/vs/base/common/event.ts | 4 ++-- src/vs/base/common/lifecycle.ts | 2 +- .../logging/debugger/rpc.ts | 4 ++-- src/vs/base/parts/ipc/common/ipc.ts | 4 ++-- .../base/parts/ipc/electron-main/ipcMain.ts | 6 +++--- .../instantiation/common/extensions.ts | 2 +- .../workbench/api/common/extHost.api.impl.ts | 19 +++++++++---------- .../workbench/api/common/extHost.protocol.ts | 4 ++-- .../workbench/api/common/extHostCommands.ts | 6 +++--- src/vs/workbench/api/node/extHostCLIServer.ts | 2 +- .../api/node/extHostConsoleForwarder.ts | 2 +- .../api/node/extensionHostProcess.ts | 2 +- .../extHostDocumentSaveParticipant.test.ts | 2 +- .../api/worker/extHostConsoleForwarder.ts | 2 +- .../authenticationQueryServiceMocks.ts | 2 +- .../test/browser/userAttentionService.test.ts | 2 +- .../parts/editor/breadcrumbModel.test.ts | 2 +- src/vs/workbench/test/browser/window.test.ts | 2 +- 22 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/typings/base-common.d.ts b/src/typings/base-common.d.ts index 56e9a6a799d..9028abb2975 100644 --- a/src/typings/base-common.d.ts +++ b/src/typings/base-common.d.ts @@ -25,7 +25,7 @@ declare global { function setTimeout(handler: string | Function, timeout?: number, ...arguments: any[]): Timeout; function clearTimeout(timeout: Timeout | undefined): void; - function setInterval(callback: (...args: any[]) => void, delay?: number, ...args: any[]): Timeout; + function setInterval(callback: (...args: unknown[]) => void, delay?: number, ...args: unknown[]): Timeout; function clearInterval(timeout: Timeout | undefined): void; diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 6e29b67c503..5c62e99faf8 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -152,8 +152,8 @@ export class ExternalElementsDragAndDropData implements IDragAndDropData { export class NativeDragAndDropData implements IDragAndDropData { - readonly types: any[]; - readonly files: any[]; + readonly types: unknown[]; + readonly files: unknown[]; constructor() { this.types = []; diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 3dcfa0c5130..e5aaa424876 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -1098,15 +1098,15 @@ export class IntervalTimer implements IDisposable { } } -export class RunOnceScheduler implements IDisposable { +export class RunOnceScheduler any = () => any> implements IDisposable { - protected runner: ((...args: unknown[]) => void) | null; + protected runner: Runner | null; private timeoutToken: Timeout | undefined; private timeout: number; private timeoutHandler: () => void; - constructor(runner: (...args: any[]) => void, delay: number) { + constructor(runner: Runner, delay: number) { this.timeoutToken = undefined; this.runner = runner; this.timeout = delay; @@ -1246,7 +1246,7 @@ export class ProcessTimeRunOnceScheduler { } } -export class RunOnceWorker extends RunOnceScheduler { +export class RunOnceWorker extends RunOnceScheduler<(units: T[]) => void> { private units: T[] = []; diff --git a/src/vs/base/common/decorators.ts b/src/vs/base/common/decorators.ts index 7510ffcec1f..74d2e56f51e 100644 --- a/src/vs/base/common/decorators.ts +++ b/src/vs/base/common/decorators.ts @@ -45,7 +45,7 @@ export function memoize(_target: Object, key: string, descriptor: PropertyDescri } const memoizeKey = `$memoize$${key}`; - descriptor[fnKey!] = function (...args: any[]) { + descriptor[fnKey!] = function (this: any, ...args: unknown[]) { if (!this.hasOwnProperty(memoizeKey)) { Object.defineProperty(this, memoizeKey, { configurable: false, @@ -54,8 +54,7 @@ export function memoize(_target: Object, key: string, descriptor: PropertyDescri value: fn.apply(this, args) }); } - // eslint-disable-next-line local/code-no-any-casts - return (this as any)[memoizeKey]; + return this[memoizeKey]; }; } diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 09d0ed7c530..a9d495ab6b0 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -707,7 +707,7 @@ export namespace Event { * Creates an {@link Event} from a node event emitter. */ export function fromNodeEventEmitter(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event { - const fn = (...args: any[]) => result.fire(map(...args)); + const fn = (...args: unknown[]) => result.fire(map(...args)); const onFirstListenerAdd = () => emitter.on(eventName, fn); const onLastListenerRemove = () => emitter.removeListener(eventName, fn); const result = new Emitter({ onWillAddFirstListener: onFirstListenerAdd, onDidRemoveLastListener: onLastListenerRemove }); @@ -724,7 +724,7 @@ export namespace Event { * Creates an {@link Event} from a DOM event emitter. */ export function fromDOMEventEmitter(emitter: DOMEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event { - const fn = (...args: any[]) => result.fire(map(...args)); + const fn = (...args: unknown[]) => result.fire(map(...args)); const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn); const onLastListenerRemove = () => emitter.removeEventListener(eventName, fn); const result = new Emitter({ onWillAddFirstListener: onFirstListenerAdd, onDidRemoveLastListener: onLastListenerRemove }); diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 630edb097f2..a75cbb1cce3 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -720,7 +720,7 @@ export class AsyncReferenceCollection { constructor(private referenceCollection: ReferenceCollection>) { } - async acquire(key: string, ...args: any[]): Promise> { + async acquire(key: string, ...args: unknown[]): Promise> { const ref = this.referenceCollection.acquire(key, ...args); try { diff --git a/src/vs/base/common/observableInternal/logging/debugger/rpc.ts b/src/vs/base/common/observableInternal/logging/debugger/rpc.ts index d19da1fe159..c4d392bca69 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/rpc.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/rpc.ts @@ -72,7 +72,7 @@ export class SimpleTypedRpcConnection { const requests = new Proxy({}, { get: (target, key: string) => { - return async (...args: any[]) => { + return async (...args: unknown[]) => { const result = await this._channel.sendRequest([key, args] satisfies OutgoingMessage); if (result.type === 'error') { throw result.value; @@ -85,7 +85,7 @@ export class SimpleTypedRpcConnection { const notifications = new Proxy({}, { get: (target, key: string) => { - return (...args: any[]) => { + return (...args: unknown[]) => { this._channel.sendNotification([key, args] satisfies OutgoingMessage); }; } diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 9d62cc8d5fa..663b7f541ee 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -1209,10 +1209,10 @@ export namespace ProxyChannel { } // Function - return async function (...args: any[]) { + return async function (...args: unknown[]) { // Add context if any - let methodArgs: any[]; + let methodArgs: unknown[]; if (options && !isUndefinedOrNull(options.context)) { methodArgs = [options.context, ...args]; } else { diff --git a/src/vs/base/parts/ipc/electron-main/ipcMain.ts b/src/vs/base/parts/ipc/electron-main/ipcMain.ts index 0137b8924eb..267c15b7125 100644 --- a/src/vs/base/parts/ipc/electron-main/ipcMain.ts +++ b/src/vs/base/parts/ipc/electron-main/ipcMain.ts @@ -25,7 +25,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { // Remember the wrapped listener so that later we can // properly implement `removeListener`. - const wrappedListener = (event: electron.IpcMainEvent, ...args: any[]) => { + const wrappedListener = (event: electron.IpcMainEvent, ...args: unknown[]) => { if (this.validateEvent(channel, event)) { listener(event, ...args); } @@ -43,7 +43,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { * only the next time a message is sent to `channel`, after which it is removed. */ once(channel: string, listener: ipcMainListener): this { - electron.ipcMain.once(channel, (event: electron.IpcMainEvent, ...args: any[]) => { + electron.ipcMain.once(channel, (event: electron.IpcMainEvent, ...args: unknown[]) => { if (this.validateEvent(channel, event)) { listener(event, ...args); } @@ -69,7 +69,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { * provided to the renderer process. Please refer to #24427 for details. */ handle(channel: string, listener: (event: electron.IpcMainInvokeEvent, ...args: any[]) => Promise): this { - electron.ipcMain.handle(channel, (event: electron.IpcMainInvokeEvent, ...args: any[]) => { + electron.ipcMain.handle(channel, (event: electron.IpcMainInvokeEvent, ...args: unknown[]) => { if (this.validateEvent(channel, event)) { return listener(event, ...args); } diff --git a/src/vs/platform/instantiation/common/extensions.ts b/src/vs/platform/instantiation/common/extensions.ts index 517a8cc2a3a..e59cc837cc6 100644 --- a/src/vs/platform/instantiation/common/extensions.ts +++ b/src/vs/platform/instantiation/common/extensions.ts @@ -26,7 +26,7 @@ export function registerSingleton(id: Serv export function registerSingleton(id: ServiceIdentifier, descriptor: SyncDescriptor): void; export function registerSingleton(id: ServiceIdentifier, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor, supportsDelayedInstantiation?: boolean | InstantiationType): void { if (!(ctorOrDescriptor instanceof SyncDescriptor)) { - ctorOrDescriptor = new SyncDescriptor(ctorOrDescriptor as new (...args: any[]) => T, [], Boolean(supportsDelayedInstantiation)); + ctorOrDescriptor = new SyncDescriptor(ctorOrDescriptor as new (...args: unknown[]) => T, [], Boolean(supportsDelayedInstantiation)); } _registry.push([id, ctorOrDescriptor]); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 37251c6f672..53b786f05a9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -351,11 +351,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: commands const commands: typeof vscode.commands = { - registerCommand(id: string, command: (...args: any[]) => T | Thenable, thisArgs?: any): vscode.Disposable { + registerCommand(id: string, command: (...args: unknown[]) => T | Thenable, thisArgs?: unknown): vscode.Disposable { return extHostCommands.registerCommand(true, id, command, thisArgs, undefined, extension); }, - registerTextEditorCommand(id: string, callback: (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args: any[]) => void, thisArg?: any): vscode.Disposable { - return extHostCommands.registerCommand(true, id, (...args: any[]): any => { + registerTextEditorCommand(id: string, callback: (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args: unknown[]) => void, thisArg?: unknown): vscode.Disposable { + return extHostCommands.registerCommand(true, id, (...args: unknown[]): any => { const activeTextEditor = extHostEditors.getActiveTextEditor(); if (!activeTextEditor) { extHostLogService.warn('Cannot execute ' + id + ' because there is no active text editor.'); @@ -364,7 +364,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return activeTextEditor.edit((edit: vscode.TextEditorEdit) => { callback.apply(thisArg, [activeTextEditor, edit, ...args]); - }).then((result) => { if (!result) { extHostLogService.warn('Edits from command ' + id + ' were not applied.'); @@ -374,9 +373,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }); }, undefined, undefined, extension); }, - registerDiffInformationCommand: (id: string, callback: (diff: vscode.LineChange[], ...args: any[]) => any, thisArg?: any): vscode.Disposable => { + registerDiffInformationCommand: (id: string, callback: (diff: vscode.LineChange[], ...args: unknown[]) => any, thisArg?: unknown): vscode.Disposable => { checkProposedApiEnabled(extension, 'diffCommand'); - return extHostCommands.registerCommand(true, id, async (...args: any[]): Promise => { + return extHostCommands.registerCommand(true, id, async (...args: unknown[]): Promise => { const activeTextEditor = extHostDocumentsAndEditors.activeEditor(true); if (!activeTextEditor) { extHostLogService.warn('Cannot execute ' + id + ' because there is no active text editor.'); @@ -387,7 +386,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I callback.apply(thisArg, [diff, ...args]); }, undefined, undefined, extension); }, - executeCommand(id: string, ...args: any[]): Thenable { + executeCommand(id: string, ...args: unknown[]): Thenable { return extHostCommands.executeCommand(id, ...args); }, getCommands(filterInternal: boolean = false): Thenable { @@ -1304,13 +1303,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidRenameFiles: (listener, thisArg, disposables) => { return _asExtensionEvent(extHostFileSystemEvent.onDidRenameFile)(listener, thisArg, disposables); }, - onWillCreateFiles: (listener: (e: vscode.FileWillCreateEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + onWillCreateFiles: (listener: (e: vscode.FileWillCreateEvent) => any, thisArg?: unknown, disposables?: vscode.Disposable[]) => { return _asExtensionEvent(extHostFileSystemEvent.getOnWillCreateFileEvent(extension))(listener, thisArg, disposables); }, - onWillDeleteFiles: (listener: (e: vscode.FileWillDeleteEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + onWillDeleteFiles: (listener: (e: vscode.FileWillDeleteEvent) => any, thisArg?: unknown, disposables?: vscode.Disposable[]) => { return _asExtensionEvent(extHostFileSystemEvent.getOnWillDeleteFileEvent(extension))(listener, thisArg, disposables); }, - onWillRenameFiles: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + onWillRenameFiles: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: unknown, disposables?: vscode.Disposable[]) => { return _asExtensionEvent(extHostFileSystemEvent.getOnWillRenameFileEvent(extension))(listener, thisArg, disposables); }, openTunnel: (forward: vscode.TunnelOptions) => { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6f5d4d775a1..bf10d83869f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2116,7 +2116,7 @@ export interface ExtHostCodeMapperShape { } export interface ExtHostCommandsShape { - $executeContributedCommand(id: string, ...args: any[]): Promise; + $executeContributedCommand(id: string, ...args: unknown[]): Promise; $getContributedCommandMetadata(): Promise<{ [id: string]: string | ICommandMetadataDto }>; } @@ -2445,7 +2445,7 @@ export interface ISuggestDataDto { // Command [ISuggestDataDtoField.commandIdent]?: string; [ISuggestDataDtoField.commandId]?: string; - [ISuggestDataDtoField.commandArguments]?: any[]; + [ISuggestDataDtoField.commandArguments]?: unknown[]; // not-standard x?: ChainedCacheId; } diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 15c769373b8..92e874dc6c9 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -425,11 +425,11 @@ export class CommandsConverter implements extHostTypeConverter.Command.ICommands } - getActualCommand(...args: any[]): vscode.Command | undefined { - return this._cache.get(args[0]); + getActualCommand(...args: unknown[]): vscode.Command | undefined { + return this._cache.get(args[0] as string); } - private _executeConvertedCommand(...args: any[]): Promise { + private _executeConvertedCommand(...args: unknown[]): Promise { const actualCmd = this.getActualCommand(...args); this._logService.trace('CommandsConverter#EXECUTE', args[0], actualCmd ? actualCmd.command : 'MISSING'); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 70b04f8fdde..14ed9e4f586 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -47,7 +47,7 @@ export interface ExtensionManagementPipeArgs { export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | OpenExternalCommandPipeArgs | ExtensionManagementPipeArgs; export interface ICommandsExecuter { - executeCommand(id: string, ...args: any[]): Promise; + executeCommand(id: string, ...args: unknown[]): Promise; } export class CLIServerBase { diff --git a/src/vs/workbench/api/node/extHostConsoleForwarder.ts b/src/vs/workbench/api/node/extHostConsoleForwarder.ts index d1568f28642..e581462c208 100644 --- a/src/vs/workbench/api/node/extHostConsoleForwarder.ts +++ b/src/vs/workbench/api/node/extHostConsoleForwarder.ts @@ -24,7 +24,7 @@ export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder { this._wrapStream('stdout', 'log'); } - protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error' | 'debug', original: (...args: any[]) => void, args: IArguments) { + protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error' | 'debug', original: (...args: unknown[]) => void, args: IArguments) { const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout; this._isMakingConsoleCall = true; stream.write(`\n${NativeLogMarkers.Start}\n`); diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index 50ef42e0a2f..06a165e7ba9 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -118,7 +118,7 @@ function patchProcess(allowExit: boolean) { process.env['ELECTRON_RUN_AS_NODE'] = '1'; // eslint-disable-next-line local/code-no-any-casts - process.on = function (event: string, listener: (...args: any[]) => void) { + process.on = function (event: string, listener: (...args: unknown[]) => void) { if (event === 'uncaughtException') { const actualListener = listener; listener = function (...args: unknown[]) { diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 40614a76d68..b13989ab91f 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -384,7 +384,7 @@ suite('ExtHostDocumentSaveParticipant', () => { test('Log failing listener', function () { let didLogSomething = false; const participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService { - override error(message: string | Error, ...args: any[]): void { + override error(message: string | Error, ...args: unknown[]): void { didLogSomething = true; } }, documents, mainThreadBulkEdits); diff --git a/src/vs/workbench/api/worker/extHostConsoleForwarder.ts b/src/vs/workbench/api/worker/extHostConsoleForwarder.ts index 01814a7d969..899d4a9d4f8 100644 --- a/src/vs/workbench/api/worker/extHostConsoleForwarder.ts +++ b/src/vs/workbench/api/worker/extHostConsoleForwarder.ts @@ -16,7 +16,7 @@ export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder { super(extHostRpc, initData); } - protected override _nativeConsoleLogMessage(_method: unknown, original: (...args: any[]) => void, args: IArguments) { + protected override _nativeConsoleLogMessage(_method: unknown, original: (...args: unknown[]) => void, args: IArguments) { // eslint-disable-next-line local/code-no-any-casts original.apply(console, args as any); } diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationQueryServiceMocks.ts b/src/vs/workbench/services/authentication/test/browser/authenticationQueryServiceMocks.ts index 4a2ca0e6284..2528c9f41ae 100644 --- a/src/vs/workbench/services/authentication/test/browser/authenticationQueryServiceMocks.ts +++ b/src/vs/workbench/services/authentication/test/browser/authenticationQueryServiceMocks.ts @@ -64,7 +64,7 @@ export abstract class BaseTestService extends Disposable { /** * Track a method call for verification in tests */ - protected trackCall(method: string, ...args: any[]): void { + protected trackCall(method: string, ...args: unknown[]): void { this._methodCalls.push({ method, args: [...args], diff --git a/src/vs/workbench/services/userAttention/test/browser/userAttentionService.test.ts b/src/vs/workbench/services/userAttention/test/browser/userAttentionService.test.ts index 07886188bb6..287550a7532 100644 --- a/src/vs/workbench/services/userAttention/test/browser/userAttentionService.test.ts +++ b/src/vs/workbench/services/userAttention/test/browser/userAttentionService.test.ts @@ -44,7 +44,7 @@ suite('UserAttentionService', () => { }; const originalCreateInstance = insta.createInstance; - sinon.stub(insta, 'createInstance').callsFake((ctor: any, ...args: any[]) => { + sinon.stub(insta, 'createInstance').callsFake((ctor: any, ...args: unknown[]) => { if (ctor === UserAttentionServiceEnv) { return hostAdapterMock; } diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index 14d2d9512ef..b48e8fe0349 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -20,7 +20,7 @@ suite('Breadcrumb Model', function () { let model: BreadcrumbsModel; const workspaceService = new TestContextService(new Workspace('ffff', [new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws', index: 0 })])); const configService = new class extends TestConfigurationService { - override getValue(...args: any[]): T | undefined { + override getValue(...args: Parameters): T | undefined { if (args[0] === 'breadcrumbs.filePath') { return 'on' as T; } diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index f758f9900d8..1290fef45c3 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -41,7 +41,7 @@ suite('Window', () => { function createWindow(id: number, slow?: boolean) { // eslint-disable-next-line local/code-no-any-casts const res = { - setTimeout: function (callback: Function, delay: number, ...args: any[]): number { + setTimeout: function (callback: Function, delay: number, ...args: unknown[]): number { setTimeoutCalls.push(id); return mainWindow.setTimeout(() => callback(id), slow ? delay * 2 : delay, ...args); From 89508b298e0171ba39ea074ba9c9d8ca833bed5e Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 10 Mar 2026 00:09:19 -0700 Subject: [PATCH 404/448] Optimize discovery process (#300317) --- .../chat/browser/promptsDebugContribution.ts | 3 - .../contrib/chat/common/chatDebugService.ts | 3 - .../promptSyntax/service/promptsService.ts | 8 +-- .../service/promptsServiceImpl.ts | 55 +++++-------------- .../resolveDebugEventDetailsTool.ts | 2 +- .../browser/promptsDebugContribution.test.ts | 3 - 6 files changed, 15 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts b/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts index 964f68d2ac0..48dc9effee2 100644 --- a/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts @@ -92,9 +92,6 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo sourceFolders: info.sourceFolders?.map(sf => ({ uri: sf.uri, storage: sf.storage, - exists: sf.exists, - fileCount: sf.fileCount, - errorMessage: sf.errorMessage, })), }; } diff --git a/src/vs/workbench/contrib/chat/common/chatDebugService.ts b/src/vs/workbench/contrib/chat/common/chatDebugService.ts index 889e496e9b1..d97213d3f3e 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugService.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugService.ts @@ -273,9 +273,6 @@ export interface IChatDebugFileEntry { export interface IChatDebugSourceFolderEntry { readonly uri: URI; readonly storage: string; - readonly exists: boolean; - readonly fileCount: number; - readonly errorMessage?: string; } /** 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 6eac3cb02b9..d03f9f66669 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -334,12 +334,6 @@ export interface IPromptFileDiscoveryResult { export interface IPromptSourceFolderResult { readonly uri: URI; readonly storage: PromptsStorage; - /** Whether the folder exists on disk */ - readonly exists: boolean; - /** Number of matching files found in this folder */ - readonly fileCount: number; - /** Error message if resolution failed */ - readonly errorMessage?: string; } /** @@ -348,7 +342,7 @@ export interface IPromptSourceFolderResult { export interface IPromptDiscoveryInfo { readonly type: PromptsType; readonly files: readonly IPromptFileDiscoveryResult[]; - /** Source folders that were searched, with their existence and file count */ + /** Source folders that were searched */ readonly sourceFolders?: readonly IPromptSourceFolderResult[]; } 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 6d38cd93e98..32e00b8a10f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -339,42 +339,14 @@ export class PromptsService extends Disposable implements IPromptsService { } /** - * Collects diagnostic information about which source folders were searched - * and whether they exist, for display in the debug panel. + * Collects diagnostic information about which source folders were searched for display in the debug panel. */ - private async _collectSourceFolderDiagnostics(type: PromptsType, foundFiles: readonly { uri: URI }[]): Promise { + private async _collectSourceFolderDiagnostics(type: PromptsType): Promise { const resolvedFolders = await this.fileLocator.getSourceFoldersInDiscoveryOrder(type); - const results: IPromptSourceFolderResult[] = []; - - for (const folder of resolvedFolders) { - const fileCount = foundFiles.filter(f => f.uri.path.startsWith(folder.uri.path + '/')).length; - let exists = fileCount > 0; - let errorMessage: string | undefined; - - if (!exists) { - try { - const stat = await this.fileService.stat(folder.uri); - exists = stat.isDirectory; - } catch (e) { - if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { - exists = false; - } else { - exists = false; - errorMessage = e instanceof Error ? e.message : String(e); - } - } - } - - results.push({ - uri: folder.uri, - storage: folder.storage, - exists, - fileCount, - errorMessage, - }); - } - - return results; + return resolvedFolders.map(folder => ({ + uri: folder.uri, + storage: folder.storage, + })); } /** @@ -1376,7 +1348,7 @@ export class PromptsService extends Disposable implements IPromptsService { // Add source folder diagnostics if not already present if (!result.sourceFolders) { - const sourceFolders = await this._collectSourceFolderDiagnostics(type, result.files.filter(f => f.status === 'loaded')); + const sourceFolders = await this._collectSourceFolderDiagnostics(type); result = { ...result, sourceFolders }; } @@ -1411,13 +1383,12 @@ export class PromptsService extends Disposable implements IPromptsService { skipReason: 'disabled' as const, extensionId: promptPath.extension?.identifier?.value })); - const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.skill, []); + const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.skill); return { type: PromptsType.skill, files, sourceFolders }; } const { files } = await this.computeSkillDiscoveryInfo(token); - const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.skill, files.filter(f => f.status === 'loaded')); - return { type: PromptsType.skill, files, sourceFolders }; + return { type: PromptsType.skill, files }; } /** @@ -1569,7 +1540,7 @@ export class PromptsService extends Disposable implements IPromptsService { } } - const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.agent, files.filter(f => f.status === 'loaded')); + const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.agent); return { type: PromptsType.agent, files, sourceFolders }; } @@ -1598,7 +1569,7 @@ export class PromptsService extends Disposable implements IPromptsService { } } - const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.prompt, files.filter(f => f.status === 'loaded')); + const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.prompt); return { type: PromptsType.prompt, files, sourceFolders }; } @@ -1627,7 +1598,7 @@ export class PromptsService extends Disposable implements IPromptsService { } } - const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.instructions, files.filter(f => f.status === 'loaded')); + const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.instructions); return { type: PromptsType.instructions, files, sourceFolders }; } @@ -1726,7 +1697,7 @@ export class PromptsService extends Disposable implements IPromptsService { } } - const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.hook, files.filter(f => f.status === 'loaded')); + const sourceFolders = await this._collectSourceFolderDiagnostics(PromptsType.hook); return { type: PromptsType.hook, files, sourceFolders }; } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts index dbf7e4e7105..fa690d45d61 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/resolveDebugEventDetailsTool.ts @@ -39,7 +39,7 @@ function formatResolvedContent(content: IChatDebugResolvedEventContent): string const lines: string[] = [`File list (${content.discoveryType}):`]; if (content.sourceFolders) { for (const folder of content.sourceFolders) { - lines.push(` Source folder: ${folder.uri.toString()} (${folder.storage}, ${folder.fileCount} files${folder.exists ? '' : ', missing'})`); + lines.push(` Source folder: ${folder.uri.toString()} (${folder.storage})`); } } for (const file of content.files) { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts index f5045a72ed8..b7a4694c4e5 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.test.ts @@ -71,8 +71,6 @@ suite('PromptsDebugContribution', () => { sourceFolders: [{ uri: URI.file('/workspace/.github/instructions'), storage: PromptsStorage.local, - exists: true, - fileCount: 1, }], }; @@ -97,7 +95,6 @@ suite('PromptsDebugContribution', () => { assert.strictEqual(resolved.files[0].name, 'test.instructions.md'); assert.strictEqual(resolved.files[0].status, 'loaded'); assert.strictEqual(resolved.sourceFolders?.length, 1); - assert.strictEqual(resolved.sourceFolders?.[0].exists, true); } }); From 261701ae145f5e66c78e9ddcbd06e1ef18e821ed Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Mar 2026 01:03:12 -0700 Subject: [PATCH 405/448] mcp: move to askquestions for elicitations (#300294) * mcp: move to askquestions for elicitations This reuses the askquestions UI to make elicitation requests. This is a nicer UX than the quickpick flow. It adds data validation to askquestions, as required by MCP, and also switches the answer types from `unknown` to `IChatQuestionAnswerValue` which is a bit more predictable to manage. * fic tests * pr comments --- .../api/common/extHostTypeConverters.ts | 2 +- .../chatQuestionCarouselPart.ts | 415 +++++++++++++----- .../media/chatQuestionCarousel.css | 26 ++ .../chat/browser/widget/chatListRenderer.ts | 8 +- .../widget/chatQuestionCarouselAutoReply.ts | 32 +- .../chat/common/chatService/chatService.ts | 45 +- .../common/chatService/chatServiceImpl.ts | 6 +- .../chatQuestionCarouselData.ts | 14 +- .../tools/builtinTools/askQuestionsTool.ts | 92 ++-- .../localAgentSessionsController.test.ts | 4 +- .../chatQuestionCarouselPart.test.ts | 209 ++++++++- .../common/chatService/mockChatService.ts | 6 +- .../builtinTools/askQuestionsTool.test.ts | 12 +- .../mcp/browser/mcpElicitationService.ts | 264 +++++++++-- 14 files changed, 879 insertions(+), 256 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 4515d21525a..9cab6521333 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2671,7 +2671,7 @@ export namespace ChatResponseQuestionCarouselPart { type: questionTypeToString(q.type), title: q.title, message: q.message ? MarkdownString.from(q.message) : undefined, - options: q.options, + options: q.options?.map(opt => ({ id: opt.id, label: opt.label, value: String(opt.value) })), defaultValue: q.defaultValue, allowFreeformInput: q.allowFreeformInput })), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index b47b64499f4..c3d240b5a30 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -20,7 +20,7 @@ import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { InputBox } from '../../../../../../base/browser/ui/inputbox/inputBox.js'; import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; import { Checkbox } from '../../../../../../base/browser/ui/toggle/toggle.js'; -import { IChatQuestion, IChatQuestionCarousel } from '../../../common/chatService/chatService.js'; +import { IChatQuestion, IChatQuestionCarousel, IChatQuestionAnswerValue, IChatQuestionValidation, IChatSingleSelectAnswer, IChatMultiSelectAnswer } from '../../../common/chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { IChatRendererContent, isResponseVM } from '../../../common/model/chatViewModel.js'; @@ -37,7 +37,7 @@ import './media/chatQuestionCarousel.css'; const PREVIOUS_QUESTION_ACTION_ID = 'workbench.action.chat.previousQuestion'; const NEXT_QUESTION_ACTION_ID = 'workbench.action.chat.nextQuestion'; export interface IChatQuestionCarouselOptions { - onSubmit: (answers: Map | undefined) => void; + onSubmit: (answers: Map | undefined) => void; shouldAutoFocus?: boolean; } @@ -48,7 +48,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent public readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; private _currentIndex = 0; - private readonly _answers = new Map(); + private readonly _answers = new Map(); private _questionContainer: HTMLElement | undefined; private _closeButtonContainer: HTMLElement | undefined; @@ -76,6 +76,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private readonly _interactiveUIStore: MutableDisposable = this._register(new MutableDisposable()); private readonly _inChatQuestionCarouselContextKey: IContextKey; + private _validationMessageElement: HTMLElement | undefined; + private _currentValidationError: string | undefined; constructor( public readonly carousel: IChatQuestionCarousel, @@ -199,6 +201,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._answers.delete(currentQuestion.id); } + // Validate on change to update the Next button state + if (currentQuestion?.validation && typeof answer === 'string' && answer !== '') { + const error = this.getValidationError(answer, currentQuestion.validation); + if (error) { + this.showValidationError(error); + } else { + this.clearValidationError(); + } + } else { + this.clearValidationError(); + } + + this.updateFooterState(); this.persistDraftState(); } @@ -233,6 +248,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private handleNextOrSubmit(): void { this.saveCurrentAnswer(); + if (!this.validateCurrentQuestion()) { + return; + } + if (this._currentIndex < this.carousel.questions.length - 1) { // Move to next question this._currentIndex++; @@ -240,6 +259,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this.renderCurrentQuestion(true); } else { // Submit + if (!this.validateRequiredFields()) { + return; + } this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -250,6 +272,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private submit(): void { this.saveCurrentAnswer(); + if (!this.validateCurrentQuestion()) { + return; + } + if (!this.validateRequiredFields()) { + return; + } this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -417,8 +445,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent /** * Collects default values for all questions in the carousel. */ - private getDefaultAnswers(): Map { - const answers = new Map(); + private getDefaultAnswers(): Map { + const answers = new Map(); for (const question of this.carousel.questions) { const defaultAnswer = this.getDefaultAnswerForQuestion(question); if (defaultAnswer !== undefined) { @@ -431,10 +459,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent /** * Gets the default answer for a specific question. */ - private getDefaultAnswerForQuestion(question: IChatQuestion): unknown { + private getDefaultAnswerForQuestion(question: IChatQuestion): IChatQuestionAnswerValue | undefined { switch (question.type) { case 'text': - return question.defaultValue; + return typeof question.defaultValue === 'string' ? question.defaultValue : undefined; case 'singleSelect': { const defaultOptionId = typeof question.defaultValue === 'string' ? question.defaultValue : undefined; @@ -443,8 +471,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent : undefined; const selectedValue = defaultOption?.value; - // Always return structured format for single-select (freeform is always shown) - return selectedValue !== undefined ? { selectedValue, freeformValue: undefined } : undefined; + return selectedValue !== undefined ? { selectedValue, freeformValue: undefined } satisfies IChatSingleSelectAnswer : undefined; } case 'multiSelect': { @@ -456,12 +483,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent .map(opt => opt.value) .filter(v => v !== undefined) ?? []; - // Always return structured format for multi-select (freeform is always shown) - return selectedValues.length > 0 ? { selectedValues, freeformValue: undefined } : undefined; + return selectedValues.length > 0 ? { selectedValues, freeformValue: undefined } satisfies IChatMultiSelectAnswer : undefined; } default: - return question.defaultValue; + return typeof question.defaultValue === 'string' ? question.defaultValue : Array.isArray(question.defaultValue) ? { selectedValues: question.defaultValue } : undefined; } } @@ -558,14 +584,25 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const headerRow = dom.$('.chat-question-header-row'); const titleRow = dom.$('.chat-question-title-row'); + // Render carousel-level message if present (e.g. from MCP elicitation) + if (this.carousel.message && this._currentIndex === 0) { + const messageMd = isMarkdownString(this.carousel.message) ? MarkdownString.lift(this.carousel.message) : new MarkdownString(this.carousel.message); + const carouselMessage = dom.$('.chat-question-carousel-message'); + const renderedMessage = questionRenderStore.add(this._markdownRendererService.render(messageMd)); + carouselMessage.appendChild(renderedMessage.element); + headerRow.appendChild(carouselMessage); + } + const questionText = question.message ?? question.title; if (questionText) { const title = dom.$('.chat-question-title'); const messageContent = this.getQuestionText(questionText); title.setAttribute('aria-label', messageContent); - const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); - const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); + const titleText = question.required + ? new MarkdownString(`${isMarkdownString(questionText) ? questionText.value : questionText} *`) + : (isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText)); + const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(titleText)); title.appendChild(renderedTitle.element); titleRow.appendChild(title); } @@ -579,6 +616,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._questionContainer.appendChild(headerRow); + // Render description if present + if (question.description) { + const descriptionEl = dom.$('.chat-question-description'); + descriptionEl.textContent = question.description; + this._questionContainer.appendChild(descriptionEl); + } + // Render input based on question type const inputContainer = dom.$('.chat-question-input-container'); this.renderInput(inputContainer, question); @@ -593,6 +637,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollableNode.classList.add('chat-question-input-scrollable'); this._questionContainer.appendChild(inputScrollableNode); + // Validation message element below the scrollable area (not inside it) + this._validationMessageElement = dom.$('.chat-question-validation-message'); + this._validationMessageElement.style.display = 'none'; + this._questionContainer.appendChild(this._validationMessageElement); + const isSingleQuestion = this.carousel.questions.length === 1; // Render footer before first layout so the scrollable area is measured against @@ -715,7 +764,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._prevButton.enabled = this._currentIndex > 0; } if (this._nextButton) { - this._nextButton.enabled = this._currentIndex < this.carousel.questions.length - 1; + const canAdvance = this._currentIndex < this.carousel.questions.length - 1; + const question = this.carousel.questions[this._currentIndex]; + const answer = this._answers.get(question?.id); + const hasAnswer = answer !== undefined && answer !== ''; + const hasValidationError = !!this._currentValidationError; + this._nextButton.enabled = canAdvance && (!question?.required || hasAnswer) && !hasValidationError; } if (this._stepIndicator) { this._stepIndicator.textContent = localize( @@ -814,8 +868,22 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const inputBox = this._inputBoxes.add(new InputBox(container, undefined, { placeholder: localize('chat.questionCarousel.enterText', 'Enter your answer'), inputBoxStyles: defaultInputBoxStyles, + validationOptions: question.validation ? { + validation: (value: string) => { + if (!value && !question.required) { + return null; + } + const error = this.getValidationError(value, question.validation!); + if (error) { + return { type: 2 /* MessageType.WARNING */, content: error }; + } + return null; + } + } : undefined, + })); + this._inputBoxes.add(inputBox.onDidChange(() => { + this.saveCurrentAnswer(); })); - this._inputBoxes.add(inputBox.onDidChange(() => this.saveCurrentAnswer())); // Restore previous answer if exists const previousAnswer = this._answers.get(question.id); @@ -843,12 +911,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // Restore previous answer if exists const previousAnswer = this._answers.get(question.id); - const previousFreeform = typeof previousAnswer === 'object' && previousAnswer !== null && hasKey(previousAnswer, { freeformValue: true }) - ? (previousAnswer as { freeformValue?: string }).freeformValue - : undefined; - const previousSelectedValue = typeof previousAnswer === 'object' && previousAnswer !== null && hasKey(previousAnswer, { selectedValue: true }) - ? (previousAnswer as { selectedValue?: unknown }).selectedValue - : previousAnswer; + const prevSingle = typeof previousAnswer === 'object' && previousAnswer !== null && hasKey(previousAnswer, { selectedValue: true }) ? previousAnswer as IChatSingleSelectAnswer : undefined; + const previousFreeform = prevSingle?.freeformValue; + const previousSelectedValue = prevSingle?.selectedValue; // Get default option id (for singleSelect, defaultValue is a single string) const defaultOptionId = typeof question.defaultValue === 'string' ? question.defaultValue : undefined; @@ -959,36 +1024,45 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent selectContainer.setAttribute('aria-activedescendant', listItems[selectedIndex].id); } - // Always show freeform input for single-select questions - const freeformContainer = dom.$('.chat-question-freeform'); + // Show freeform input only when explicitly allowed + let freeformTextarea: HTMLTextAreaElement | undefined; + if (question.allowFreeformInput !== false) { + const freeformContainer = dom.$('.chat-question-freeform'); - const freeformNumber = dom.$('.chat-question-freeform-number'); - freeformNumber.textContent = `${options.length + 1}`; - freeformContainer.appendChild(freeformNumber); + const freeformNumber = dom.$('.chat-question-freeform-number'); + freeformNumber.textContent = `${options.length + 1}`; + freeformContainer.appendChild(freeformNumber); - const freeformTextarea = dom.$('textarea.chat-question-freeform-textarea'); - freeformTextarea.placeholder = localize('chat.questionCarousel.enterCustomAnswer', 'Enter custom answer'); - freeformTextarea.rows = 1; + freeformTextarea = dom.$('textarea.chat-question-freeform-textarea'); + freeformTextarea.placeholder = localize('chat.questionCarousel.enterCustomAnswer', 'Enter custom answer'); + freeformTextarea.rows = 1; - if (previousFreeform !== undefined) { - freeformTextarea.value = previousFreeform; - } - - // Setup auto-resize behavior - const autoResize = this.setupTextareaAutoResize(freeformTextarea); - - // clear when we start typing in freeform - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { - if (freeformTextarea.value.length > 0) { - updateSelection(-1); - } else { - this.saveCurrentAnswer(); + if (previousFreeform !== undefined) { + freeformTextarea.value = previousFreeform; } - })); - freeformContainer.appendChild(freeformTextarea); - container.appendChild(freeformContainer); - this._freeformTextareas.set(question.id, freeformTextarea); + // Setup auto-resize behavior + const autoResize = this.setupTextareaAutoResize(freeformTextarea); + + // clear when we start typing in freeform + const capturedFreeform = freeformTextarea; + this._inputBoxes.add(dom.addDisposableListener(capturedFreeform, dom.EventType.INPUT, () => { + if (capturedFreeform.value.length > 0) { + updateSelection(-1); + } else { + this.saveCurrentAnswer(); + } + })); + + freeformContainer.appendChild(freeformTextarea); + container.appendChild(freeformContainer); + this._freeformTextareas.set(question.id, freeformTextarea); + + // Resize textarea if it has restored content + if (previousFreeform !== undefined) { + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(capturedFreeform), () => autoResize())); + } + } // Keyboard navigation for the list this._inputBoxes.add(dom.addDisposableListener(selectContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { @@ -1017,7 +1091,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (numberIndex < listItems.length) { e.preventDefault(); updateSelection(numberIndex); - } else if (numberIndex === listItems.length) { + } else if (freeformTextarea && numberIndex === listItems.length) { e.preventDefault(); updateSelection(-1); freeformTextarea.focus(); @@ -1030,16 +1104,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } })); - // Resize textarea if it has restored content - if (previousFreeform !== undefined) { - this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize())); - } - // focus on the row when first rendered or textarea if it has content if (this._shouldAutoFocus()) { - if (previousFreeform) { - this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => { - freeformTextarea.focus(); + if (freeformTextarea && previousFreeform) { + const capturedFreeform = freeformTextarea; + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(capturedFreeform), () => { + capturedFreeform.focus(); })); } else if (listItems.length > 0) { const focusIndex = selectedIndex >= 0 ? selectedIndex : 0; @@ -1065,12 +1135,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // Restore previous answer if exists const previousAnswer = this._answers.get(question.id); - const previousFreeform = typeof previousAnswer === 'object' && previousAnswer !== null && hasKey(previousAnswer, { freeformValue: true }) - ? (previousAnswer as { freeformValue?: string }).freeformValue - : undefined; - const previousSelectedValues = typeof previousAnswer === 'object' && previousAnswer !== null && hasKey(previousAnswer, { selectedValues: true }) - ? (previousAnswer as { selectedValues?: unknown[] }).selectedValues - : (Array.isArray(previousAnswer) ? previousAnswer : []); + const prevMulti = typeof previousAnswer === 'object' && previousAnswer !== null && hasKey(previousAnswer, { selectedValues: true }) ? previousAnswer as IChatMultiSelectAnswer : undefined; + const previousFreeform = prevMulti?.freeformValue; + const previousSelectedValues = prevMulti?.selectedValues ?? []; // Get default option ids (for multiSelect, defaultValue can be string or string[]) const defaultOptionIds: string[] = Array.isArray(question.defaultValue) @@ -1164,30 +1231,38 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._multiSelectCheckboxes.set(question.id, checkboxes); - // Always show freeform input for multi-select questions - const freeformContainer = dom.$('.chat-question-freeform'); + // Show freeform input only when explicitly allowed + let freeformTextarea: HTMLTextAreaElement | undefined; + if (question.allowFreeformInput !== false) { + const freeformContainer = dom.$('.chat-question-freeform'); - // Number indicator for freeform (comes after all options) - const freeformNumber = dom.$('.chat-question-freeform-number'); - freeformNumber.textContent = `${options.length + 1}`; - freeformContainer.appendChild(freeformNumber); + // Number indicator for freeform (comes after all options) + const freeformNumber = dom.$('.chat-question-freeform-number'); + freeformNumber.textContent = `${options.length + 1}`; + freeformContainer.appendChild(freeformNumber); - const freeformTextarea = dom.$('textarea.chat-question-freeform-textarea'); - freeformTextarea.placeholder = localize('chat.questionCarousel.enterCustomAnswer', 'Enter custom answer'); - freeformTextarea.rows = 1; + freeformTextarea = dom.$('textarea.chat-question-freeform-textarea'); + freeformTextarea.placeholder = localize('chat.questionCarousel.enterCustomAnswer', 'Enter custom answer'); + freeformTextarea.rows = 1; - if (previousFreeform !== undefined) { - freeformTextarea.value = previousFreeform; + if (previousFreeform !== undefined) { + freeformTextarea.value = previousFreeform; + } + + // Setup auto-resize behavior + const autoResize = this.setupTextareaAutoResize(freeformTextarea); + this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => this.saveCurrentAnswer())); + + freeformContainer.appendChild(freeformTextarea); + container.appendChild(freeformContainer); + this._freeformTextareas.set(question.id, freeformTextarea); + + // Resize textarea if it has restored content + if (previousFreeform !== undefined) { + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize())); + } } - // Setup auto-resize behavior - const autoResize = this.setupTextareaAutoResize(freeformTextarea); - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => this.saveCurrentAnswer())); - - freeformContainer.appendChild(freeformTextarea); - container.appendChild(freeformContainer); - this._freeformTextareas.set(question.id, freeformTextarea); - // Keyboard navigation for the list this._inputBoxes.add(dom.addDisposableListener(selectContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); @@ -1221,23 +1296,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (numberIndex < checkboxes.length) { e.preventDefault(); checkboxes[numberIndex].domNode.click(); - } else if (numberIndex === checkboxes.length) { + } else if (freeformTextarea && numberIndex === checkboxes.length) { e.preventDefault(); freeformTextarea.focus(); } } })); - // Resize textarea if it has restored content - if (previousFreeform !== undefined) { - this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => autoResize())); - } - // Focus on the appropriate row when rendered or textarea if it has content if (this._shouldAutoFocus()) { - if (previousFreeform) { - this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(freeformTextarea), () => { - freeformTextarea.focus(); + if (freeformTextarea && previousFreeform) { + const capturedFreeform = freeformTextarea; + this._inputBoxes.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(capturedFreeform), () => { + capturedFreeform.focus(); })); } else if (listItems.length > 0) { const initialFocusIndex = firstCheckedIndex >= 0 ? firstCheckedIndex : 0; @@ -1249,7 +1320,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } } - private getCurrentAnswer(): unknown { + private getCurrentAnswer(): IChatQuestionAnswerValue | undefined { const question = this.carousel.questions[this._currentIndex]; if (!question) { return undefined; @@ -1258,12 +1329,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent switch (question.type) { case 'text': { const inputBox = this._textInputBoxes.get(question.id); - return inputBox?.value ?? question.defaultValue; + return inputBox?.value ?? (typeof question.defaultValue === 'string' ? question.defaultValue : Array.isArray(question.defaultValue) ? { selectedValues: question.defaultValue } : undefined); } case 'singleSelect': { const data = this._singleSelectItems.get(question.id); - let selectedValue: unknown = undefined; + let selectedValue: string | undefined = undefined; if (data && data.selectedIndex >= 0) { selectedValue = question.options?.[data.selectedIndex]?.value; } @@ -1278,17 +1349,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; if (freeformValue) { // Freeform takes priority - ignore selectedValue - return { selectedValue: undefined, freeformValue }; + return { selectedValue: undefined, freeformValue } satisfies IChatSingleSelectAnswer; } if (selectedValue !== undefined) { - return { selectedValue, freeformValue: undefined }; + return { selectedValue, freeformValue: undefined } satisfies IChatSingleSelectAnswer; } return undefined; } case 'multiSelect': { const checkboxes = this._multiSelectCheckboxes.get(question.id); - const selectedValues: unknown[] = []; + const selectedValues: string[] = []; if (checkboxes) { checkboxes.forEach((checkbox, index) => { if (checkbox.checked) { @@ -1307,13 +1378,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // Return whatever was selected - defaults are applied at render time when // checkboxes are initially checked, so empty selection means user unchecked all if (freeformValue || selectedValues.length > 0) { - return { selectedValues, freeformValue }; + return { selectedValues, freeformValue } satisfies IChatMultiSelectAnswer; } return undefined; } default: - return question.defaultValue; + return typeof question.defaultValue === 'string' ? question.defaultValue : Array.isArray(question.defaultValue) ? { selectedValues: question.defaultValue } : undefined; } } @@ -1374,33 +1445,29 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent /** * Formats an answer for display in the summary. */ - private formatAnswerForSummary(question: IChatQuestion, answer: unknown): string { + private formatAnswerForSummary(question: IChatQuestion, answer: IChatQuestionAnswerValue): string { switch (question.type) { case 'text': return String(answer); case 'singleSelect': { - if (typeof answer === 'object' && answer !== null && hasKey(answer, { selectedValue: true })) { - const { selectedValue, freeformValue } = answer as { selectedValue?: unknown; freeformValue?: string }; - const selectedLabel = question.options?.find(opt => opt.value === selectedValue)?.label; + if (typeof answer === 'object') { + const { selectedValue, freeformValue } = answer as IChatSingleSelectAnswer; + const selectedLabel = selectedValue !== undefined ? question.options?.find(opt => opt.value === selectedValue)?.label : undefined; // For singleSelect, freeform takes priority over selection if (freeformValue) { return freeformValue; } return selectedLabel ?? String(selectedValue ?? ''); } - // Handle case where selectedValue was stripped during JSON serialization (undefined values are omitted by JSON.stringify) - if (typeof answer === 'object' && answer !== null && hasKey(answer, { freeformValue: true })) { - return (answer as { freeformValue?: string }).freeformValue ?? ''; - } const label = question.options?.find(opt => opt.value === answer)?.label; return label ?? String(answer); } case 'multiSelect': { - if (typeof answer === 'object' && answer !== null && hasKey(answer, { selectedValues: true })) { - const { selectedValues, freeformValue } = answer as { selectedValues?: unknown[]; freeformValue?: string }; - const labels = (selectedValues ?? []) + if (typeof answer === 'object' && hasKey(answer, { selectedValues: true })) { + const { selectedValues, freeformValue } = answer; + const labels = selectedValues .map(v => question.options?.find(opt => opt.value === v)?.label ?? String(v)); // For multiSelect, combine selections and freeform with comma separator if (freeformValue) { @@ -1408,11 +1475,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } return labels.join(localize('chat.questionCarousel.listSeparator', ', ')); } - if (Array.isArray(answer)) { - return answer - .map(v => question.options?.find(opt => opt.value === v)?.label ?? String(v)) - .join(localize('chat.questionCarousel.listSeparator', ', ')); - } return String(answer); } @@ -1426,6 +1488,131 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return renderAsPlaintext(md); } + /** + * Validates the current question's answer against its validation rules. + * Returns true if valid, false if validation errors were shown. + */ + private validateCurrentQuestion(): boolean { + const question = this.carousel.questions[this._currentIndex]; + if (!question) { + return true; + } + + const answer = this._answers.get(question.id); + + // Check required + if (question.required && (answer === undefined || answer === '')) { + this.showValidationError(localize('chat.questionCarousel.required', 'This field is required')); + return false; + } + + // Validate text inputs + if (question.type === 'text' && question.validation && typeof answer === 'string' && answer !== '') { + const error = this.getValidationError(answer, question.validation); + if (error) { + this.showValidationError(error); + return false; + } + } + + this.clearValidationError(); + return true; + } + + /** + * Validates that all required questions have been answered. + * Returns true if all required fields are satisfied. + */ + private validateRequiredFields(): boolean { + for (let i = 0; i < this.carousel.questions.length; i++) { + const question = this.carousel.questions[i]; + if (!question.required) { + continue; + } + const answer = this._answers.get(question.id); + if (answer === undefined || answer === '') { + // Navigate to the unanswered required question + this.saveCurrentAnswer(); + this._currentIndex = i; + this.persistDraftState(); + this.renderCurrentQuestion(true); + this.showValidationError(localize('chat.questionCarousel.required', 'This field is required')); + return false; + } + } + return true; + } + + /** + * Returns a validation error message for the given value, or undefined if valid. + */ + private getValidationError(value: string, validation: IChatQuestionValidation): string | undefined { + if (validation.minLength !== undefined && value.length < validation.minLength) { + return localize('chat.questionCarousel.validation.minLength', 'Minimum length is {0}', validation.minLength); + } + if (validation.maxLength !== undefined && value.length > validation.maxLength) { + return localize('chat.questionCarousel.validation.maxLength', 'Maximum length is {0}', validation.maxLength); + } + if (validation.format) { + switch (validation.format) { + case 'email': + if (!value.includes('@')) { + return localize('chat.questionCarousel.validation.email', 'Please enter a valid email address'); + } + break; + case 'uri': + if (!URL.canParse(value)) { + return localize('chat.questionCarousel.validation.uri', 'Please enter a valid URI'); + } + break; + case 'date': { + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(value) || isNaN(new Date(value).getTime())) { + return localize('chat.questionCarousel.validation.date', 'Please enter a valid date (YYYY-MM-DD)'); + } + break; + } + case 'date-time': + if (isNaN(new Date(value).getTime())) { + return localize('chat.questionCarousel.validation.dateTime', 'Please enter a valid date-time'); + } + break; + } + } + if (validation.isInteger !== undefined || validation.minimum !== undefined || validation.maximum !== undefined) { + const num = Number(value); + if (isNaN(num)) { + return localize('chat.questionCarousel.validation.number', 'Please enter a valid number'); + } + if (validation.isInteger && !Number.isInteger(num)) { + return localize('chat.questionCarousel.validation.integer', 'Please enter a valid integer'); + } + if (validation.minimum !== undefined && num < validation.minimum) { + return localize('chat.questionCarousel.validation.minimum', 'Minimum value is {0}', validation.minimum); + } + if (validation.maximum !== undefined && num > validation.maximum) { + return localize('chat.questionCarousel.validation.maximum', 'Maximum value is {0}', validation.maximum); + } + } + return undefined; + } + + private showValidationError(message: string): void { + this._currentValidationError = message; + if (this._validationMessageElement) { + this._validationMessageElement.textContent = message; + this._validationMessageElement.style.display = ''; + } + } + + private clearValidationError(): void { + this._currentValidationError = undefined; + if (this._validationMessageElement) { + this._validationMessageElement.textContent = ''; + this._validationMessageElement.style.display = 'none'; + } + } + hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { // does not have same content when it is not skipped and is active and we stop the response if (!this._isSkipped && !this.carousel.isUsed && isResponseVM(element) && element.isComplete) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index 477ce894745..62d2a0a03b7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -386,6 +386,32 @@ } } +/* carousel-level message (e.g. from MCP elicitation) */ +.interactive-session .chat-question-carousel-container .chat-question-carousel-message { + padding: 8px 16px 0; + font-size: var(--vscode-chat-font-size-body-s); + color: var(--vscode-descriptionForeground); + + .rendered-markdown p { + margin: 0; + } +} + +/* field description below question title */ +.interactive-session .chat-question-carousel-container .chat-question-description { + padding: 4px 16px; + font-size: var(--vscode-chat-font-size-body-s); + color: var(--vscode-descriptionForeground); +} + +/* validation error message below input area */ +.interactive-session .chat-question-carousel-container .chat-question-validation-message { + padding: 0 16px 4px; + font-size: var(--vscode-chat-font-size-body-s); + color: var(--vscode-editorWarning-foreground); + flex-shrink: 0; +} + /* summary (after finished) */ .interactive-session .chat-question-carousel-summary { display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 727179577cf..14b3217cf6d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -56,7 +56,7 @@ import { IChatAgentMetadata } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatTextEditGroup } from '../../common/model/chatModel.js'; import { chatSubcommandLeader } from '../../common/requestParser/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, ChatRequestQueueKind, IChatConfirmation, IChatContentReference, IChatDisabledClaudeHooksPart, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatPullRequestContent, IChatQuestionCarousel, IChatService, IChatTask, IChatTaskSerialized, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, isChatFollowup } from '../../common/chatService/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, ChatRequestQueueKind, IChatConfirmation, IChatContentReference, IChatDisabledClaudeHooksPart, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatPullRequestContent, IChatQuestionAnswerValue, IChatQuestionAnswers, IChatQuestionCarousel, IChatService, IChatTask, IChatTaskSerialized, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, isChatFollowup } from '../../common/chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; import { localChatSessionType } from '../../common/chatSessionsService.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; @@ -2164,9 +2164,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, part: ChatQuestionCarouselPart) => { + const handleSubmit = async (answers: Map | undefined, part: ChatQuestionCarouselPart) => { // Mark the carousel as used and store the answers - const answersRecord = answers ? Object.fromEntries(answers) : undefined; + const answersRecord: IChatQuestionAnswers | undefined = answers ? Object.fromEntries(answers) : undefined; carousel.data = answersRecord ?? {}; carousel.isUsed = true; if (carousel instanceof ChatQuestionCarouselData) { @@ -2318,7 +2318,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined) => Promise, + submit: (answers: Map | undefined) => Promise, modelName: string | undefined, requestMessageText: string | undefined, ): void { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts b/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts index 51ea07629f0..04c0619459c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts @@ -15,7 +15,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { IChatQuestion, IChatQuestionCarousel } from '../../common/chatService/chatService.js'; +import { IChatQuestion, IChatQuestionAnswerValue, IChatQuestionCarousel, IChatSingleSelectAnswer } from '../../common/chatService/chatService.js'; import { ChatConfiguration } from '../../common/constants.js'; import { ChatMessageRole, getTextResponseFromStream, ILanguageModelsService } from '../../common/languageModels.js'; import { Event } from '../../../../../base/common/event.js'; @@ -59,7 +59,7 @@ export class ChatQuestionCarouselAutoReply extends Disposable { async autoReply( carousel: IChatQuestionCarousel, - submit: (answers: Map | undefined) => Promise, + submit: (answers: Map | undefined) => Promise, modelName: string | undefined, requestMessageText: string | undefined, token: CancellationToken, @@ -186,7 +186,7 @@ export class ChatQuestionCarouselAutoReply extends Disposable { carousel: IChatQuestionCarousel, requestMessageText: string | undefined, token: CancellationToken, - ): Promise> { + ): Promise> { const prompt = this.buildPrompt(carousel, requestMessageText, false); const response = await this.languageModelsService.sendChatRequest( modelId, @@ -217,13 +217,13 @@ export class ChatQuestionCarouselAutoReply extends Disposable { // #region Answer parsing and resolution - private parseAnswers(responseText: string, carousel: IChatQuestionCarousel): Map { + private parseAnswers(responseText: string, carousel: IChatQuestionCarousel): Map { const parsed = this.tryParseJsonObject(responseText); if (!parsed) { return new Map(); } - const answers = new Map(); + const answers = new Map(); for (const question of carousel.questions) { const rawAnswer = parsed[question.id]; const resolved = this.resolveAnswerFromRaw(question, rawAnswer); @@ -236,10 +236,10 @@ export class ChatQuestionCarouselAutoReply extends Disposable { private mergeAnswers( carousel: IChatQuestionCarousel, - resolvedAnswers: Map, - fallbackAnswers: Map, - ): Map { - const merged = new Map(); + resolvedAnswers: Map, + fallbackAnswers: Map, + ): Map { + const merged = new Map(); for (const question of carousel.questions) { const fallback = fallbackAnswers.get(question.id); if (this.hasDefaultValue(question) && fallback !== undefined) { @@ -270,7 +270,7 @@ export class ChatQuestionCarouselAutoReply extends Disposable { } } - private resolveAnswerFromRaw(question: IChatQuestion, raw: unknown): unknown | undefined { + private resolveAnswerFromRaw(question: IChatQuestion, raw: unknown): IChatQuestionAnswerValue | undefined { switch (question.type) { case 'text': { if (typeof raw === 'string') { @@ -305,7 +305,7 @@ export class ChatQuestionCarouselAutoReply extends Disposable { const match = selectedInput ? this.matchQuestionOption(question, selectedInput) : undefined; if (match) { - return { selectedValue: match.value, freeformValue: undefined }; + return { selectedValue: match.value, freeformValue: undefined } satisfies IChatSingleSelectAnswer; } return undefined; } @@ -341,7 +341,7 @@ export class ChatQuestionCarouselAutoReply extends Disposable { } } - private matchQuestionOption(question: IChatQuestion, rawInput: string): { id: string; value: unknown } | undefined { + private matchQuestionOption(question: IChatQuestion, rawInput: string): { id: string; value: string } | undefined { const options = question.options ?? []; if (!options.length) { return undefined; @@ -374,8 +374,8 @@ export class ChatQuestionCarouselAutoReply extends Disposable { // #region Fallback answers - buildFallbackCarouselAnswers(carousel: IChatQuestionCarousel, requestMessageText: string | undefined): Map { - const answers = new Map(); + buildFallbackCarouselAnswers(carousel: IChatQuestionCarousel, requestMessageText: string | undefined): Map { + const answers = new Map(); for (const question of carousel.questions) { const answer = this.getFallbackAnswerForQuestion(question, requestMessageText); if (answer !== undefined) { @@ -385,12 +385,12 @@ export class ChatQuestionCarouselAutoReply extends Disposable { return answers; } - private getFallbackAnswerForQuestion(question: IChatQuestion, requestMessageText: string | undefined): unknown { + private getFallbackAnswerForQuestion(question: IChatQuestion, requestMessageText: string | undefined): IChatQuestionAnswerValue | undefined { const fallbackFreeform = requestMessageText?.trim() || localize('chat.questionCarousel.autoReplyFallback', 'OK'); switch (question.type) { case 'text': - return question.defaultValue ?? fallbackFreeform; + return typeof question.defaultValue === 'string' ? question.defaultValue : Array.isArray(question.defaultValue) ? { selectedValues: question.defaultValue } : fallbackFreeform; case 'singleSelect': { const defaultOptionId = typeof question.defaultValue === 'string' ? question.defaultValue : undefined; const defaultOption = defaultOptionId ? question.options?.find(opt => opt.id === defaultOptionId) : undefined; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 309ed397d88..a083e6212a5 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -350,6 +350,18 @@ export interface IChatConfirmation { kind: 'confirmation'; } +/** + * Validation rules for a question in a question carousel. + */ +export interface IChatQuestionValidation { + minLength?: number; + maxLength?: number; + format?: 'email' | 'uri' | 'date' | 'date-time'; + minimum?: number; + maximum?: number; + isInteger?: boolean; +} + /** * Represents an individual question in a question carousel. */ @@ -358,11 +370,32 @@ export interface IChatQuestion { type: 'text' | 'singleSelect' | 'multiSelect'; title: string; message?: string | IMarkdownString; - options?: { id: string; label: string; value: unknown }[]; + description?: string; + options?: { id: string; label: string; value: string }[]; defaultValue?: string | string[]; allowFreeformInput?: boolean; + required?: boolean; + validation?: IChatQuestionValidation; } +/** Answer shape for a single-select question. */ +export interface IChatSingleSelectAnswer { + selectedValue?: string; + freeformValue?: string; +} + +/** Answer shape for a multi-select question. */ +export interface IChatMultiSelectAnswer { + selectedValues: string[]; + freeformValue?: string; +} + +/** Union of all possible answer values in a question carousel. */ +export type IChatQuestionAnswerValue = string | IChatSingleSelectAnswer | IChatMultiSelectAnswer; + +/** Record mapping question IDs to their typed answer values. */ +export type IChatQuestionAnswers = Record; + /** * A carousel for presenting multiple questions inline in the chat response. * Users can navigate between questions and submit their answers. @@ -373,9 +406,13 @@ export interface IChatQuestionCarousel { /** Unique identifier for resolving the carousel answers back to the extension */ resolveId?: string; /** Storage for collected answers when user submits */ - data?: Record; + data?: IChatQuestionAnswers; /** Whether the carousel has been submitted/skipped */ isUsed?: boolean; + /** Top-level message shown above the questions (e.g. from MCP elicitation message) */ + message?: string | IMarkdownString; + /** Source attribution (e.g. MCP server) */ + source?: ToolDataSource; kind: 'questionCarousel'; } @@ -1459,8 +1496,8 @@ export interface IChatService { readonly onDidPerformUserAction: Event; notifyUserAction(event: IChatUserActionEvent): void; - readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: Record | undefined }>; - notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void; + readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: IChatQuestionAnswers | undefined }>; + notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: IChatQuestionAnswers | undefined): void; readonly onDidDisposeSession: Event<{ readonly sessionResource: URI[]; readonly reason: 'cleared' }>; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 598a45ebec2..2d554ce0f8e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -40,7 +40,7 @@ import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, ICha import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../requestParser/chatRequestParser.js'; -import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatSendResultSent, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; +import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatSendResultSent, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatQuestionAnswers, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSessionsService } from '../chatSessionsService.js'; import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js'; @@ -117,7 +117,7 @@ export class ChatService extends Disposable implements IChatService { private readonly _onDidPerformUserAction = this._register(new Emitter()); public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; - private readonly _onDidReceiveQuestionCarouselAnswer = this._register(new Emitter<{ requestId: string; resolveId: string; answers: Record | undefined }>()); + private readonly _onDidReceiveQuestionCarouselAnswer = this._register(new Emitter<{ requestId: string; resolveId: string; answers: IChatQuestionAnswers | undefined }>()); public readonly onDidReceiveQuestionCarouselAnswer = this._onDidReceiveQuestionCarouselAnswer.event; private readonly _onDidDisposeSession = this._register(new Emitter<{ readonly sessionResource: URI[]; reason: 'cleared' }>()); @@ -272,7 +272,7 @@ export class ChatService extends Disposable implements IChatService { } } - notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void { + notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: IChatQuestionAnswers | undefined): void { this._onDidReceiveQuestionCarouselAnswer.fire({ requestId, resolveId, answers }); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts index ef8ba5ae529..5feb507b755 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatQuestionCarouselData.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { DeferredPromise } from '../../../../../../base/common/async.js'; -import { IChatQuestion, IChatQuestionCarousel } from '../../chatService/chatService.js'; +import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { IChatQuestion, IChatQuestionAnswers, IChatQuestionCarousel } from '../../chatService/chatService.js'; +import { ToolDataSource } from '../../tools/languageModelToolsService.js'; /** * Runtime representation of a question carousel with a {@link DeferredPromise} @@ -13,16 +15,18 @@ import { IChatQuestion, IChatQuestionCarousel } from '../../chatService/chatServ */ export class ChatQuestionCarouselData implements IChatQuestionCarousel { public readonly kind = 'questionCarousel' as const; - public readonly completion = new DeferredPromise<{ answers: Record | undefined }>(); - public draftAnswers: Record | undefined; + public readonly completion = new DeferredPromise<{ answers: IChatQuestionAnswers | undefined }>(); + public draftAnswers: IChatQuestionAnswers | undefined; public draftCurrentIndex: number | undefined; constructor( public questions: IChatQuestion[], public allowSkip: boolean, public resolveId?: string, - public data?: Record, + public data?: IChatQuestionAnswers, public isUsed?: boolean, + public message?: string | IMarkdownString, + public source?: ToolDataSource, ) { } toJSON(): IChatQuestionCarousel { @@ -33,6 +37,8 @@ export class ChatQuestionCarouselData implements IChatQuestionCarousel { resolveId: this.resolveId, data: this.data, isUsed: this.isUsed, + message: this.message, + source: this.source, }; } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index 7f7bb156f07..8dda3785b93 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -8,9 +8,10 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { hasKey } from '../../../../../../base/common/types.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; -import { IChatQuestion, IChatService } from '../../chatService/chatService.js'; +import { IChatQuestion, IChatQuestionAnswers, IChatQuestionAnswerValue, IChatMultiSelectAnswer, IChatService, IChatSingleSelectAnswer } from '../../chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../model/chatProgressTypes/chatQuestionCarouselData.js'; import { IChatRequestModel } from '../../model/chatModel.js'; import { ChatPermissionLevel } from '../../constants.js'; @@ -336,7 +337,7 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { }; } - protected convertCarouselAnswers(questions: IQuestion[], carouselAnswers: Record | undefined, idToHeaderMap: Map): IAnswerResult { + protected convertCarouselAnswers(questions: IQuestion[], carouselAnswers: IChatQuestionAnswers | undefined, idToHeaderMap: Map): IAnswerResult { const result: IAnswerResult = { answers: {} }; if (carouselAnswers) { @@ -362,7 +363,7 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { // Look up the answer using the internal ID that was used in the carousel const internalId = headerToIdMap.get(question.header); - const answer = internalId ? carouselAnswers[internalId] : undefined; + const answer: IChatQuestionAnswerValue | undefined = internalId ? carouselAnswers[internalId] : undefined; this.logService.trace(`[AskQuestionsTool] Processing question "${question.header}" (internal ID: ${internalId}), raw answer: ${JSON.stringify(answer)}, type: ${typeof answer}`); if (answer === undefined) { @@ -391,67 +392,36 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { freeText: null, skipped: false }; - } else if (typeof answer === 'object' && answer !== null) { - const answerObj = answer as Record; - const freeformValue = typeof answerObj.freeformValue === 'string' && answerObj.freeformValue ? answerObj.freeformValue : null; - const selectedValues = Array.isArray(answerObj.selectedValues) ? answerObj.selectedValues.map(v => String(v)) : undefined; - const selectedValue = answerObj.selectedValue; - const label = typeof answerObj.label === 'string' ? answerObj.label : undefined; - - if (selectedValues) { - result.answers[question.header] = { - selected: selectedValues, - freeText: freeformValue, - skipped: false - }; - } else if (typeof selectedValue === 'string') { - if (question.options?.some(opt => opt.label === selectedValue)) { - result.answers[question.header] = { - selected: [selectedValue], - freeText: freeformValue, - skipped: false - }; - } else { - result.answers[question.header] = { - selected: [], - freeText: freeformValue ?? selectedValue, - skipped: false - }; - } - } else if (Array.isArray(selectedValue)) { - result.answers[question.header] = { - selected: selectedValue.map(v => String(v)), - freeText: freeformValue, - skipped: false - }; - } else if (selectedValue === undefined || selectedValue === null) { - if (freeformValue) { - result.answers[question.header] = { - selected: [], - freeText: freeformValue, - skipped: false - }; - } else { - result.answers[question.header] = { - selected: [], - freeText: null, - skipped: true - }; - } - } else if (freeformValue) { + } else if (typeof answer === 'object' && hasKey(answer, { selectedValues: true })) { + const { selectedValues, freeformValue } = answer as IChatMultiSelectAnswer; + result.answers[question.header] = { + selected: selectedValues, + freeText: freeformValue ?? null, + skipped: false + }; + } else if (typeof answer === 'object' && (hasKey(answer, { selectedValue: true }) || hasKey(answer, { freeformValue: true }))) { + const { selectedValue, freeformValue } = answer as IChatSingleSelectAnswer; + if (freeformValue) { result.answers[question.header] = { selected: [], freeText: freeformValue, skipped: false }; - } else if (label) { - result.answers[question.header] = { - selected: [label], - freeText: null, - skipped: false - }; + } else if (selectedValue !== undefined) { + if (question.options?.some(opt => opt.label === selectedValue)) { + result.answers[question.header] = { + selected: [selectedValue], + freeText: null, + skipped: false + }; + } else { + result.answers[question.header] = { + selected: [], + freeText: selectedValue, + skipped: false + }; + } } else { - this.logService.warn(`[AskQuestionsTool] Unknown answer object format for "${question.header}": ${JSON.stringify(answer)}`); result.answers[question.header] = { selected: [], freeText: null, @@ -459,7 +429,7 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { }; } } else { - this.logService.warn(`[AskQuestionsTool] Unknown answer format for "${question.header}": ${typeof answer}`); + this.logService.warn(`[AskQuestionsTool] Unknown answer format for "${question.header}": ${JSON.stringify(answer)}`); result.answers[question.header] = { selected: [], freeText: null, @@ -517,8 +487,8 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { * Build carousel answer data keyed by carousel question IDs for rendering * the completed summary in the UI during autopilot mode. */ - private buildAutopilotCarouselAnswers(questions: IQuestion[], carousel: ChatQuestionCarouselData, idToHeaderMap: Map): Record { - const data: Record = {}; + private buildAutopilotCarouselAnswers(questions: IQuestion[], carousel: ChatQuestionCarouselData, idToHeaderMap: Map): IChatQuestionAnswers { + const data: IChatQuestionAnswers = {}; // Build reverse map: original header -> internal carousel question ID const headerToIdMap = new Map(); for (const [internalId, originalHeader] of idToHeaderMap) { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index 344a7b7238a..f5ede2843bf 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -17,7 +17,7 @@ import { workbenchInstantiationService } from '../../../../../test/browser/workb import { LocalAgentsSessionsController } from '../../../browser/agentSessions/localAgentSessionsController.js'; import { ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { IChatModel, IChatRequestModel, IChatResponseModel } from '../../../common/model/chatModel.js'; -import { ChatRequestQueueKind, IChatDetail, IChatService, IChatSessionStartOptions, ResponseModelState } from '../../../common/chatService/chatService.js'; +import { ChatRequestQueueKind, IChatDetail, IChatQuestionAnswers, IChatService, IChatSessionStartOptions, ResponseModelState } from '../../../common/chatService/chatService.js'; import { ChatSessionStatus, IChatSessionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; import { ChatAgentLocation } from '../../../common/constants.js'; @@ -173,7 +173,7 @@ class MockChatService implements IChatService { readonly onDidReceiveQuestionCarouselAnswer = Event.None; - notifyQuestionCarouselAnswer(_requestId: string, _resolveId: string, _answers: Record | undefined): void { } + notifyQuestionCarouselAnswer(_requestId: string, _resolveId: string, _answers: IChatQuestionAnswers | undefined): void { } async transferChatSession(): Promise { } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index 8b3dd5551d7..7bc6a6c0412 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -9,7 +9,7 @@ import { MarkdownString } from '../../../../../../../base/common/htmlContent.js' import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../../../../browser/widget/chatContentParts/chatQuestionCarouselPart.js'; -import { IChatQuestionCarousel } from '../../../../common/chatService/chatService.js'; +import { IChatQuestionAnswerValue, IChatQuestionCarousel } from '../../../../common/chatService/chatService.js'; import { IChatContentPartRenderContext } from '../../../../browser/widget/chatContentParts/chatContentParts.js'; import { ChatQuestionCarouselData } from '../../../../common/model/chatProgressTypes/chatQuestionCarouselData.js'; @@ -29,7 +29,7 @@ suite('ChatQuestionCarouselPart', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let widget: ChatQuestionCarouselPart; - let submittedAnswers: Map | undefined | null = null; + let submittedAnswers: Map | undefined | null = null; function createWidget(carousel: IChatQuestionCarousel): ChatQuestionCarouselPart { const instantiationService = workbenchInstantiationService(undefined, store); @@ -203,7 +203,7 @@ suite('ChatQuestionCarouselPart', () => { assert.strictEqual(checkboxes.length, 3, 'Should have 3 checkboxes'); }); - test('freeform textarea is always rendered for singleSelect', () => { + test('freeform textarea is rendered for singleSelect by default', () => { const carousel = createMockCarousel([ { id: 'q1', @@ -217,10 +217,10 @@ suite('ChatQuestionCarouselPart', () => { createWidget(carousel); const freeformTextarea = widget.domNode.querySelector('.chat-question-freeform-textarea'); - assert.ok(freeformTextarea, 'Freeform textarea should always be rendered for singleSelect'); + assert.ok(freeformTextarea, 'Freeform textarea should be rendered by default for singleSelect'); }); - test('freeform textarea is always rendered for multiSelect', () => { + test('freeform textarea is rendered for multiSelect by default', () => { const carousel = createMockCarousel([ { id: 'q1', @@ -234,7 +234,45 @@ suite('ChatQuestionCarouselPart', () => { createWidget(carousel); const freeformTextarea = widget.domNode.querySelector('.chat-question-freeform-textarea'); - assert.ok(freeformTextarea, 'Freeform textarea should always be rendered for multiSelect'); + assert.ok(freeformTextarea, 'Freeform textarea should be rendered by default for multiSelect'); + }); + + test('freeform textarea is hidden when allowFreeformInput is false for singleSelect', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + allowFreeformInput: false, + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' } + ] + } + ]); + createWidget(carousel); + + const freeformTextarea = widget.domNode.querySelector('.chat-question-freeform-textarea'); + assert.strictEqual(freeformTextarea, null, 'Freeform textarea should not be rendered when allowFreeformInput is false'); + }); + + test('freeform textarea is hidden when allowFreeformInput is false for multiSelect', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'multiSelect', + title: 'Choose multiple', + allowFreeformInput: false, + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' } + ] + } + ]); + createWidget(carousel); + + const freeformTextarea = widget.domNode.querySelector('.chat-question-freeform-textarea'); + assert.strictEqual(freeformTextarea, null, 'Freeform textarea should not be rendered when allowFreeformInput is false'); }); test('default options are pre-selected for singleSelect', () => { @@ -727,4 +765,163 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(skippedMessage, 'Should show skipped message when no data'); }); }); + + suite('Description and Message', () => { + test('renders question description when provided', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Email', description: 'Enter your email address' } + ]); + createWidget(carousel); + + const desc = widget.domNode.querySelector('.chat-question-description'); + assert.ok(desc, 'Description element should be rendered'); + assert.strictEqual(desc?.textContent, 'Enter your email address'); + }); + + test('does not render description element when not provided', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name' } + ]); + createWidget(carousel); + + const desc = widget.domNode.querySelector('.chat-question-description'); + assert.strictEqual(desc, null, 'Description element should not exist when not provided'); + }); + + test('renders carousel-level message on first question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name' }, + { id: 'q2', type: 'text', title: 'Email' } + ]); + carousel.message = 'Please fill in the following:'; + createWidget(carousel); + + const message = widget.domNode.querySelector('.chat-question-carousel-message'); + assert.ok(message, 'Carousel message should be rendered'); + assert.ok(message?.textContent?.includes('Please fill in the following:')); + }); + + test('renders carousel-level message as markdown', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name' } + ]); + carousel.message = new MarkdownString('**Important:** Fill this form'); + createWidget(carousel); + + const message = widget.domNode.querySelector('.chat-question-carousel-message'); + assert.ok(message, 'Carousel message should be rendered'); + assert.ok(message?.querySelector('.rendered-markdown'), 'Message should be rendered as markdown'); + }); + + test('shows required indicator on required questions', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name', required: true } + ]); + createWidget(carousel); + + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title?.textContent?.includes('*'), 'Required indicator (*) should be shown'); + }); + + test('does not show required indicator on optional questions', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Nickname' } + ]); + createWidget(carousel); + + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title?.textContent); + assert.ok(!title?.textContent?.includes('*'), 'Required indicator should not be shown'); + }); + }); + + suite('Validation', () => { + test('renders validation message element', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'text', + title: 'Email', + validation: { format: 'email' } + } + ]); + createWidget(carousel); + + const validationMsg = widget.domNode.querySelector('.chat-question-validation-message') as HTMLElement | null; + assert.ok(validationMsg, 'Validation message element should exist'); + assert.strictEqual(validationMsg?.style.display, 'none', 'Validation message should be hidden initially'); + }); + + test('blocks submit on required empty text field', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name', required: true } + ]); + createWidget(carousel); + + // Try to submit without entering a value + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; + assert.ok(submitButton, 'Submit button should exist'); + submitButton.click(); + + // Should show validation error and not submit + const validationMsg = widget.domNode.querySelector('.chat-question-validation-message'); + assert.ok(validationMsg?.textContent, 'Validation error should be shown'); + assert.strictEqual(submittedAnswers, null, 'Should not have submitted'); + }); + + test('next button is disabled when required text field is empty', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name', required: true }, + { id: 'q2', type: 'text', title: 'Age' } + ]); + createWidget(carousel); + + // Next button should be disabled since required field has no answer + const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLButtonElement; + assert.ok(nextButton, 'Next button should exist'); + assert.ok(nextButton.classList.contains('disabled'), 'Next button should be disabled when required field is empty'); + }); + + test('allows submit on required field with value', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Name', required: true } + ]); + createWidget(carousel); + + // Enter a value in the text input + const inputBox = widget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement; + assert.ok(inputBox, 'Input should exist'); + inputBox.value = 'John'; + inputBox.dispatchEvent(new Event('input', { bubbles: true })); + + // Submit + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; + submitButton.click(); + + assert.ok(submittedAnswers !== null, 'Should have submitted'); + }); + + test('validates required field across questions on submit', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Optional' }, + { id: 'q2', type: 'text', title: 'Required', required: true } + ]); + createWidget(carousel); + + // Navigate to q2 without filling q1 (optional, so allowed) + widget.navigateToNextQuestion(); + + // Go back to q1 and try to submit (q2 required but empty) + widget.navigateToPreviousQuestion(); + + // Cmd+Enter should check all required fields + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; + if (submitButton) { + submitButton.click(); + } + + // Should not submit because q2 is required but empty + assert.strictEqual(submittedAnswers, null, 'Should not submit when required field is empty'); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index e8fb7d438c2..ca141665f03 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -10,7 +10,7 @@ import { IObservable, observableValue } from '../../../../../../base/common/obse import { URI } from '../../../../../../base/common/uri.js'; import { IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../../common/model/chatModel.js'; import { IParsedChatRequest } from '../../../common/requestParser/chatParserTypes.js'; -import { ChatRequestQueueKind, ChatSendResult, IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; +import { ChatRequestQueueKind, ChatSendResult, IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatQuestionAnswers, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../common/constants.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; @@ -118,8 +118,8 @@ export class MockChatService implements IChatService { notifyUserAction(event: IChatUserActionEvent): void { throw new Error('Method not implemented.'); } - readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: Record | undefined }> = undefined!; - notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void { + readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: IChatQuestionAnswers | undefined }> = undefined!; + notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: IChatQuestionAnswers | undefined): void { throw new Error('Method not implemented.'); } readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!; diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts index f82b6bbe55d..8085c61d223 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js'; -import { NullLogService } from '../../../../../../../platform/log/common/log.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IChatQuestionAnswers, IChatService } from '../../../../common/chatService/chatService.js'; import { AskQuestionsTool, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; -import { IChatService } from '../../../../common/chatService/chatService.js'; class TestableAskQuestionsTool extends AskQuestionsTool { - public testConvertCarouselAnswers(questions: IQuestion[], carouselAnswers: Record | undefined): IAnswerResult { + public testConvertCarouselAnswers(questions: IQuestion[], carouselAnswers: IChatQuestionAnswers | undefined): IAnswerResult { // Create an identity map where each header is also the internal ID // This simulates the simple case for testing the answer conversion logic const idToHeaderMap = new Map(); @@ -70,7 +70,7 @@ suite('AskQuestionsTool - convertCarouselAnswers', () => { { header: 'Features', question: 'Pick features', multiSelect: true, options: [{ label: 'A' }, { label: 'B' }] } ]; - const result = tool.testConvertCarouselAnswers(questions, { Features: ['A', 'B'] }); + const result = tool.testConvertCarouselAnswers(questions, { Features: { selectedValues: ['A', 'B'] } }); assert.deepStrictEqual(result.answers['Features'], { selected: ['A', 'B'], freeText: null, skipped: false }); }); @@ -131,7 +131,7 @@ suite('AskQuestionsTool - convertCarouselAnswers', () => { const result = tool.testConvertCarouselAnswers(questions, { Q1: 'text', Q2: { selectedValue: 'A' }, - Q3: ['x', 'y'] + Q3: { selectedValues: ['x', 'y'] } }); assert.strictEqual(result.answers['Q1'].freeText, 'text'); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts b/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts index 622dc36f198..0e64866bd27 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts @@ -12,13 +12,15 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { ChatElicitationRequestPart } from '../../chat/common/model/chatProgressTypes/chatElicitationRequestPart.js'; +import { ChatQuestionCarouselData } from '../../chat/common/model/chatProgressTypes/chatQuestionCarouselData.js'; import { ChatModel } from '../../chat/common/model/chatModel.js'; -import { ElicitationState, IChatService } from '../../chat/common/chatService/chatService.js'; +import { ElicitationState, IChatQuestion, IChatQuestionAnswers, IChatQuestionValidation, IChatService } from '../../chat/common/chatService/chatService.js'; import { ElicitationKind, ElicitResult, IFormModeElicitResult, IMcpElicitationService, IMcpServer, IMcpToolCallContext, IUrlModeElicitResult, McpConnectionState, MpcResponseError } from '../common/mcpTypes.js'; import { mcpServerToSourceData } from '../common/mcpTypesUtils.js'; import { MCP } from '../common/modelContextProtocol.js'; @@ -88,41 +90,52 @@ export class McpElicitationService implements IMcpElicitationService { if (chatModel instanceof ChatModel) { const request = chatModel.getRequests().at(-1); if (request) { - const part = new ChatElicitationRequestPart( - localize('mcp.elicit.title', 'Request for Input'), - elicitation.message, - localize('msg.subtitle', "{0} (MCP Server)", server.definition.label), - localize('mcp.elicit.accept', 'Respond'), - localize('mcp.elicit.reject', 'Cancel'), - async () => { - const p = this._doElicitForm(elicitation, token); - resolve(p); - const result = await p; - part.acceptedResult = result.content; - return result.action === 'accept' ? ElicitationState.Accepted : ElicitationState.Rejected; - }, - () => { - resolve({ action: 'decline' }); - return Promise.resolve(ElicitationState.Rejected); - }, - mcpServerToSourceData(server), + const { questions, idToPropertyMap } = this._convertSchemaToQuestions(elicitation); + const carousel = new ChatQuestionCarouselData( + questions, + /* allowSkip */ true, + /* resolveId */ undefined, + /* data */ undefined, + /* isUsed */ undefined, + /* message */ new MarkdownString(elicitation.message), + /* source */ mcpServerToSourceData(server), ); - chatModel.acceptResponseProgress(request, part); + + chatModel.acceptResponseProgress(request, carousel); + + store.add(token.onCancellationRequested(() => { + carousel.completion.complete({ answers: undefined }); + })); + + carousel.completion.p.then(result => { + if (!result.answers) { + resolve({ action: 'cancel' }); + } else { + const content = this._convertCarouselAnswersToElicitResult( + result.answers, + idToPropertyMap, + elicitation.requestedSchema.properties, + ); + resolve({ action: 'accept', content }); + } + }); + return; } - } else { - const handle = this._notificationService.notify({ - message: elicitation.message, - source: localize('mcp.elicit.source', 'MCP Server ({0})', server.definition.label), - severity: Severity.Info, - actions: { - primary: [store.add(new Action('mcp.elicit.give', localize('mcp.elicit.give', 'Respond'), undefined, true, () => resolve(this._doElicitForm(elicitation, token))))], - secondary: [store.add(new Action('mcp.elicit.cancel', localize('mcp.elicit.cancel', 'Cancel'), undefined, true, () => resolve({ action: 'decline' })))], - } - }); - store.add(handle.onDidClose(() => resolve({ action: 'cancel' }))); - store.add(token.onCancellationRequested(() => resolve({ action: 'cancel' }))); } + // Fallback: no chat session → notification + quickpick + const handle = this._notificationService.notify({ + message: elicitation.message, + source: localize('mcp.elicit.source', 'MCP Server ({0})', server.definition.label), + severity: Severity.Info, + actions: { + primary: [store.add(new Action('mcp.elicit.give', localize('mcp.elicit.give', 'Respond'), undefined, true, () => resolve(this._doElicitForm(elicitation, token))))], + secondary: [store.add(new Action('mcp.elicit.cancel', localize('mcp.elicit.cancel', 'Cancel'), undefined, true, () => resolve({ action: 'decline' })))], + } + }); + store.add(handle.onDidClose(() => resolve({ action: 'cancel' }))); + store.add(token.onCancellationRequested(() => resolve({ action: 'cancel' }))); + }).finally(() => store.dispose()); return { kind: ElicitationKind.Form, value, dispose: () => { } }; @@ -518,4 +531,191 @@ export class McpElicitationService implements IMcpElicitationService { } return { isValid: true, parsedValue: parsed }; } + + /** + * Converts an MCP elicitation schema into IChatQuestion[] for the carousel UI. + * Returns the questions and a map from question ID to schema property name. + */ + private _convertSchemaToQuestions(elicitation: MCP.ElicitRequestFormParams | Pre20251125ElicitationParams): { questions: IChatQuestion[]; idToPropertyMap: Map } { + const properties = Object.entries(elicitation.requestedSchema.properties); + const requiredFields = new Set(elicitation.requestedSchema.required || []); + const questions: IChatQuestion[] = []; + const idToPropertyMap = new Map(); + + for (const [propertyName, schema] of properties) { + const id = generateUuid(); + idToPropertyMap.set(id, propertyName); + + const title = schema.title || propertyName; + const description = schema.description; + const isRequired = requiredFields.has(propertyName); + + if (schema.type === 'boolean') { + questions.push({ + id, + type: 'singleSelect', + title, + description, + required: isRequired, + allowFreeformInput: false, + options: [ + { id: 'true', label: localize('mcp.elicit.true', 'True'), value: 'true' }, + { id: 'false', label: localize('mcp.elicit.false', 'False'), value: 'false' }, + ], + defaultValue: schema.default !== undefined ? String(schema.default) : undefined, + }); + } else if (isLegacyTitledEnumSchema(schema)) { + questions.push({ + id, + type: 'singleSelect', + title, + description, + required: isRequired, + allowFreeformInput: false, + options: schema.enum.map((v, i) => ({ + id: v, + label: schema.enumNames[i] ? `${v} - ${schema.enumNames[i]}` : v, + value: v, + })), + defaultValue: schema.default, + }); + } else if (isTitledSingleEnumSchema(schema)) { + questions.push({ + id, + type: 'singleSelect', + title, + description, + required: isRequired, + allowFreeformInput: false, + options: schema.oneOf.map(({ const: value, title: optTitle }) => ({ + id: value, + label: optTitle ? `${value} - ${optTitle}` : value, + value, + })), + defaultValue: schema.default, + }); + } else if (isUntitledEnumSchema(schema)) { + questions.push({ + id, + type: 'singleSelect', + title, + description, + required: isRequired, + allowFreeformInput: false, + options: schema.enum.map(v => ({ id: v, label: v, value: v })), + defaultValue: schema.default, + }); + } else if (isTitledMultiEnumSchema(schema)) { + questions.push({ + id, + type: 'multiSelect', + title, + description, + required: isRequired, + allowFreeformInput: false, + options: schema.items.anyOf.map(({ const: value, title: optTitle }) => ({ + id: value, + label: optTitle ? `${value} - ${optTitle}` : value, + value, + })), + defaultValue: schema.default, + }); + } else if (isUntitledMultiEnumSchema(schema)) { + questions.push({ + id, + type: 'multiSelect', + title, + description, + required: isRequired, + allowFreeformInput: false, + options: schema.items.enum.map(v => ({ id: v, label: v, value: v })), + defaultValue: schema.default, + }); + } else { + // String, number, integer → text input with validation + const validation: IChatQuestionValidation = {}; + if (schema.type === 'string') { + if (schema.minLength !== undefined) { validation.minLength = schema.minLength; } + if (schema.maxLength !== undefined) { validation.maxLength = schema.maxLength; } + if (schema.format) { validation.format = schema.format; } + } else if (schema.type === 'number' || schema.type === 'integer') { + if (schema.minimum !== undefined) { validation.minimum = schema.minimum; } + if (schema.maximum !== undefined) { validation.maximum = schema.maximum; } + if (schema.type === 'integer') { validation.isInteger = true; } + } + + questions.push({ + id, + type: 'text', + title, + description, + required: isRequired, + defaultValue: schema.default !== undefined ? String(schema.default) : undefined, + validation: Object.keys(validation).length > 0 ? validation : undefined, + }); + } + } + + return { questions, idToPropertyMap }; + } + + /** + * Converts carousel answers (keyed by question ID) back into the + * MCP ElicitResult content format (keyed by schema property names), + * coercing types as needed. + */ + private _convertCarouselAnswersToElicitResult( + answers: IChatQuestionAnswers, + idToPropertyMap: Map, + schemaProperties: Record, + ): Record { + const content: Record = {}; + + for (const [questionId, answer] of Object.entries(answers)) { + const propertyName = idToPropertyMap.get(questionId); + if (!propertyName) { + continue; + } + + const schema = schemaProperties[propertyName]; + if (!schema) { + continue; + } + + // Extract the raw value from structured answers + let rawValue: unknown = answer; + if (typeof answer === 'object' && answer !== null) { + const obj = answer as Record; + if ('selectedValue' in obj) { + rawValue = obj.selectedValue; + } else if ('selectedValues' in obj) { + rawValue = obj.selectedValues; + } else if ('freeformValue' in obj && obj.freeformValue) { + rawValue = obj.freeformValue; + } + } + + if (rawValue === undefined || rawValue === null) { + continue; + } + + // Type coercion based on schema + if (schema.type === 'boolean') { + content[propertyName] = rawValue === 'true' || rawValue === true; + } else if (schema.type === 'number' || schema.type === 'integer') { + const num = Number(rawValue); + if (!isNaN(num)) { + content[propertyName] = num; + } + } else if (schema.type === 'array') { + if (Array.isArray(rawValue)) { + content[propertyName] = rawValue.map(v => String(v)); + } + } else { + content[propertyName] = String(rawValue); + } + } + + return content; + } } From 5a356d228e3fd44a8a40fabe0a4e4f9433bd4fdb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 10 Mar 2026 09:25:06 +0100 Subject: [PATCH 406/448] Enhance quick suggestions with inline completions (#300371) * Enhance quick suggestions behavior with inline completions: allow triggering when inline provider returns no results * Improve inline completions handling: suppress suggestions when inline completions are active * CCR --- src/vs/editor/common/config/editorOptions.ts | 4 +- .../browser/model/suggestWidgetAdapter.ts | 12 + .../contrib/suggest/browser/suggestModel.ts | 93 ++++++- .../suggest/test/browser/suggestModel.test.ts | 235 +++++++++++++++--- 4 files changed, 305 insertions(+), 39 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 287992d1744..313793845dd 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -3772,7 +3772,7 @@ class EditorQuickSuggestions extends BaseEditorOption { if (this._triggerState !== undefined) { return; @@ -409,16 +420,19 @@ export class SuggestModel implements IDisposable { return; } + let waitForInlineCompletions = false; if (!QuickSuggestionsOptions.isAllOn(config)) { // Check the type of the token that triggered this model.tokenization.tokenizeIfCheap(pos.lineNumber); const lineTokens = model.tokenization.getLineTokens(pos.lineNumber); const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); - if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'on') { - if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'offWhenInlineCompletions' - || (this._languageFeaturesService.inlineCompletionsProvider.has(model) && this._editor.getOption(EditorOption.inlineSuggest).enabled)) { - return; - } + const value = QuickSuggestionsOptions.valueFor(config, tokenType); + if (value === 'off' || value === 'inline') { + return; + } + if (value === 'offWhenInlineCompletions') { + waitForInlineCompletions = this._languageFeaturesService.inlineCompletionsProvider.has(model) + && this._editor.getOption(EditorOption.inlineSuggest).enabled; } } @@ -431,12 +445,73 @@ export class SuggestModel implements IDisposable { return; } - // we made it till here -> trigger now - this.trigger({ auto: true }); + if (waitForInlineCompletions) { + // Wait for inline completions to resolve before deciding + this._waitForInlineCompletionsAndTrigger(model, pos); + } else { + this.trigger({ auto: true }); + } }, this._editor.getOption(EditorOption.quickSuggestionsDelay)); } + private _waitForInlineCompletionsAndTrigger(initialModel: ITextModel, initialPosition: Position): void { + const initialModelVersion = initialModel.getVersionId(); + const inlineController = getInlineCompletionsController(this._editor); + const inlineModel = inlineController?.model.get(); + if (!inlineModel) { + this.trigger({ auto: true }); + return; + } + + const state = inlineModel.state.get(); + if (state?.inlineSuggestion) { + // Inline completions are already showing - suppress + return; + } + + const store = new DisposableStore(); + this._waitForInlineCompletions = store; + + const triggerAndCleanUp = (doTrigger: boolean) => { + store.dispose(); + if (this._waitForInlineCompletions === store) { + this._waitForInlineCompletions = undefined; + } + if (this._triggerState !== undefined) { + return; + } + if (!doTrigger) { + return; + } + const currentModel = this._editor.getModel(); + const currentPosition = this._editor.getPosition(); + if (currentModel === initialModel + && currentModel.getVersionId() === initialModelVersion + && currentPosition?.equals(initialPosition) + && this._editor.hasWidgetFocus() + ) { + this.trigger({ auto: true }); + } + }; + + // Race: observe inline completions state vs 750ms timeout + disposableTimeout(() => { + triggerAndCleanUp(true); + inlineModel.stop('automatic'); + }, 750, store); + + store.add(autorun(reader => { + const status = inlineModel.status.read(reader); + const currentState = inlineModel.state.read(reader); + if (!currentState && status === 'loading') { + // Still loading + return; + } + triggerAndCleanUp(!currentState); + })); + } + private _refilterCompletionItems(): void { assertType(this._editor.hasModel()); assertType(this._triggerState !== undefined); diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index ff465706c0c..d99d63f7c98 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -25,7 +25,7 @@ import { SuggestController } from '../../browser/suggestController.js'; import { ISuggestMemoryService } from '../../browser/suggestMemory.js'; import { LineContext, SuggestModel } from '../../browser/suggestModel.js'; import { ISelectedSuggestion } from '../../browser/suggestWidget.js'; -import { createTestCodeEditor, ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; +import { createTestCodeEditor, ITestCodeEditor, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { createModelServices, createTextModel, instantiateTextModel } from '../../../../test/common/testTextModel.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; @@ -42,6 +42,15 @@ import { getSnippetSuggestSupport, setSnippetSuggestSupport } from '../../browse import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../base/common/async.js'; +import { InlineCompletionsController } from '../../../inlineCompletions/browser/controller/inlineCompletionsController.js'; +import { InlineSuggestionsView } from '../../../inlineCompletions/browser/view/inlineSuggestionsView.js'; +import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { IMenuService, IMenu } from '../../../../../platform/actions/common/actions.js'; +import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; +import { IEditorWorkerService } from '../../../../common/services/editorWorker.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { ModifierKeyEmitter } from '../../../../../base/browser/dom.js'; function createMockEditor(model: TextModel, languageFeaturesService: ILanguageFeaturesService): ITestCodeEditor { @@ -1230,11 +1239,11 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); }); - test('offWhenInlineCompletions - suppresses quick suggest when inline provider exists', function () { + test('offWhenInlineCompletions - allows quick suggest when inline provider returns empty results', function () { disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport)); - // Register a dummy inline completions provider + // Register a dummy inline completions provider that returns no items const inlineProvider: InlineCompletionsProvider = { provideInlineCompletions: () => ({ items: [] }), disposeInlineCompletions: () => { } @@ -1244,20 +1253,12 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { return withOracle((suggestOracle, editor) => { editor.updateOptions({ quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' } }); - return new Promise((resolve, reject) => { - const unexpectedSuggestSub = suggestOracle.onDidSuggest(() => { - unexpectedSuggestSub.dispose(); - reject(new Error('Quick suggestions should not have been triggered')); - }); - + // Without an InlineCompletionsController, the fallback triggers immediately + return assertEvent(suggestOracle.onDidSuggest, () => { editor.setPosition({ lineNumber: 1, column: 4 }); editor.trigger('keyboard', Handler.Type, { text: 'd' }); - - // Wait for the quick suggest delay to pass without triggering - setTimeout(() => { - unexpectedSuggestSub.dispose(); - resolve(); - }, 200); + }, suggestEvent => { + assert.strictEqual(suggestEvent.triggerOptions.auto, true); }); }); }); @@ -1336,7 +1337,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); }); - test('string shorthand - "offWhenInlineCompletions" suppresses when inline provider exists', function () { + test('string shorthand - "offWhenInlineCompletions" allows quick suggest when inline provider returns empty', function () { return runWithFakedTimers({ useFakeTimers: true }, () => { disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport)); @@ -1347,24 +1348,202 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider)); return withOracle((suggestOracle, editor) => { - // Use string shorthand — applies to all token types + // Use string shorthand - applies to all token types editor.updateOptions({ quickSuggestions: 'offWhenInlineCompletions' }); - return new Promise((resolve, reject) => { - const sub = suggestOracle.onDidSuggest(() => { - sub.dispose(); - reject(new Error('Quick suggestions should have been suppressed by offWhenInlineCompletions shorthand')); - }); - + // Without InlineCompletionsController, the fallback triggers immediately + return assertEvent(suggestOracle.onDidSuggest, () => { editor.setPosition({ lineNumber: 1, column: 4 }); editor.trigger('keyboard', Handler.Type, { text: 'd' }); - - setTimeout(() => { - sub.dispose(); - resolve(); - }, 200); + }, suggestEvent => { + assert.strictEqual(suggestEvent.triggerOptions.auto, true); }); }); }); }); }); + +suite('SuggestModel - offWhenInlineCompletions with InlineCompletionsController', function () { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const completionProvider: CompletionItemProvider = { + _debugDisplayName: 'test', + provideCompletionItems(doc, pos): CompletionList { + const wordUntil = doc.getWordUntilPosition(pos); + return { + incomplete: false, + suggestions: [{ + label: doc.getWordUntilPosition(pos).word, + kind: CompletionItemKind.Property, + insertText: 'foofoo', + range: new Range(pos.lineNumber, wordUntil.startColumn, pos.lineNumber, wordUntil.endColumn) + }] + }; + } + }; + + async function withSuggestModelAndInlineCompletions( + text: string, + inlineProvider: InlineCompletionsProvider, + callback: (suggestModel: SuggestModel, editor: ITestCodeEditor) => Promise, + ): Promise { + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const disposableStore = new DisposableStore(); + try { + const languageFeaturesService = new LanguageFeaturesService(); + disposableStore.add(languageFeaturesService.completionProvider.register({ pattern: '**' }, completionProvider)); + disposableStore.add(languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, inlineProvider)); + + const serviceCollection = new ServiceCollection( + [ILanguageFeaturesService, languageFeaturesService], + [ITelemetryService, NullTelemetryService], + [ILogService, new NullLogService()], + [IStorageService, disposableStore.add(new InMemoryStorageService())], + [IKeybindingService, new MockKeybindingService()], + [IEditorWorkerService, new class extends mock() { + override computeWordRanges() { + return Promise.resolve({}); + } + }], + [ISuggestMemoryService, new class extends mock() { + override memorize(): void { } + override select(): number { return 0; } + }], + [IMenuService, new class extends mock() { + override createMenu() { + return new class extends mock() { + override onDidChange = Event.None; + override dispose() { } + }; + } + }], + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], + [IEnvironmentService, new class extends mock() { + override isBuilt: boolean = true; + override isExtensionDevelopment: boolean = false; + }], + [IAccessibilitySignalService, new class extends mock() { + override async playSignal() { } + override isSoundEnabled() { return false; } + }], + [IDefaultAccountService, new class extends mock() { + override onDidChangeDefaultAccount = Event.None; + override getDefaultAccount = async () => null; + override setDefaultAccountProvider = () => { }; + }], + ); + + await withAsyncTestCodeEditor(text, { serviceCollection }, async (editor, _editorViewModel, instantiationService) => { + instantiationService.stubInstance(InlineSuggestionsView, { + dispose: () => { } + }); + editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); + editor.registerAndInstantiateContribution(InlineCompletionsController.ID, InlineCompletionsController); + + editor.hasWidgetFocus = () => true; + editor.updateOptions({ + quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' }, + }); + + const suggestModel = disposableStore.add( + editor.invokeWithinContext(accessor => accessor.get(IInstantiationService).createInstance(SuggestModel, editor)) + ); + + await callback(suggestModel, editor); + }); + } finally { + disposableStore.dispose(); + ModifierKeyEmitter.disposeInstance(); + } + }); + } + + test('suppresses quick suggest when inline completions are showing ghost text', async function () { + const inlineProvider: InlineCompletionsProvider = { + provideInlineCompletions: (model, pos) => { + // Return a completion that extends the current word - must be visible at cursor + const word = model.getWordAtPosition(pos); + if (!word) { return { items: [] }; } + return { + items: [{ + insertText: word.word + 'Suffix', + range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn), + }] + }; + }, + disposeInlineCompletions: () => { } + }; + + await withSuggestModelAndInlineCompletions('abc def', inlineProvider, async (suggestModel, editor) => { + let didSuggest = false; + const sub = suggestModel.onDidSuggest(() => { didSuggest = true; }); + + editor.setPosition({ lineNumber: 1, column: 4 }); + editor.trigger('keyboard', Handler.Type, { text: 'd' }); + + await timeout(200); + + sub.dispose(); + assert.strictEqual(didSuggest, false, 'Quick suggestions should have been suppressed when inline completions are showing'); + }); + }); + + test('allows quick suggest when inline completions resolve with no results', async function () { + const inlineProvider: InlineCompletionsProvider = { + provideInlineCompletions: () => ({ items: [] }), + disposeInlineCompletions: () => { } + }; + + await withSuggestModelAndInlineCompletions('abc def', inlineProvider, async (suggestModel, editor) => { + let didSuggest = false; + const sub = suggestModel.onDidSuggest(e => { + didSuggest = true; + assert.strictEqual(e.triggerOptions.auto, true); + }); + + editor.setPosition({ lineNumber: 1, column: 4 }); + editor.trigger('keyboard', Handler.Type, { text: 'd' }); + + await timeout(200); + + sub.dispose(); + assert.strictEqual(didSuggest, true, 'Quick suggestions should have been triggered after inline completions resolved empty'); + }); + }); + + test('allows quick suggest when inlineSuggest is disabled even with provider', async function () { + const inlineProvider: InlineCompletionsProvider = { + provideInlineCompletions: (model, pos) => { + const word = model.getWordAtPosition(pos); + if (!word) { return { items: [] }; } + return { + items: [{ + insertText: word.word + 'Suffix', + range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn), + }] + }; + }, + disposeInlineCompletions: () => { } + }; + + await withSuggestModelAndInlineCompletions('abc def', inlineProvider, async (suggestModel, editor) => { + editor.updateOptions({ inlineSuggest: { enabled: false } }); + + let didSuggest = false; + const sub = suggestModel.onDidSuggest(e => { + didSuggest = true; + assert.strictEqual(e.triggerOptions.auto, true); + }); + + editor.setPosition({ lineNumber: 1, column: 4 }); + editor.trigger('keyboard', Handler.Type, { text: 'd' }); + + await timeout(200); + + sub.dispose(); + assert.strictEqual(didSuggest, true, 'Quick suggestions should have been triggered when inlineSuggest is disabled'); + }); + }); +}); From 11246017b66d444e6aa5e3a2535e161498182b51 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 10 Mar 2026 11:09:40 +0100 Subject: [PATCH 407/448] Revert "Debug Panel: oTel data source support and Import/export (#299256)" (#300381) This reverts commit 5c842594811efeb339c22bafe5e9fb7ce27bfa51. --- .../common/extensionsApiProposals.ts | 2 +- .../api/browser/mainThreadChatDebug.ts | 47 -------- .../workbench/api/common/extHost.protocol.ts | 2 - .../workbench/api/common/extHostChatDebug.ts | 103 +---------------- .../actions/chatOpenAgentDebugPanelAction.ts | 105 +----------------- .../chat/browser/chatDebug/chatDebugEditor.ts | 47 ++++++-- .../browser/chatDebug/chatDebugFlowGraph.ts | 53 +++++---- .../browser/chatDebug/chatDebugFlowLayout.ts | 10 +- .../browser/chatDebug/chatDebugHomeView.ts | 49 ++++---- .../browser/chatDebug/chatDebugLogsView.ts | 34 +----- .../contrib/chat/common/chatDebugService.ts | 30 ----- .../chat/common/chatDebugServiceImpl.ts | 55 +-------- src/vscode-dts/vscode.proposed.chatDebug.d.ts | 65 +---------- 13 files changed, 100 insertions(+), 502 deletions(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 00e09a016ac..a9bc2d2fa10 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -45,7 +45,7 @@ const _allApiProposals = { }, chatDebug: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatDebug.d.ts', - version: 3 + version: 2 }, chatHooks: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatHooks.d.ts', diff --git a/src/vs/workbench/api/browser/mainThreadChatDebug.ts b/src/vs/workbench/api/browser/mainThreadChatDebug.ts index 169324d37e7..82594dcb038 100644 --- a/src/vs/workbench/api/browser/mainThreadChatDebug.ts +++ b/src/vs/workbench/api/browser/mainThreadChatDebug.ts @@ -5,9 +5,7 @@ import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; -import { VSBuffer } from '../../../base/common/buffer.js'; import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugService } from '../../contrib/chat/common/chatDebugService.js'; -import { IChatService } from '../../contrib/chat/common/chatService/chatService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostChatDebugShape, ExtHostContext, IChatDebugEventDto, MainContext, MainThreadChatDebugShape } from '../common/extHost.protocol.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; @@ -21,7 +19,6 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb constructor( extHostContext: IExtHostContext, @IChatDebugService private readonly _chatDebugService: IChatDebugService, - @IChatService private readonly _chatService: IChatService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatDebug); @@ -39,26 +36,6 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb }, resolveChatDebugLogEvent: async (eventId, token) => { return this._proxy.$resolveChatDebugLogEvent(handle, eventId, token); - }, - provideChatDebugLogExport: async (sessionResource, token) => { - // Gather core events and session title to pass to the extension. - const coreEventDtos = this._chatDebugService.getEvents(sessionResource) - .filter(e => this._chatDebugService.isCoreEvent(e)) - .map(e => this._serializeEvent(e)); - const sessionTitle = this._chatService.getSessionTitle(sessionResource); - const result = await this._proxy.$exportChatDebugLog(handle, sessionResource, coreEventDtos, sessionTitle, token); - return result?.buffer; - }, - resolveChatDebugLogImport: async (data, token) => { - const result = await this._proxy.$importChatDebugLog(handle, VSBuffer.wrap(data), token); - if (!result) { - return undefined; - } - const uri = URI.revive(result.uri); - if (result.sessionTitle) { - this._chatDebugService.setImportedSessionTitle(uri, result.sessionTitle); - } - return uri; } })); } @@ -81,30 +58,6 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb this._chatDebugService.addProviderEvent(revived); } - private _serializeEvent(event: IChatDebugEvent): IChatDebugEventDto { - const base = { - id: event.id, - sessionResource: event.sessionResource, - created: event.created.getTime(), - parentEventId: event.parentEventId, - }; - - switch (event.kind) { - case 'toolCall': - return { ...base, kind: 'toolCall', toolName: event.toolName, toolCallId: event.toolCallId, input: event.input, output: event.output, result: event.result, durationInMillis: event.durationInMillis }; - case 'modelTurn': - return { ...base, kind: 'modelTurn', model: event.model, requestName: event.requestName, inputTokens: event.inputTokens, outputTokens: event.outputTokens, totalTokens: event.totalTokens, durationInMillis: event.durationInMillis }; - case 'generic': - return { ...base, kind: 'generic', name: event.name, details: event.details, level: event.level, category: event.category }; - case 'subagentInvocation': - return { ...base, kind: 'subagentInvocation', agentName: event.agentName, description: event.description, status: event.status, durationInMillis: event.durationInMillis, toolCallCount: event.toolCallCount, modelTurnCount: event.modelTurnCount }; - case 'userMessage': - return { ...base, kind: 'userMessage', message: event.message, sections: event.sections.map(s => ({ name: s.name, content: s.content })) }; - case 'agentResponse': - return { ...base, kind: 'agentResponse', message: event.message, sections: event.sections.map(s => ({ name: s.name, content: s.content })) }; - } - } - private _reviveEvent(dto: IChatDebugEventDto, sessionResource: URI): IChatDebugEvent { const base = { id: dto.id, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6f5d4d775a1..eb65735e58a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1502,8 +1502,6 @@ export type IChatDebugResolvedEventContentDto = IChatDebugEventTextContentDto | export interface ExtHostChatDebugShape { $provideChatDebugLog(handle: number, sessionResource: UriComponents, token: CancellationToken): Promise; $resolveChatDebugLogEvent(handle: number, eventId: string, token: CancellationToken): Promise; - $exportChatDebugLog(handle: number, sessionResource: UriComponents, coreEvents: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise; - $importChatDebugLog(handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined>; } export interface MainThreadChatDebugShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostChatDebug.ts b/src/vs/workbench/api/common/extHostChatDebug.ts index b83bb2f3cf5..04125f3e551 100644 --- a/src/vs/workbench/api/common/extHostChatDebug.ts +++ b/src/vs/workbench/api/common/extHostChatDebug.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtHostChatDebugShape, IChatDebugEventDto, IChatDebugResolvedEventContentDto, MainContext, MainThreadChatDebugShape } from './extHost.protocol.js'; -import { ChatDebugGenericEvent, ChatDebugLogLevel, ChatDebugMessageContentType, ChatDebugMessageSection, ChatDebugModelTurnEvent, ChatDebugSubagentInvocationEvent, ChatDebugSubagentStatus, ChatDebugToolCallEvent, ChatDebugToolCallResult, ChatDebugUserMessageEvent, ChatDebugAgentResponseEvent } from './extHostTypes.js'; +import { ChatDebugMessageContentType, ChatDebugSubagentStatus, ChatDebugToolCallResult } from './extHostTypes.js'; import { IExtHostRpcService } from './extHostRpcService.js'; export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShape { @@ -292,106 +291,6 @@ export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShap } } - private _deserializeEvent(dto: IChatDebugEventDto): vscode.ChatDebugEvent | undefined { - const created = new Date(dto.created); - const sessionResource = dto.sessionResource ? URI.revive(dto.sessionResource) : undefined; - switch (dto.kind) { - case 'toolCall': { - const evt = new ChatDebugToolCallEvent(dto.toolName, created); - evt.id = dto.id; - evt.sessionResource = sessionResource; - evt.parentEventId = dto.parentEventId; - evt.toolCallId = dto.toolCallId; - evt.input = dto.input; - evt.output = dto.output; - evt.result = dto.result === 'success' ? ChatDebugToolCallResult.Success - : dto.result === 'error' ? ChatDebugToolCallResult.Error - : undefined; - evt.durationInMillis = dto.durationInMillis; - return evt; - } - case 'modelTurn': { - const evt = new ChatDebugModelTurnEvent(created); - evt.id = dto.id; - evt.sessionResource = sessionResource; - evt.parentEventId = dto.parentEventId; - evt.model = dto.model; - evt.inputTokens = dto.inputTokens; - evt.outputTokens = dto.outputTokens; - evt.totalTokens = dto.totalTokens; - evt.durationInMillis = dto.durationInMillis; - return evt; - } - case 'generic': { - const evt = new ChatDebugGenericEvent(dto.name, dto.level as ChatDebugLogLevel, created); - evt.id = dto.id; - evt.sessionResource = sessionResource; - evt.parentEventId = dto.parentEventId; - evt.details = dto.details; - evt.category = dto.category; - return evt; - } - case 'subagentInvocation': { - const evt = new ChatDebugSubagentInvocationEvent(dto.agentName, created); - evt.id = dto.id; - evt.sessionResource = sessionResource; - evt.parentEventId = dto.parentEventId; - evt.description = dto.description; - evt.status = dto.status === 'running' ? ChatDebugSubagentStatus.Running - : dto.status === 'completed' ? ChatDebugSubagentStatus.Completed - : dto.status === 'failed' ? ChatDebugSubagentStatus.Failed - : undefined; - evt.durationInMillis = dto.durationInMillis; - evt.toolCallCount = dto.toolCallCount; - evt.modelTurnCount = dto.modelTurnCount; - return evt; - } - case 'userMessage': { - const evt = new ChatDebugUserMessageEvent(dto.message, created); - evt.id = dto.id; - evt.sessionResource = sessionResource; - evt.parentEventId = dto.parentEventId; - evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content)); - return evt; - } - case 'agentResponse': { - const evt = new ChatDebugAgentResponseEvent(dto.message, created); - evt.id = dto.id; - evt.sessionResource = sessionResource; - evt.parentEventId = dto.parentEventId; - evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content)); - return evt; - } - default: - return undefined; - } - } - - async $exportChatDebugLog(_handle: number, sessionResource: UriComponents, coreEventDtos: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise { - if (!this._provider?.provideChatDebugLogExport) { - return undefined; - } - const sessionUri = URI.revive(sessionResource); - const coreEvents = coreEventDtos.map(dto => this._deserializeEvent(dto)).filter((e): e is vscode.ChatDebugEvent => e !== undefined); - const options: vscode.ChatDebugLogExportOptions = { coreEvents, sessionTitle }; - const result = await this._provider.provideChatDebugLogExport(sessionUri, options, token); - if (!result) { - return undefined; - } - return VSBuffer.wrap(result); - } - - async $importChatDebugLog(_handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined> { - if (!this._provider?.resolveChatDebugLogImport) { - return undefined; - } - const result = await this._provider.resolveChatDebugLogImport(data.buffer, token); - if (!result) { - return undefined; - } - return { uri: result.uri, sessionTitle: result.sessionTitle }; - } - override dispose(): void { for (const store of this._activeProgress.values()) { store.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts index 685ebc58948..860559d64e3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts @@ -3,18 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize, localize2 } from '../../../../../nls.js'; +import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -22,7 +16,6 @@ import { IChatDebugService } from '../../common/chatDebugService.js'; import { ChatViewId, IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js'; import { ChatDebugEditorInput } from '../chatDebug/chatDebugEditorInput.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; import { IChatDebugEditorOptions } from '../chatDebug/chatDebugTypes.js'; /** @@ -99,100 +92,4 @@ export function registerChatOpenAgentDebugPanelAction() { await editorService.openEditor(ChatDebugEditorInput.instance, options); } }); - - const defaultDebugLogFileName = 'agent-debug-log.json'; - const debugLogFilters = [{ name: localize('chatDebugLog.file.label', "Agent Debug Log"), extensions: ['json'] }]; - - registerAction2(class ExportAgentDebugLogAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.exportAgentDebugLog', - title: localize2('chat.exportAgentDebugLog.label', "Export Agent Debug Log..."), - icon: Codicon.desktopDownload, - f1: true, - category: Categories.Developer, - precondition: ChatContextKeys.enabled, - menu: [{ - id: MenuId.EditorTitle, - group: 'navigation', - when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID), - order: 10 - }], - }); - } - - async run(accessor: ServicesAccessor): Promise { - const chatDebugService = accessor.get(IChatDebugService); - const fileDialogService = accessor.get(IFileDialogService); - const fileService = accessor.get(IFileService); - const notificationService = accessor.get(INotificationService); - - const sessionResource = chatDebugService.activeSessionResource; - if (!sessionResource) { - notificationService.notify({ severity: Severity.Info, message: localize('chatDebugLog.noSession', "No active debug session to export. Navigate to a session first.") }); - return; - } - - const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName); - const outputPath = await fileDialogService.showSaveDialog({ defaultUri, filters: debugLogFilters }); - if (!outputPath) { - return; - } - - const data = await chatDebugService.exportLog(sessionResource); - if (!data) { - notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.exportFailed', "Export is not supported by the current provider.") }); - return; - } - - await fileService.writeFile(outputPath, VSBuffer.wrap(data)); - } - }); - - registerAction2(class ImportAgentDebugLogAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.importAgentDebugLog', - title: localize2('chat.importAgentDebugLog.label', "Import Agent Debug Log..."), - icon: Codicon.cloudUpload, - f1: true, - category: Categories.Developer, - precondition: ChatContextKeys.enabled, - menu: [{ - id: MenuId.EditorTitle, - group: 'navigation', - when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID), - order: 11 - }], - }); - } - - async run(accessor: ServicesAccessor): Promise { - const chatDebugService = accessor.get(IChatDebugService); - const editorService = accessor.get(IEditorService); - const fileDialogService = accessor.get(IFileDialogService); - const fileService = accessor.get(IFileService); - const notificationService = accessor.get(INotificationService); - - const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName); - const result = await fileDialogService.showOpenDialog({ - defaultUri, - canSelectFiles: true, - filters: debugLogFilters - }); - if (!result) { - return; - } - - const content = await fileService.readFile(result[0]); - const sessionUri = await chatDebugService.importLog(content.value.buffer); - if (!sessionUri) { - notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.importFailed', "Import is not supported by the current provider.") }); - return; - } - - const options: IChatDebugEditorOptions = { pinned: true, sessionResource: sessionUri, viewHint: 'overview' }; - await editorService.openEditor(ChatDebugEditorInput.instance, options); - } - }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index 867053d97ac..8276d701b2a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -65,6 +65,9 @@ export class ChatDebugEditor extends EditorPane { private readonly sessionModelListener = this._register(new MutableDisposable()); private readonly modelChangeListeners = this._register(new DisposableMap()); + /** Saved session resource so we can restore it after the editor is re-shown. */ + private savedSessionResource: URI | undefined; + /** * Stops the streaming pipeline and clears cached events for the * active session. Called when navigating away from a session or @@ -172,10 +175,7 @@ export class ChatDebugEditor extends EditorPane { this._register(this.chatService.onDidCreateModel(model => { if (this.viewState === ViewState.Home) { - // Auto-navigate to the new session when the debug panel is - // already open on the home view. This avoids the user having to - // wait for the title to resolve and manually clicking the session. - this.navigateToSession(model.sessionResource); + this.homeView?.render(); } // Track title changes per model, disposing the previous listener @@ -307,11 +307,40 @@ export class ChatDebugEditor extends EditorPane { super.setEditorVisible(visible); if (visible) { this.telemetryService.publicLog2<{}, ChatDebugPanelOpenedClassification>('chatDebugPanelOpened'); - // Re-show the current view so it reloads events from scratch, - // ensuring correct ordering and no stale duplicates. - // Navigation from new openEditor() options is handled by - // setOptions → _applyNavigationOptions (fires after this). - this.showView(this.viewState); + // Note: do NOT read this.options here. When the editor becomes + // visible via openEditor(), setEditorVisible fires before + // setOptions, so this.options still contains stale values from + // the previous openEditor() call. Navigation from new options + // is handled entirely by setOptions → _applyNavigationOptions. + // Here we only restore the previous state when the editor is + // re-shown without a new openEditor() call (e.g., tab switch). + if (this.viewState === ViewState.Home) { + const sessionResource = this.chatDebugService.activeSessionResource ?? this.savedSessionResource; + this.savedSessionResource = undefined; + if (sessionResource) { + this.navigateToSession(sessionResource, 'overview'); + } else { + this.showView(ViewState.Home); + } + } else { + // Re-activate the streaming pipeline for the current session, + // restoring the saved session resource if the editor was temporarily hidden. + const sessionResource = this.chatDebugService.activeSessionResource ?? this.savedSessionResource; + this.savedSessionResource = undefined; + if (sessionResource) { + this.chatDebugService.activeSessionResource = sessionResource; + if (!this.chatDebugService.hasInvokedProviders(sessionResource)) { + this.chatDebugService.invokeProviders(sessionResource); + } + } else { + this.showView(ViewState.Home); + } + } + } else { + // Remember the active session so we can restore when re-shown + this.savedSessionResource = this.chatDebugService.activeSessionResource; + // Stop the streaming pipeline when the editor is hidden + this.endActiveSession(); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts index 48e5ce0dced..442c56360b1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts @@ -179,18 +179,13 @@ export function buildFlowGraph(events: readonly IChatDebugEvent[]): FlowNode[] { // For subagent invocations, enrich with description from the // filtered-out completion sibling, or fall back to the event's own field. - let label = getEventLabel(event, effectiveKind); - const sublabel = getEventSublabel(event, effectiveKind); + let sublabel = getEventSublabel(event, effectiveKind); let tooltip = getEventTooltip(event); let description: string | undefined; if (effectiveKind === 'subagentInvocation') { description = getSubagentDescription(event); - // Show "Subagent: " as the label so users can identify - // these nodes and see what task they perform. - label = description - ? localize('subagentWithDesc', "Subagent: {0}", truncateLabel(description, 30)) - : localize('subagentLabel', "Subagent"); if (description) { + sublabel = truncateLabel(description, 30) + (sublabel ? ` \u00b7 ${sublabel}` : ''); // Ensure description appears in tooltip if not already present if (tooltip && !tooltip.includes(description)) { const lines = tooltip.split('\n'); @@ -204,7 +199,7 @@ export function buildFlowGraph(events: readonly IChatDebugEvent[]): FlowNode[] { id: event.id ?? `event-${events.indexOf(event)}`, kind: effectiveKind, category: event.kind === 'generic' ? event.category : undefined, - label, + label: getEventLabel(event, effectiveKind), sublabel, description, tooltip, @@ -529,17 +524,29 @@ function getEventLabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEvent[' const kind = effectiveKind ?? event.kind; switch (kind) { case 'userMessage': - return localize('userLabel', "User Message"); + return localize('userLabel', "User"); case 'modelTurn': return event.kind === 'modelTurn' ? (event.model ?? localize('modelTurnLabel', "Model Turn")) : localize('modelTurnLabel', "Model Turn"); case 'toolCall': - return event.kind === 'toolCall' ? event.toolName : event.kind === 'generic' ? event.name : localize('toolCallLabel', "Tool Call"); + return event.kind === 'toolCall' ? event.toolName : event.kind === 'generic' ? event.name : ''; case 'subagentInvocation': - return event.kind === 'subagentInvocation' ? event.agentName : localize('subagentFallback', "Subagent"); - case 'agentResponse': - return localize('agentResponseLabel', "Agent Response"); + return event.kind === 'subagentInvocation' ? event.agentName : ''; + case 'agentResponse': { + if (event.kind === 'agentResponse') { + return event.message || localize('responseLabel', "Response"); + } + // Remapped generic event — extract model name from parenthesized suffix + // e.g. "Agent response (claude-opus-4.5)" → "claude-opus-4.5" + if (event.kind === 'generic') { + const match = /\(([^)]+)\)\s*$/.exec(event.name); + if (match) { + return match[1]; + } + } + return localize('responseLabel', "Response"); + } case 'generic': - return event.kind === 'generic' ? event.name : localize('genericLabel', "Event"); + return event.kind === 'generic' ? event.name : ''; } } @@ -581,32 +588,30 @@ function getEventSublabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEven } case 'userMessage': case 'agentResponse': { - // Use the message summary as the sublabel. For remapped generic - // events, use the details property. + // For proper typed events, prefer the first section's content + // (which has the actual message text) over the `message` field + // (which is a short summary/name). Fall back to `message` when + // no sections are available. For remapped generic events, use + // the details property. let text: string | undefined; if (event.kind === 'userMessage' || event.kind === 'agentResponse') { - text = event.message; + text = event.sections[0]?.content || event.message; } else if (event.kind === 'generic') { text = event.details; } if (!text) { return undefined; } - // Find the first meaningful line, skipping trivial lines like - // lone brackets/braces that appear when the message is JSON. + // Find the first non-empty line (content may start with newlines) const lines = text.split('\n'); let firstLine = ''; for (const line of lines) { const trimmed = line.trim(); - if (trimmed && trimmed.length > 2) { + if (trimmed) { firstLine = trimmed; break; } } - if (!firstLine) { - // Fall back to the full text collapsed to a single line - firstLine = text.replace(/\s+/g, ' ').trim(); - } if (!firstLine) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts index 403003e62bd..cf9dd80103e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts @@ -159,15 +159,7 @@ function measureNodeWidth(label: string, sublabel?: string): number { } function subgraphHeaderLabel(node: FlowNode): string { - // For subagent nodes, the label already includes the description - // (e.g. "Subagent: Count markdown files"), so don't append it again. - if (node.kind === 'subagentInvocation') { - return node.label; - } - if (node.description && node.description !== node.label) { - return `${node.label}: ${node.description}`; - } - return node.label; + return node.description ? `${node.label}: ${node.description}` : node.label; } function measureSubgraphHeaderWidth(headerLabel: string): number { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts index 0492768b660..f167776e078 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts @@ -85,24 +85,7 @@ export class ChatDebugHomeView extends Disposable { const items: HTMLButtonElement[] = []; for (const sessionResource of sessionResources) { - const rawTitle = this.chatService.getSessionTitle(sessionResource); - let sessionTitle: string; - if (rawTitle && !isUUID(rawTitle)) { - sessionTitle = rawTitle; - } else if (LocalChatSessionUri.isLocalSession(sessionResource)) { - sessionTitle = localize('chatDebug.newSession', "New Chat"); - } else { - // For imported/external sessions, use the stored title if available - const importedTitle = this.chatDebugService.getImportedSessionTitle(sessionResource); - if (importedTitle) { - sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", importedTitle); - } else { - // Fall back to URI segment - const uriLabel = sessionResource.path || sessionResource.fragment || sessionResource.toString(); - const segment = uriLabel.replace(/^\/+/, '').split('/').pop() || uriLabel; - sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", segment); - } - } + const sessionTitle = this.chatService.getSessionTitle(sessionResource) || LocalChatSessionUri.parseLocalSessionId(sessionResource) || sessionResource.toString(); const isActive = activeSessionResource !== undefined && sessionResource.toString() === activeSessionResource.toString(); const item = DOM.append(sessionList, $('button.chat-debug-home-session-item')); @@ -115,20 +98,32 @@ export class ChatDebugHomeView extends Disposable { DOM.append(item, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`)); const titleSpan = DOM.append(item, $('span.chat-debug-home-session-item-title')); - titleSpan.textContent = sessionTitle; - const ariaLabel = isActive - ? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle) - : sessionTitle; - item.setAttribute('aria-label', ariaLabel); + // Show shimmer when the title is still a UUID — the session is + // either not yet loaded or hasn't produced a real title yet. + const isShimmering = isUUID(sessionTitle); + if (isShimmering) { + titleSpan.classList.add('chat-debug-home-session-item-shimmer'); + item.disabled = true; + item.setAttribute('aria-busy', 'true'); + item.setAttribute('aria-label', localize('chatDebug.loadingSession', "Loading session…")); + } else { + titleSpan.textContent = sessionTitle; + const ariaLabel = isActive + ? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle) + : sessionTitle; + item.setAttribute('aria-label', ariaLabel); + } if (isActive) { DOM.append(item, $('span.chat-debug-home-session-badge', undefined, localize('chatDebug.active', "Active"))); } - this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => { - this._onNavigateToSession.fire(sessionResource); - })); - items.push(item); + if (!isShimmering) { + this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => { + this._onNavigateToSession.fire(sessionResource); + })); + items.push(item); + } } // Arrow key navigation between session items diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index 550fa005b81..8cbf8c1a90d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -12,7 +12,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -64,7 +63,6 @@ export class ChatDebugLogsView extends Disposable { private currentDimension: Dimension | undefined; private readonly eventListener = this._register(new MutableDisposable()); private readonly sessionStateDisposable = this._register(new MutableDisposable()); - private readonly refreshScheduler: RunOnceScheduler; private shimmerRow!: HTMLElement; constructor( @@ -77,7 +75,6 @@ export class ChatDebugLogsView extends Disposable { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); - this.refreshScheduler = this._register(new RunOnceScheduler(() => this.refreshList(), 50)); this.container = DOM.append(parent, $('.chat-debug-logs')); DOM.hide(this.container); @@ -386,32 +383,8 @@ export class ChatDebugLogsView extends Disposable { } addEvent(event: IChatDebugEvent): void { - // Binary-insert to maintain chronological order without a full sort. - // Events almost always arrive in order, so the insertion point is - // typically at the end (O(log n) comparison, O(1) splice). - const time = event.created.getTime(); - let lo = 0; - let hi = this.events.length; - while (lo < hi) { - const mid = (lo + hi) >>> 1; - if (this.events[mid].created.getTime() <= time) { - lo = mid + 1; - } else { - hi = mid; - } - } - if (lo === this.events.length) { - this.events.push(event); - } else { - this.events.splice(lo, 0, event); - } - this.scheduleRefresh(); - } - - private scheduleRefresh(): void { - if (!this.refreshScheduler.isScheduled()) { - this.refreshScheduler.schedule(); - } + this.events.push(event); + this.refreshList(); } private loadEvents(): void { @@ -419,7 +392,8 @@ export class ChatDebugLogsView extends Disposable { const addEventDisposable = this.chatDebugService.onDidAddEvent(e => { if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) { - this.addEvent(e); + this.events.push(e); + this.refreshList(); } }); diff --git a/src/vs/workbench/contrib/chat/common/chatDebugService.ts b/src/vs/workbench/contrib/chat/common/chatDebugService.ts index d97213d3f3e..7a6e5721ba1 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugService.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugService.ts @@ -196,34 +196,6 @@ export interface IChatDebugService extends IDisposable { */ resolveEvent(eventId: string): Promise; - /** - /** - * Export the debug log for a session via the registered provider. - */ - exportLog(sessionResource: URI): Promise; - - /** - * Import a previously exported debug log via the registered provider. - * Returns the session URI for the imported data. - */ - importLog(data: Uint8Array): Promise; - - /** - * Returns true if the event was logged by VS Code core - * (not sourced from an external provider). - */ - isCoreEvent(event: IChatDebugEvent): boolean; - - /** - * Store a human-readable title for an imported session. - */ - setImportedSessionTitle(sessionResource: URI, title: string): void; - - /** - * Get the stored title for an imported session, if available. - */ - getImportedSessionTitle(sessionResource: URI): string | undefined; - /** * Fired when debug data is attached to a session. */ @@ -342,6 +314,4 @@ export type IChatDebugResolvedEventContent = IChatDebugEventTextContent | IChatD export interface IChatDebugLogProvider { provideChatDebugLog(sessionResource: URI, token: CancellationToken): Promise; resolveChatDebugLogEvent?(eventId: string, token: CancellationToken): Promise; - provideChatDebugLogExport?(sessionResource: URI, token: CancellationToken): Promise; - resolveChatDebugLogImport?(data: Uint8Array, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts index c80186d968d..9cdd711a311 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts @@ -39,12 +39,6 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic /** Events that were returned by providers (not internally logged). */ private readonly _providerEvents = new WeakSet(); - /** Session URIs created via import, allowed through the invokeProviders guard. */ - private readonly _importedSessions = new ResourceMap(); - - /** Human-readable titles for imported sessions. */ - private readonly _importedSessionTitles = new ResourceMap(); - activeSessionResource: URI | undefined; log(sessionResource: URI, name: string, details?: string, level: ChatDebugLogLevel = ChatDebugLogLevel.Info, options?: { id?: string; category?: string; parentEventId?: string }): void { @@ -141,10 +135,10 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic } async invokeProviders(sessionResource: URI): Promise { - - if (!LocalChatSessionUri.isLocalSession(sessionResource) && !this._importedSessions.has(sessionResource)) { + if (!LocalChatSessionUri.isLocalSession(sessionResource)) { return; } + // Cancel only the previous invocation for THIS session, not others. // Each session has its own pipeline so events from multiple sessions // can be streamed concurrently. @@ -253,51 +247,6 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic return undefined; } - isCoreEvent(event: IChatDebugEvent): boolean { - return !this._providerEvents.has(event); - } - - setImportedSessionTitle(sessionResource: URI, title: string): void { - this._importedSessionTitles.set(sessionResource, title); - } - - getImportedSessionTitle(sessionResource: URI): string | undefined { - return this._importedSessionTitles.get(sessionResource); - } - - async exportLog(sessionResource: URI): Promise { - for (const provider of this._providers) { - if (provider.provideChatDebugLogExport) { - try { - const data = await provider.provideChatDebugLogExport(sessionResource, CancellationToken.None); - if (data !== undefined) { - return data; - } - } catch (err) { - onUnexpectedError(err); - } - } - } - return undefined; - } - - async importLog(data: Uint8Array): Promise { - for (const provider of this._providers) { - if (provider.resolveChatDebugLogImport) { - try { - const sessionUri = await provider.resolveChatDebugLogImport(data, CancellationToken.None); - if (sessionUri !== undefined) { - this._importedSessions.set(sessionUri, true); - return sessionUri; - } - } catch (err) { - onUnexpectedError(err); - } - } - } - return undefined; - } - override dispose(): void { for (const cts of this._invocationCts.values()) { cts.cancel(); diff --git a/src/vscode-dts/vscode.proposed.chatDebug.d.ts b/src/vscode-dts/vscode.proposed.chatDebug.d.ts index 3fe781d29fc..f74f4e7ba11 100644 --- a/src/vscode-dts/vscode.proposed.chatDebug.d.ts +++ b/src/vscode-dts/vscode.proposed.chatDebug.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 3 +// version: 2 declare module 'vscode' { /** @@ -642,37 +642,6 @@ declare module 'vscode' { eventId: string, token: CancellationToken ): ProviderResult; - - /** - * Export the debug log for a chat session as a serialized byte array. - * The extension controls the format (e.g., OTLP JSON with Copilot extensions). - * Core provides the save dialog and writes the returned bytes to disk. - * - * @param sessionResource The resource URI of the chat session to export. - * @param options Export options including core events and session metadata. - * @param token A cancellation token. - * @returns The serialized debug log data, or undefined if export is not available. - */ - provideChatDebugLogExport?( - sessionResource: Uri, - options: ChatDebugLogExportOptions, - token: CancellationToken - ): ProviderResult; - - /** - * Import a previously exported debug log from a serialized byte array. - * Core provides the open dialog and reads the file bytes. - * The extension deserializes the data and returns a session URI that can be - * opened in the debug panel via {@link provideChatDebugLog}. - * - * @param data The serialized debug log data (as returned by {@link provideChatDebugLogExport}). - * @param token A cancellation token. - * @returns The imported session info, or undefined if import failed. - */ - resolveChatDebugLogImport?( - data: Uint8Array, - token: CancellationToken - ): ProviderResult; } export namespace chat { @@ -685,36 +654,4 @@ declare module 'vscode' { */ export function registerChatDebugLogProvider(provider: ChatDebugLogProvider): Disposable; } - - /** - * Options passed to {@link ChatDebugLogProvider.provideChatDebugLogExport}. - */ - export interface ChatDebugLogExportOptions { - /** - * Core-originated debug events (prompt discovery, skill loading, etc.) - * for the session. The extension may include these in the export alongside its own data. - */ - readonly coreEvents: readonly ChatDebugEvent[]; - - /** - * Session title, if available. - * Used to provide a human-readable label in the exported file. - */ - readonly sessionTitle?: string; - } - - /** - * Result of importing a debug log via {@link ChatDebugLogProvider.resolveChatDebugLogImport}. - */ - export interface ChatDebugLogImportResult { - /** - * The session resource URI for the imported session. - */ - readonly uri: Uri; - - /** - * The session title from the imported file, if available. - */ - readonly sessionTitle?: string; - } } From 950ab0704be211929e5f1bc9a0809201edc7ea1c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:51:57 +0100 Subject: [PATCH 408/448] Git - adopt the new package to use copy-on-write for the worktree include files (#299583) * Git - adopt the new package to use copy-on-write for the worktree include files --------- Co-authored-by: deepak1556 --- extensions/git/package-lock.json | 20 +++++++++ extensions/git/package.json | 1 + extensions/git/src/repository.ts | 76 ++++++++++++++++++++------------ 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index b552ce9fa5b..6353cbc2753 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", + "@vscode/fs-copyfile": "2.0.0", "byline": "^5.0.0", "file-type": "16.5.4", "picomatch": "2.3.1", @@ -218,6 +219,19 @@ "vscode": "^1.75.0" } }, + "node_modules/@vscode/fs-copyfile": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vscode/fs-copyfile/-/fs-copyfile-2.0.0.tgz", + "integrity": "sha512-ARb4+9rN905WjJtQ2mSBG/q4pjJkSRun/MkfCeRkk7h/5J8w4vd18NCePFJ/ZucIwXx/7mr9T6nz9Vtt1tk7hg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">=22.6.0" + } + }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -274,6 +288,12 @@ "node": ">=16" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", diff --git a/extensions/git/package.json b/extensions/git/package.json index 1fbac49569f..f0e49309944 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -4346,6 +4346,7 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", + "@vscode/fs-copyfile": "2.0.0", "byline": "^5.0.0", "file-type": "16.5.4", "picomatch": "2.3.1", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index bd6b6a5c7ff..53db3e48495 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { cp } from '@vscode/fs-copyfile'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import * as fs from 'fs'; @@ -1930,59 +1931,76 @@ export class Repository implements Disposable { gitIgnoredFiles.delete(uri.fsPath); } - // Add the folder paths for git ignored files + // Compute the base directory for each glob pattern (the fixed + // prefix before any wildcard characters). This will be used to + // optimize the upward traversal when adding parent directories. + const filePatternBases = new Set(); + for (const pattern of worktreeIncludeFiles) { + const segments = pattern.split(/[\/\\]/); + const fixedSegments: string[] = []; + for (const seg of segments) { + if (/[*?{}[\]]/.test(seg)) { + break; + } + fixedSegments.push(seg); + } + filePatternBases.add(path.join(this.root, ...fixedSegments)); + } + + // Add the folder paths for git ignored files, walking + // up only to the nearest file pattern base directory. const gitIgnoredPaths = new Set(gitIgnoredFiles); for (const filePath of gitIgnoredFiles) { let dir = path.dirname(filePath); - while (dir !== this.root && !gitIgnoredFiles.has(dir)) { + while (dir !== this.root && !gitIgnoredPaths.has(dir)) { gitIgnoredPaths.add(dir); + if (filePatternBases.has(dir)) { + break; + } dir = path.dirname(dir); } } - return gitIgnoredPaths; + // Find minimal set of paths (folders and files) to copy. Keep only topmost + // paths — if a directory is already in the set, all its descendants are + // implicitly included and don't need separate entries. + let lastTopmost: string | undefined; + const pathsToCopy = new Set(); + for (const p of Array.from(gitIgnoredPaths).sort()) { + if (lastTopmost && (p === lastTopmost || p.startsWith(lastTopmost + path.sep))) { + continue; + } + pathsToCopy.add(p); + lastTopmost = p; + } + + return pathsToCopy; } private async _copyWorktreeIncludeFiles(worktreePath: string): Promise { - const gitIgnoredPaths = await this._getWorktreeIncludePaths(); - if (gitIgnoredPaths.size === 0) { + const worktreeIncludePaths = await this._getWorktreeIncludePaths(); + if (worktreeIncludePaths.size === 0) { return; } try { - // Find minimal set of paths (folders and files) to copy. - // The goal is to reduce the number of copy operations - // needed. - const pathsToCopy = new Set(); - for (const filePath of gitIgnoredPaths) { - const relativePath = path.relative(this.root, filePath); - const firstSegment = relativePath.split(path.sep)[0]; - pathsToCopy.add(path.join(this.root, firstSegment)); - } - - const startTime = Date.now(); + const startTime = performance.now(); const limiter = new Limiter(15); - const files = Array.from(pathsToCopy); + const files = Array.from(worktreeIncludePaths); // Copy files - const results = await Promise.allSettled(files.map(sourceFile => - limiter.queue(async () => { + const results = await Promise.allSettled(files.map(sourceFile => { + return limiter.queue(async () => { const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); - await fsPromises.cp(sourceFile, targetFile, { - filter: src => gitIgnoredPaths.has(src), - force: true, - mode: fs.constants.COPYFILE_FICLONE, - recursive: true, - verbatimSymlinks: true - }); - }) - )); + await cp(sourceFile, targetFile, { force: true, recursive: true, verbatimSymlinks: true }); + }); + })); // Log any failed operations const failedOperations = results.filter(r => r.status === 'rejected'); - this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${Date.now() - startTime}ms]`); + this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${(performance.now() - startTime).toFixed(2)}ms]`); if (failedOperations.length > 0) { window.showWarningMessage(l10n.t('Failed to copy {0} folder(s)/file(s) to the worktree.', failedOperations.length)); From 35e0427ee28212f59761e9158008ad4fe73a616d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:18:21 +0100 Subject: [PATCH 409/448] Sessions - expose session base branch information (#300415) * Sessions - expose session base branch information * Pull request feedback --- extensions/git/src/api/api1.ts | 4 ++ extensions/git/src/api/git.d.ts | 2 + .../contrib/changes/browser/changesView.ts | 14 +++- .../browser/mainThreadGitExtensionService.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 6 ++ .../api/common/extHostGitExtensionService.ts | 67 +++++++++++++------ .../contrib/git/common/gitService.ts | 6 ++ 7 files changed, 76 insertions(+), 24 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index d65c75bbf01..b97337596d1 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -355,6 +355,10 @@ export class ApiRepository implements Repository { generateRandomBranchName(): Promise { return this.#repository.generateRandomBranchName(); } + + isBranchProtected(branch?: Branch): boolean { + return this.#repository.isBranchProtected(branch); + } } export class ApiGit implements Git { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 84560e038f4..8a258ba5741 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -328,6 +328,8 @@ export interface Repository { migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; generateRandomBranchName(): Promise; + + isBranchProtected(branch?: Branch): boolean; } export interface RemoteSource { diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 9c30d453409..e81b46def22 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -88,8 +88,8 @@ const enum ChangesVersionMode { Uncommitted = 'uncommitted' } -const changesVersionModeContextKey = new RawContextKey('changesVersionMode', ChangesVersionMode.AllChanges); -const hasUncommittedChangesContextKey = new RawContextKey('hasUncommittedChanges', false); +const changesVersionModeContextKey = new RawContextKey('sessions.changesVersionMode', ChangesVersionMode.AllChanges); +const hasUncommittedChangesContextKey = new RawContextKey('sessions.hasUncommittedChanges', false); // --- List Item @@ -730,6 +730,13 @@ export class ChangesViewPane extends ViewPane { return (repositoryFiles?.length ?? 0) > 0; })); + // Set context key for merge base branch protection + const isMergeBaseBranchProtectedContextKey = new RawContextKey('sessions.isMergeBaseBranchProtected', false); + this.renderDisposables.add(bindContextKey(isMergeBaseBranchProtectedContextKey, this.scopedContextKeyService, r => { + const repository = this.activeSessionRepositoryObs.read(r)?.read(r).value; + return repository?.state.read(r).HEAD?.base?.isProtected === true; + })); + // Set context key for PR state from session metadata const hasOpenPullRequestKey = scopedContextKeyService.createKey('sessions.hasOpenPullRequest', false); this.renderDisposables.add(autorun(reader => { @@ -796,6 +803,9 @@ export class ChangesViewPane extends ViewPane { if (action.id === 'chatEditing.synchronizeChanges') { return { showIcon: true, showLabel: true, isSecondary: true }; } + if (action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR') { + return { showIcon: true, showLabel: true, isSecondary: false }; + } return undefined; } } diff --git a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts index 40005d672df..e9579b0a3ec 100644 --- a/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadGitExtensionService.ts @@ -38,6 +38,7 @@ function toGitRepositoryState(dto: GitRepositoryStateDto | undefined): GitReposi name: dto.HEAD.name, commit: dto.HEAD.commit, remote: dto.HEAD.remote, + base: dto.HEAD.base, upstream: dto.HEAD.upstream, ahead: dto.HEAD.ahead, behind: dto.HEAD.behind, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index eb65735e58a..ecd7167f23c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3652,11 +3652,17 @@ export interface GitBranchDto { readonly commit?: string; readonly type: GitRefTypeDto; readonly remote?: string; + readonly base?: GitBaseRefDto; readonly upstream?: GitUpstreamRefDto; readonly ahead?: number; readonly behind?: number; } +export interface GitBaseRefDto { + readonly name: string; + readonly isProtected: boolean; +} + export interface GitUpstreamRefDto { readonly remote: string; readonly name: string; diff --git a/src/vs/workbench/api/common/extHostGitExtensionService.ts b/src/vs/workbench/api/common/extHostGitExtensionService.ts index 93f8c7ee7da..4d1dd2e2acb 100644 --- a/src/vs/workbench/api/common/extHostGitExtensionService.ts +++ b/src/vs/workbench/api/common/extHostGitExtensionService.ts @@ -11,7 +11,7 @@ import { ExtensionIdentifier } from '../../../platform/extensions/common/extensi import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { IExtHostExtensionService } from './extHostExtensionService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; -import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitDiffChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js'; +import { ExtHostGitExtensionShape, GitBaseRefDto, GitBranchDto, GitChangeDto, GitDiffChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js'; import { ResourceMap } from '../../../base/common/map.js'; const GIT_EXTENSION_ID = 'vscode.git'; @@ -31,6 +31,7 @@ function toGitBranchDto(branch: Branch): GitBranchDto { commit: branch.commit, type: toGitRefTypeDto(branch.type), remote: branch.remote, + base: branch.base, upstream: branch.upstream ? toGitUpstreamRefDto(branch.upstream) : undefined, ahead: branch.ahead, behind: branch.behind, @@ -81,16 +82,6 @@ function toGitChangeDto(change: Change): GitChangeDto { } } -function toGitRepositoryStateDto(state: RepositoryState): GitRepositoryStateDto { - return { - HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined, - mergeChanges: state.mergeChanges.map(toGitChangeDto), - indexChanges: state.indexChanges.map(toGitChangeDto), - workingTreeChanges: state.workingTreeChanges.map(toGitChangeDto), - untrackedChanges: state.untrackedChanges.map(toGitChangeDto), - }; -} - interface DiffChange extends Change { readonly insertions: number; readonly deletions: number; @@ -101,8 +92,10 @@ interface Repository { readonly state: RepositoryState; status(): Promise; + getBranchBase(name: string): Promise; getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise; diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; + isBranchProtected(branch?: Branch): boolean; } interface Change { @@ -122,11 +115,17 @@ interface RepositoryState { } interface Branch extends GitRef { + readonly base?: BaseRef; readonly upstream?: UpstreamRef; readonly ahead?: number; readonly behind?: number; } +interface BaseRef { + readonly name: string; + readonly isProtected: boolean; +} + interface UpstreamRef { readonly remote: string; readonly name: string; @@ -208,11 +207,8 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi const existingHandle = this._repositoryByUri.get(repository.rootUri); if (existingHandle !== undefined) { - return { - handle: existingHandle, - rootUri: repository.rootUri, - state: toGitRepositoryStateDto(repository.state), - }; + const state = await this._getRepositoryState(repository); + return { handle: existingHandle, rootUri: repository.rootUri, state }; } let repositoryState = repository.state; @@ -236,11 +232,8 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi this._proxy.$onDidChangeRepository(handle); })); - return { - handle, - rootUri: repository.rootUri, - state: toGitRepositoryStateDto(repository.state), - }; + const state = await this._getRepositoryState(repository); + return { handle, rootUri: repository.rootUri, state }; } async $getRefs(handle: number, query: GitRefQueryDto, token?: vscode.CancellationToken): Promise { @@ -282,7 +275,37 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi return undefined; } - return toGitRepositoryStateDto(repository.state); + return this._getRepositoryState(repository); + } + + private async _getRepositoryState(repository: Repository): Promise { + const state = repository.state; + + // Base branch + const base = await this._getBranchBase(repository); + + return { + HEAD: state.HEAD ? toGitBranchDto({ ...state.HEAD, base }) : undefined, + mergeChanges: state.mergeChanges.map(toGitChangeDto), + indexChanges: state.indexChanges.map(toGitChangeDto), + workingTreeChanges: state.workingTreeChanges.map(toGitChangeDto), + untrackedChanges: state.untrackedChanges.map(toGitChangeDto), + }; + } + + private async _getBranchBase(repository: Repository): Promise { + const state = repository.state; + if (!state.HEAD?.name) { + return undefined; + } + + const baseBranch = await repository.getBranchBase(state.HEAD.name); + if (!baseBranch?.name) { + return undefined; + } + + const isProtected = repository.isBranchProtected(baseBranch); + return { name: baseBranch.name, isProtected }; } async $diffBetweenWithStats(handle: number, ref1: string, ref2: string, path?: string): Promise { diff --git a/src/vs/workbench/contrib/git/common/gitService.ts b/src/vs/workbench/contrib/git/common/gitService.ts index 5f1b6284838..ce605402b6f 100644 --- a/src/vs/workbench/contrib/git/common/gitService.ts +++ b/src/vs/workbench/contrib/git/common/gitService.ts @@ -49,11 +49,17 @@ export interface GitRepositoryState { } export interface GitBranch extends GitRef { + readonly base?: GitBaseRef; readonly upstream?: GitUpstreamRef; readonly ahead?: number; readonly behind?: number; } +export interface GitBaseRef { + readonly name: string; + readonly isProtected: boolean; +} + export interface GitUpstreamRef { readonly remote: string; readonly name: string; From cba3c7aac7d24aad778917c26756092cdfc0ad47 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:52:52 +0100 Subject: [PATCH 410/448] Add API proposal version check (#300392) * Add API proposal version check * CCR feedback * Test with version bump * Comment improvements * Undo test version bump * More comment improvement --- .../workflows/api-proposal-version-check.yml | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 .github/workflows/api-proposal-version-check.yml diff --git a/.github/workflows/api-proposal-version-check.yml b/.github/workflows/api-proposal-version-check.yml new file mode 100644 index 00000000000..1edfc19028f --- /dev/null +++ b/.github/workflows/api-proposal-version-check.yml @@ -0,0 +1,245 @@ +name: API Proposal Version Check + +on: + pull_request: + branches: + - main + - 'release/*' + paths: + - 'src/vscode-dts/vscode.proposed.*.d.ts' + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + actions: write + checks: write + +concurrency: + group: api-proposal-${{ github.event.pull_request.number || github.event.issue.number }} + cancel-in-progress: true + +jobs: + check-version-changes: + name: Check API Proposal Version Changes + # Run on PR events, or on issue_comment if it's on a PR and contains the override command + if: | + github.event_name == 'pull_request' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/api-proposal-change-required')) + runs-on: ubuntu-latest + steps: + - name: Get PR info + id: pr_info + uses: actions/github-script@v7 + with: + script: | + let prNumber, headSha, baseSha; + + if (context.eventName === 'pull_request') { + prNumber = context.payload.pull_request.number; + headSha = context.payload.pull_request.head.sha; + baseSha = context.payload.pull_request.base.sha; + } else { + // issue_comment event - need to fetch PR details + prNumber = context.payload.issue.number; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + headSha = pr.head.sha; + baseSha = pr.base.sha; + } + + core.setOutput('number', prNumber); + core.setOutput('head_sha', headSha); + core.setOutput('base_sha', baseSha); + + - name: Check for override comment + id: check_override + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.pr_info.outputs.number }}; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + // Only accept overrides from trusted users (repo members/collaborators) + const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; + const overrideComment = comments.find(comment => + comment.body.includes('/api-proposal-change-required') && + trustedAssociations.includes(comment.author_association) + ); + + if (overrideComment) { + console.log(`Override comment found by ${overrideComment.user.login} (${overrideComment.author_association})`); + core.setOutput('override_found', 'true'); + core.setOutput('override_user', overrideComment.user.login); + } else { + // Check if there's an override from an untrusted user + const untrustedOverride = comments.find(comment => + comment.body.includes('/api-proposal-change-required') && + !trustedAssociations.includes(comment.author_association) + ); + if (untrustedOverride) { + console.log(`Override comment by ${untrustedOverride.user.login} ignored (${untrustedOverride.author_association} is not trusted)`); + } + console.log('No valid override comment found'); + core.setOutput('override_found', 'false'); + } + + # If triggered by the override comment, create a successful check run on the PR head + - name: Pass on override comment + if: steps.check_override.outputs.override_found == 'true' + uses: actions/github-script@v7 + with: + script: | + const headSha = '${{ steps.pr_info.outputs.head_sha }}'; + console.log(`Override comment found by ${{ steps.check_override.outputs.override_user }}`); + console.log('API proposal version change has been acknowledged.'); + + // Create a successful check run on the PR head SHA so the status updates + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Check API Proposal Version Changes', + head_sha: headSha, + status: 'completed', + conclusion: 'success', + output: { + title: 'API Proposal Version Change Acknowledged', + summary: `Override approved by @${{ steps.check_override.outputs.override_user }}` + } + }); + + # Only continue checking if no override found + - name: Checkout repository + if: steps.check_override.outputs.override_found != 'true' + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for version changes + if: steps.check_override.outputs.override_found != 'true' + id: version_check + env: + HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }} + BASE_SHA: ${{ steps.pr_info.outputs.base_sha }} + run: | + set -e + + # Use merge-base to get accurate diff of what the PR actually changes + MERGE_BASE=$(git merge-base "$BASE_SHA" "$HEAD_SHA") + echo "Merge base: $MERGE_BASE" + + # Get the list of changed proposed API files (diff against merge-base) + CHANGED_FILES=$(git diff --name-only "$MERGE_BASE" "$HEAD_SHA" -- 'src/vscode-dts/vscode.proposed.*.d.ts' || true) + + if [ -z "$CHANGED_FILES" ]; then + echo "No proposed API files changed" + echo "version_changed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Changed proposed API files:" + echo "$CHANGED_FILES" + + VERSION_CHANGED="false" + CHANGED_LIST="" + + for FILE in $CHANGED_FILES; do + # Check if file exists in head + if ! git cat-file -e "$HEAD_SHA:$FILE" 2>/dev/null; then + echo "File $FILE was deleted, skipping version check" + continue + fi + + # Get version from head (current PR) + HEAD_VERSION=$(git show "$HEAD_SHA:$FILE" | grep -E '^// version: [0-9]+' | sed 's/.*version: //' || echo "") + + # Get version from merge-base (what the PR is based on) + BASE_VERSION=$(git show "$MERGE_BASE:$FILE" 2>/dev/null | grep -E '^// version: [0-9]+' | sed 's/.*version: //' || echo "") + + echo "File: $FILE" + echo " Base version: ${BASE_VERSION:-'(none)'}" + echo " Head version: ${HEAD_VERSION:-'(none)'}" + + # Check if version was added or changed + if [ -n "$HEAD_VERSION" ] && [ "$HEAD_VERSION" != "$BASE_VERSION" ]; then + echo " -> Version changed!" + VERSION_CHANGED="true" + FILENAME=$(basename "$FILE") + if [ -n "$CHANGED_LIST" ]; then + CHANGED_LIST="$CHANGED_LIST, $FILENAME" + else + CHANGED_LIST="$FILENAME" + fi + fi + done + + echo "version_changed=$VERSION_CHANGED" >> $GITHUB_OUTPUT + echo "changed_files=$CHANGED_LIST" >> $GITHUB_OUTPUT + + - name: Post warning comment + if: steps.check_override.outputs.override_found != 'true' && steps.version_check.outputs.version_changed == 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.pr_info.outputs.number }}; + const changedFiles = '${{ steps.version_check.outputs.changed_files }}'; + + // Check if we already posted a warning comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const marker = ''; + const existingComment = comments.find(comment => + comment.body.includes(marker) + ); + + const body = `${marker} + ## ⚠️ API Proposal Version Change Detected + + The following proposed API files have version changes: **${changedFiles}** + + API proposal version changes should only be used when maintaining compatibility is not possible. Consider keeping the version as is and maintaining backward compatibility. + + **Any version changes must be adopted by the consuming extensions before the next insiders for the extension to work.** + + --- + + If the version change is required, comment \`/api-proposal-change-required\` to unblock this check and acknowledge that you will update any critical consuming extensions (Copilot Chat).`; + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + console.log('Updated existing warning comment'); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); + console.log('Posted new warning comment'); + } + + - name: Fail if version changed without override + if: steps.check_override.outputs.override_found != 'true' && steps.version_check.outputs.version_changed == 'true' + run: | + echo "::error::API proposal version changed in: ${{ steps.version_check.outputs.changed_files }}" + echo "To unblock, comment '/api-proposal-change-required' on the PR." + exit 1 From 83e4102717528f7b96a59fa6f73043677be85790 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:51:18 +0100 Subject: [PATCH 411/448] Sessions - tweak toolbar actions (#300434) --- src/vs/sessions/contrib/changes/browser/changesView.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index e81b46def22..e44ffa127e1 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -806,6 +806,12 @@ export class ChangesViewPane extends ViewPane { if (action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR') { return { showIcon: true, showLabel: true, isSecondary: false }; } + if (action.id === 'github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR') { + return { showIcon: true, showLabel: false, isSecondary: true }; + } + if (action.id === 'github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge') { + return { showIcon: true, showLabel: true, isSecondary: false }; + } return undefined; } } From f316d9db7e724819854a44333591f040555b1f9c Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 10 Mar 2026 14:11:45 +0100 Subject: [PATCH 412/448] ci and PR review comments in agent feedback --- .../browser/agentFeedbackEditorActions.ts | 14 +- .../browser/agentFeedbackEditorOverlay.ts | 1 + .../agentFeedbackEditorWidgetContribution.ts | 74 ++- .../browser/agentFeedbackService.ts | 87 ++- .../media/agentFeedbackEditorWidget.css | 13 + .../browser/sessionEditorComments.ts | 21 +- .../agentFeedbackEditorWidget.fixture.ts | 60 +- .../browser/sessionEditorComments.test.ts | 48 +- .../contrib/changes/browser/changesView.ts | 80 ++- .../contrib/changes/browser/ciStatusWidget.ts | 519 ++++++++++++++++++ .../changes/browser/media/ciStatusWidget.css | 188 +++++++ .../browser/codeReview.contributions.ts | 54 +- .../codeReview/browser/codeReviewService.ts | 182 +++++- .../test/browser/codeReviewService.test.ts | 12 +- .../browser/fetchers/githubPRCIFetcher.ts | 201 +++++++ .../browser/fetchers/githubPRFetcher.ts | 362 ++++++++++++ .../fetchers/githubRepositoryFetcher.ts | 42 ++ .../github/browser/github.contribution.ts | 9 + .../contrib/github/browser/githubApiClient.ts | 148 +++++ .../contrib/github/browser/githubService.ts | 100 ++++ .../models/githubPullRequestCIModel.ts | 90 +++ .../browser/models/githubPullRequestModel.ts | 142 +++++ .../browser/models/githubRepositoryModel.ts | 40 ++ .../sessions/contrib/github/common/types.ts | 147 +++++ .../test/browser/githubFetchers.test.ts | 446 +++++++++++++++ .../github/test/browser/githubModels.test.ts | 279 ++++++++++ .../github/test/browser/githubService.test.ts | 133 +++++ .../browser/sessionsManagementService.ts | 116 ++++ src/vs/sessions/sessions.desktop.main.ts | 1 + 29 files changed, 3544 insertions(+), 65 deletions(-) create mode 100644 src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts create mode 100644 src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css create mode 100644 src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts create mode 100644 src/vs/sessions/contrib/github/browser/fetchers/githubPRFetcher.ts create mode 100644 src/vs/sessions/contrib/github/browser/fetchers/githubRepositoryFetcher.ts create mode 100644 src/vs/sessions/contrib/github/browser/github.contribution.ts create mode 100644 src/vs/sessions/contrib/github/browser/githubApiClient.ts create mode 100644 src/vs/sessions/contrib/github/browser/githubService.ts create mode 100644 src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts create mode 100644 src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts create mode 100644 src/vs/sessions/contrib/github/browser/models/githubRepositoryModel.ts create mode 100644 src/vs/sessions/contrib/github/common/types.ts create mode 100644 src/vs/sessions/contrib/github/test/browser/githubFetchers.test.ts create mode 100644 src/vs/sessions/contrib/github/test/browser/githubModels.test.ts create mode 100644 src/vs/sessions/contrib/github/test/browser/githubService.test.ts diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts index eb9a50f5b97..c16e39d3bde 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorActions.ts @@ -66,6 +66,7 @@ abstract class AgentFeedbackEditorAction extends Action2 { sessionResource, agentFeedbackService.getFeedback(sessionResource), codeReviewService.getReviewState(sessionResource).get(), + codeReviewService.getPRReviewState(sessionResource).get(), ); if (comments.length > 0) { return this.runWithSession(accessor, sessionResource); @@ -148,11 +149,11 @@ class NavigateFeedbackAction extends AgentFeedbackEditorAction { override async runWithSession(accessor: ServicesAccessor, sessionResource: URI): Promise { const agentFeedbackService = accessor.get(IAgentFeedbackService); const codeReviewService = accessor.get(ICodeReviewService); - const editorService = accessor.get(IEditorService); const comments = getSessionEditorComments( sessionResource, agentFeedbackService.getFeedback(sessionResource), codeReviewService.getReviewState(sessionResource).get(), + codeReviewService.getPRReviewState(sessionResource).get(), ); const comment = agentFeedbackService.getNextNavigableItem(sessionResource, comments, this._next); @@ -160,16 +161,7 @@ class NavigateFeedbackAction extends AgentFeedbackEditorAction { return; } - await editorService.openEditor({ - resource: comment.resourceUri, - options: { - preserveFocus: false, - revealIfVisible: true, - selection: { startLineNumber: comment.range.startLineNumber, startColumn: comment.range.startColumn }, // place the cursor but not selection - } - }); - - agentFeedbackService.setNavigationAnchor(sessionResource, comment.id); + await agentFeedbackService.revealSessionComment(sessionResource, comment.id, comment.resourceUri, comment.range); } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts index 60990b03926..7780c40acab 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts @@ -198,6 +198,7 @@ class AgentFeedbackOverlayController { sessionResource, agentFeedbackService.getFeedback(sessionResource), codeReviewService.getReviewState(sessionResource).read(r), + codeReviewService.getPRReviewState(sessionResource).read(r), ); if (comments.length > 0) { navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource, comments); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index ad4b69371fc..09ccece7717 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -25,10 +25,11 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import * as nls from '../../../../nls.js'; import { IAgentFeedbackService } from './agentFeedbackService.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { getSessionForResource } from './agentFeedbackEditorUtils.js'; -import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; -import { getResourceEditorComments, getSessionEditorComments, groupNearbySessionEditorComments, ISessionEditorComment, SessionEditorCommentSource, toSessionEditorCommentId } from './sessionEditorComments.js'; +import { ICodeReviewService, IPRReviewState } from '../../codeReview/browser/codeReviewService.js'; +import { getSessionEditorComments, groupNearbySessionEditorComments, ISessionEditorComment, SessionEditorCommentSource, toSessionEditorCommentId } from './sessionEditorComments.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; @@ -239,6 +240,10 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid } private _getTypeLabel(comment: ISessionEditorComment): string { + if (comment.source === SessionEditorCommentSource.PRReview) { + return nls.localize('prReviewComment', "PR Review"); + } + if (comment.source === SessionEditorCommentSource.CodeReview) { return comment.suggestion ? nls.localize('reviewSuggestion', "Review Suggestion") @@ -276,6 +281,10 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid } private _removeComment(comment: ISessionEditorComment): void { + if (comment.source === SessionEditorCommentSource.PRReview) { + this._codeReviewService.resolvePRReviewThread(this._sessionResource!, comment.sourceId); + return; + } if (comment.source === SessionEditorCommentSource.CodeReview) { this._codeReviewService.removeComment(this._sessionResource, comment.sourceId); return; @@ -522,7 +531,10 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito return; } - this._rebuildWidgets(this._codeReviewService.getReviewState(this._sessionResource).read(reader)); + this._rebuildWidgets( + this._codeReviewService.getReviewState(this._sessionResource).read(reader), + this._codeReviewService.getPRReviewState(this._sessionResource).read(reader), + ); this._handleNavigation(); })); } @@ -536,7 +548,10 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._agentSessionsService); } - private _rebuildWidgets(reviewState = this._sessionResource ? this._codeReviewService.getReviewState(this._sessionResource).get() : undefined): void { + private _rebuildWidgets( + reviewState = this._sessionResource ? this._codeReviewService.getReviewState(this._sessionResource).get() : undefined, + prReviewState: IPRReviewState | undefined = this._sessionResource ? this._codeReviewService.getPRReviewState(this._sessionResource).get() : undefined, + ): void { this._clearWidgets(); if (!this._sessionResource || !reviewState) { @@ -552,8 +567,9 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito this._sessionResource, this._agentFeedbackService.getFeedback(this._sessionResource), reviewState, + prReviewState, ); - const fileComments = getResourceEditorComments(model.uri, comments); + const fileComments = this._getCommentsForModel(model.uri, comments); if (fileComments.length === 0) { return; } @@ -568,6 +584,51 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito } } + private _getCommentsForModel(resourceUri: URI, comments: readonly ISessionEditorComment[]): readonly ISessionEditorComment[] { + const change = this._getSessionChangeForResource(resourceUri); + if (!change) { + return comments.filter(comment => isEqual(comment.resourceUri, resourceUri)); + } + + if (!this._isCurrentOrModifiedResource(change, resourceUri)) { + return []; + } + + return comments.filter(comment => comment.resourceUri.fsPath === resourceUri.fsPath); + } + + private _getSessionChangeForResource(resourceUri: URI): IChatSessionFileChange | IChatSessionFileChange2 | undefined { + if (!this._sessionResource) { + return undefined; + } + + const changes = this._agentSessionsService.getSession(this._sessionResource)?.changes; + if (!(changes instanceof Array)) { + return undefined; + } + + return changes.find(change => this._changeMatchesFsPath(change, resourceUri)); + } + + private _changeMatchesFsPath(change: IChatSessionFileChange | IChatSessionFileChange2, resourceUri: URI): boolean { + if (isIChatSessionFileChange2(change)) { + return change.uri.fsPath === resourceUri.fsPath + || change.modifiedUri?.fsPath === resourceUri.fsPath + || change.originalUri?.fsPath === resourceUri.fsPath; + } + + return change.modifiedUri.fsPath === resourceUri.fsPath + || change.originalUri?.fsPath === resourceUri.fsPath; + } + + private _isCurrentOrModifiedResource(change: IChatSessionFileChange | IChatSessionFileChange2, resourceUri: URI): boolean { + if (isIChatSessionFileChange2(change)) { + return isEqual(change.uri, resourceUri) || (change.modifiedUri ? isEqual(change.modifiedUri, resourceUri) : false); + } + + return isEqual(change.modifiedUri, resourceUri); + } + private _handleNavigation(): void { if (!this._sessionResource) { return; @@ -582,6 +643,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito this._sessionResource, this._agentFeedbackService.getFeedback(this._sessionResource), this._codeReviewService.getReviewState(this._sessionResource).get(), + this._codeReviewService.getPRReviewState(this._sessionResource).get(), ); const bearing = this._agentFeedbackService.getNavigationBearing(this._sessionResource, comments); if (bearing.activeIdx < 0) { @@ -593,7 +655,7 @@ class AgentFeedbackEditorWidgetContribution extends Disposable implements IEdito return; } - if (!isEqual(activeFeedback.resourceUri, model.uri)) { + if (this._getCommentsForModel(model.uri, [activeFeedback]).length === 0) { for (const widget of this._widgets) { widget.collapse(); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index d4f30fd99e5..c92a4ab67c4 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -11,9 +11,10 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { generateUuid } from '../../../../base/common/uuid.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { agentSessionContainsResource, editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -281,17 +282,85 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe } async revealSessionComment(sessionResource: URI, commentId: string, resourceUri: URI, range: IRange): Promise { - await this._editorService.openEditor({ - resource: resourceUri, - options: { - preserveFocus: false, - revealIfVisible: true, - selection: { startLineNumber: range.startLineNumber, startColumn: range.startColumn }, - } - }); + const selection = { startLineNumber: range.startLineNumber, startColumn: range.startColumn }; + const sessionChange = this._getSessionChange(resourceUri, this._agentSessionsService.getSession(sessionResource)?.changes); + + if (sessionChange?.isDeletion && sessionChange.originalUri) { + await this._editorService.openEditor({ + resource: sessionChange.originalUri, + options: { + modal: {}, + preserveFocus: false, + revealIfVisible: true, + selection, + } + }, MODAL_GROUP); + } else if (sessionChange?.originalUri) { + await this._editorService.openEditor({ + original: { resource: sessionChange.originalUri }, + modified: { resource: sessionChange.modifiedUri }, + options: { + modal: {}, + preserveFocus: false, + revealIfVisible: true, + selection, + } + }, MODAL_GROUP); + } else { + await this._editorService.openEditor({ + resource: sessionChange?.modifiedUri ?? resourceUri, + options: { + modal: {}, + preserveFocus: false, + revealIfVisible: true, + selection, + } + }, MODAL_GROUP); + } + this.setNavigationAnchor(sessionResource, commentId); } + private _getSessionChange(resourceUri: URI, changes: readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[] | { + readonly files: number; + readonly insertions: number; + readonly deletions: number; + } | undefined): { originalUri?: URI; modifiedUri: URI; isDeletion: boolean } | undefined { + if (!(changes instanceof Array)) { + return undefined; + } + + const matchingChange = changes.find(change => this._changeContainsResource(change, resourceUri)); + if (!matchingChange) { + return undefined; + } + + if (isIChatSessionFileChange2(matchingChange)) { + return { + originalUri: matchingChange.originalUri, + modifiedUri: matchingChange.modifiedUri ?? matchingChange.uri, + isDeletion: matchingChange.modifiedUri === undefined, + }; + } + + return { + originalUri: matchingChange.originalUri, + modifiedUri: matchingChange.modifiedUri, + isDeletion: false, + }; + } + + private _changeContainsResource(change: IChatSessionFileChange | IChatSessionFileChange2, resourceUri: URI): boolean { + if (isIChatSessionFileChange2(change)) { + return change.uri.fsPath === resourceUri.fsPath + || change.originalUri?.fsPath === resourceUri.fsPath + || change.modifiedUri?.fsPath === resourceUri.fsPath; + } + + return change.modifiedUri.fsPath === resourceUri.fsPath + || change.originalUri?.fsPath === resourceUri.fsPath; + } + getNextFeedback(sessionResource: URI, next: boolean): IAgentFeedback | undefined { return this.getNextNavigableItem(sessionResource, this.getFeedback(sessionResource), next); } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css index 608af1249b1..938da413b02 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -154,6 +154,10 @@ box-shadow: inset 2px 0 0 var(--vscode-editorWarning-foreground); } +.agent-feedback-widget-item-prReview { + box-shadow: inset 2px 0 0 var(--vscode-editorInfo-foreground); +} + .agent-feedback-widget-item-header { display: flex; align-items: flex-start; @@ -200,6 +204,11 @@ color: var(--vscode-editorWarning-foreground); } +.agent-feedback-widget-item-prReview .agent-feedback-widget-item-type { + background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 22%, transparent); + color: var(--vscode-editorInfo-foreground); +} + /* Line info */ .agent-feedback-widget-line-info { font-size: 10px; @@ -240,6 +249,10 @@ background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 10%, transparent); } +.agent-feedback-widget-item-prReview .agent-feedback-widget-suggestion { + background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 10%, transparent); +} + .agent-feedback-widget-suggestion-title, .agent-feedback-widget-suggestion-range { font-size: 10px; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts b/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts index f5e740cbd98..ef756423d42 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/sessionEditorComments.ts @@ -6,11 +6,12 @@ import { IRange, Range } from '../../../../editor/common/core/range.js'; import { URI } from '../../../../base/common/uri.js'; import { IAgentFeedback } from './agentFeedbackService.js'; -import { CodeReviewStateKind, ICodeReviewComment, ICodeReviewState, ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js'; +import { CodeReviewStateKind, ICodeReviewComment, ICodeReviewState, ICodeReviewSuggestion, IPRReviewComment, IPRReviewState, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js'; export const enum SessionEditorCommentSource { AgentFeedback = 'agentFeedback', CodeReview = 'codeReview', + PRReview = 'prReview', } export interface ISessionEditorComment { @@ -30,10 +31,15 @@ export function getCodeReviewComments(reviewState: ICodeReviewState): readonly I return reviewState.kind === CodeReviewStateKind.Result ? reviewState.comments : []; } +export function getPRReviewComments(prReviewState: IPRReviewState | undefined): readonly IPRReviewComment[] { + return prReviewState?.kind === PRReviewStateKind.Loaded ? prReviewState.comments : []; +} + export function getSessionEditorComments( sessionResource: URI, agentFeedbackItems: readonly IAgentFeedback[], reviewState: ICodeReviewState, + prReviewState?: IPRReviewState, ): readonly ISessionEditorComment[] { const comments: ISessionEditorComment[] = []; @@ -66,6 +72,19 @@ export function getSessionEditorComments( }); } + for (const item of getPRReviewComments(prReviewState)) { + comments.push({ + id: toSessionEditorCommentId(SessionEditorCommentSource.PRReview, item.id), + sourceId: item.id, + source: SessionEditorCommentSource.PRReview, + sessionResource, + resourceUri: item.uri, + range: item.range, + text: item.body, + canConvertToAgentFeedback: true, + }); + } + comments.sort(compareSessionEditorComments); return comments; } diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts index b8b75bec923..3beeee9243d 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackEditorWidget.fixture.ts @@ -16,7 +16,7 @@ import { TokenizationRegistry } from '../../../../../editor/common/languages.js' import { IAgentFeedback, IAgentFeedbackService } from '../../browser/agentFeedbackService.js'; import { AgentFeedbackEditorWidget } from '../../browser/agentFeedbackEditorWidgetContribution.js'; import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; -import { CodeReviewStateKind, ICodeReviewService, ICodeReviewState, ICodeReviewSuggestion } from '../../../codeReview/browser/codeReviewService.js'; +import { CodeReviewStateKind, ICodeReviewService, ICodeReviewState, ICodeReviewSuggestion, IPRReviewState, PRReviewStateKind } from '../../../codeReview/browser/codeReviewService.js'; import { ISessionEditorComment, SessionEditorCommentSource } from '../../browser/sessionEditorComments.js'; const sessionResource = URI.parse('vscode-agent-session://fixture/session-1'); @@ -92,6 +92,19 @@ function createReviewComment(id: string, text: string, startLineNumber: number, }; } +function createPRReviewComment(id: string, text: string, startLineNumber: number, endLineNumber: number = startLineNumber): ISessionEditorComment { + return { + id: `prReview:${id}`, + sourceId: id, + source: SessionEditorCommentSource.PRReview, + text, + resourceUri: fileResource, + range: createRange(startLineNumber, endLineNumber), + sessionResource, + canConvertToAgentFeedback: true, + }; +} + function createMockAgentFeedbackService(): IAgentFeedbackService { return new class extends mock() { override readonly onDidChangeFeedback = Event.None; @@ -150,6 +163,14 @@ function createMockCodeReviewService(): ICodeReviewService { override removeComment(): void { } override dismissReview(): void { } + + private readonly _prState = observableValue('fixture.prReviewState', { kind: PRReviewStateKind.None }); + + override getPRReviewState() { + return this._prState; + } + + override async resolvePRReviewThread(): Promise { } }(); } @@ -272,6 +293,18 @@ const suggestionMix = [ createFeedbackComment('f-3', 'Keep the helper name aligned with the domain concept.', 9), ]; +const prReviewOnly = [ + createPRReviewComment('pr-1', 'This variable should be renamed to match our naming conventions.', 2), + createPRReviewComment('pr-2', 'Please add error handling for the edge case when second is zero.', 7, 8), +]; + +const allSourcesMixed = [ + createFeedbackComment('f-1', 'Prefer a clearer variable name on this line.', 2), + createPRReviewComment('pr-1', 'Our style guide says to use descriptive names here.', 3), + createReviewComment('r-1', 'This should be extracted into a helper.', 6), + createPRReviewComment('pr-2', 'This logic duplicates what we have in utils.ts — consider reusing.', 8, 9), +]; + export default defineThemedFixtureGroup({ path: 'sessions/agentFeedback/' }, { CollapsedSingleComment: defineComponentFixture({ labels: { kind: 'screenshot' }, @@ -345,6 +378,31 @@ export default defineThemedFixtureGroup({ path: 'sessions/agentFeedback/' }, { }), }), + ExpandedPRReviewOnly: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: prReviewOnly, + expanded: true, + }), + }), + + ExpandedAllSourcesMixed: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: allSourcesMixed, + expanded: true, + }), + }), + + ExpandedFocusedPRReview: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: context => renderWidget(context, { + commentItems: allSourcesMixed, + expanded: true, + focusedCommentId: 'prReview:pr-2', + }), + }), + HiddenWidget: defineComponentFixture({ labels: { kind: 'screenshot' }, render: context => renderWidget(context, { diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts index dec6e0ce3aa..f7bb4f0aa5b 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/sessionEditorComments.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { URI } from '../../../../../base/common/uri.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { CodeReviewStateKind, ICodeReviewState } from '../../../codeReview/browser/codeReviewService.js'; +import { CodeReviewStateKind, ICodeReviewState, IPRReviewState, PRReviewStateKind } from '../../../codeReview/browser/codeReviewService.js'; import { getResourceEditorComments, getSessionEditorComments, groupNearbySessionEditorComments, hasAgentFeedbackComments, SessionEditorCommentSource } from '../../browser/sessionEditorComments.js'; type ICodeReviewResultState = Extract; @@ -95,4 +95,50 @@ suite('SessionEditorComments', () => { assert.deepStrictEqual(getResourceEditorComments(fileA, comments).map(comment => comment.source), [SessionEditorCommentSource.AgentFeedback]); assert.deepStrictEqual(getResourceEditorComments(fileB, comments).map(comment => comment.source), [SessionEditorCommentSource.CodeReview]); }); + + test('includes PR review comments when prReviewState is loaded', () => { + const prState: IPRReviewState = { + kind: PRReviewStateKind.Loaded, + comments: [ + { id: 'pr-thread-1', uri: fileA, range: new Range(5, 1, 5, 1), body: 'Please fix this', author: 'reviewer' }, + { id: 'pr-thread-2', uri: fileB, range: new Range(1, 1, 1, 1), body: 'Looks wrong', author: 'reviewer' }, + ], + }; + + const comments = getSessionEditorComments(session, [], reviewState([]), prState); + assert.strictEqual(comments.length, 2); + assert.deepStrictEqual(comments.map(c => `${c.resourceUri.path}:${c.range.startLineNumber}:${c.source}`), [ + '/a.ts:5:prReview', + '/b.ts:1:prReview', + ]); + assert.strictEqual(comments[0].canConvertToAgentFeedback, true); + }); + + test('merges PR review comments with other sources sorted correctly', () => { + const prState: IPRReviewState = { + kind: PRReviewStateKind.Loaded, + comments: [ + { id: 'pr-thread-1', uri: fileA, range: new Range(7, 1, 7, 1), body: 'PR comment', author: 'reviewer' }, + ], + }; + + const comments = getSessionEditorComments(session, [ + { id: 'feedback-a', text: 'feedback a', resourceUri: fileA, range: new Range(3, 1, 3, 1), sessionResource: session }, + ], reviewState([ + { id: 'review-a', uri: fileA, range: new Range(10, 1, 10, 1), body: 'review', kind: 'issue', severity: 'warning' }, + ]), prState); + + assert.strictEqual(comments.length, 3); + assert.deepStrictEqual(comments.map(c => `${c.range.startLineNumber}:${c.source}`), [ + '3:agentFeedback', + '7:prReview', + '10:codeReview', + ]); + }); + + test('omits PR review comments when prReviewState is not loaded', () => { + const prState: IPRReviewState = { kind: PRReviewStateKind.None }; + const comments = getSessionEditorComments(session, [], reviewState([]), prState); + assert.strictEqual(comments.length, 0); + }); }); diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 9c30d453409..2a2499f7f11 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -60,8 +60,10 @@ import { IExtensionService } from '../../../../workbench/services/extensions/com import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; -import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../codeReview/browser/codeReviewService.js'; +import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js'; import { IGitRepository, IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; +import { IGitHubService } from '../../github/browser/githubService.js'; +import { CIStatusWidget } from './ciStatusWidget.js'; const $ = dom.$; @@ -222,6 +224,7 @@ export class ChangesViewPane extends ViewPane { private actionsContainer: HTMLElement | undefined; private tree: WorkbenchCompressibleObjectTree | undefined; + private ciStatusWidget: CIStatusWidget | undefined; private readonly renderDisposables = this._register(new DisposableStore()); @@ -289,6 +292,7 @@ export class ChangesViewPane extends ViewPane { @IStorageService private readonly storageService: IStorageService, @ICodeReviewService private readonly codeReviewService: ICodeReviewService, @IGitService private readonly gitService: IGitService, + @IGitHubService private readonly gitHubService: IGitHubService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -456,6 +460,9 @@ export class ChangesViewPane extends ViewPane { // List container this.listContainer = dom.append(this.contentContainer, $('.chat-editing-session-list')); + // CI Status widget beneath the card + this.ciStatusWidget = this._register(this.instantiationService.createInstance(CIStatusWidget, this.bodyContainer)); + this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { this.onVisible(); @@ -547,21 +554,33 @@ export class ChangesViewPane extends ViewPane { const sessionResource = activeSessionResource.read(reader); const sessionChanges = [...sessionFileChangesObs.read(reader)]; - if (!sessionResource || sessionChanges.length === 0) { + if (!sessionResource) { return new Map(); } + const result = new Map(); + const prReviewState = this.codeReviewService.getPRReviewState(sessionResource).read(reader); + if (prReviewState.kind === PRReviewStateKind.Loaded) { + for (const comment of prReviewState.comments) { + const uriKey = comment.uri.fsPath; + result.set(uriKey, (result.get(uriKey) ?? 0) + 1); + } + } + + if (sessionChanges.length === 0) { + return result; + } + const reviewFiles = getCodeReviewFilesFromSessionChanges(sessionChanges as readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]); const reviewVersion = getCodeReviewVersion(reviewFiles); const reviewState = this.codeReviewService.getReviewState(sessionResource).read(reader); if (reviewState.kind !== CodeReviewStateKind.Result || reviewState.version !== reviewVersion) { - return new Map(); + return result; } - const result = new Map(); for (const comment of reviewState.comments) { - const uriKey = comment.uri.toString(); + const uriKey = comment.uri.fsPath; result.set(uriKey, (result.get(uriKey) ?? 0) + 1); } @@ -587,7 +606,7 @@ export class ChangesViewPane extends ViewPane { changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified', linesAdded: entry.insertions, linesRemoved: entry.deletions, - reviewCommentCount: reviewCommentCountByFile.get(uri.toString()) ?? 0, + reviewCommentCount: reviewCommentCountByFile.get(uri.fsPath) ?? 0, }; }); }); @@ -750,9 +769,11 @@ export class ChangesViewPane extends ViewPane { const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; // Read code review state to update the button label dynamically - let codeReviewCommentCount: number | undefined; + let reviewCommentCount: number | undefined; let codeReviewLoading = false; if (sessionResource) { + const prReviewState = this.codeReviewService.getPRReviewState(sessionResource).read(reader); + const prReviewCommentCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0; const sessionChanges = this.agentSessionsService.getSession(sessionResource)?.changes; if (sessionChanges instanceof Array && sessionChanges.length > 0) { const reviewFiles = getCodeReviewFilesFromSessionChanges(sessionChanges as readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]); @@ -760,9 +781,15 @@ export class ChangesViewPane extends ViewPane { const reviewState = this.codeReviewService.getReviewState(sessionResource).read(reader); if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === reviewVersion) { codeReviewLoading = true; - } else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === reviewVersion && reviewState.comments.length > 0) { - codeReviewCommentCount = reviewState.comments.length; + } else { + const codeReviewCommentCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === reviewVersion ? reviewState.comments.length : 0; + const totalReviewCommentCount = codeReviewCommentCount + prReviewCommentCount; + if (totalReviewCommentCount > 0) { + reviewCommentCount = totalReviewCommentCount; + } } + } else if (prReviewCommentCount > 0) { + reviewCommentCount = prReviewCommentCount; } } @@ -788,8 +815,8 @@ export class ChangesViewPane extends ViewPane { if (codeReviewLoading) { return { showIcon: true, showLabel: true, isSecondary: true, customLabel: '$(loading~spin)', customClass: 'code-review-loading' }; } - if (codeReviewCommentCount !== undefined) { - return { showIcon: true, showLabel: true, isSecondary: true, customLabel: String(codeReviewCommentCount), customClass: 'code-review-comments' }; + if (reviewCommentCount !== undefined) { + return { showIcon: true, showLabel: true, isSecondary: true, customLabel: String(reviewCommentCount), customClass: 'code-review-comments' }; } return { showIcon: true, showLabel: false, isSecondary: true }; } @@ -943,6 +970,33 @@ export class ChangesViewPane extends ViewPane { })); } + // Bind CI status widget to active session's PR CI model + if (this.ciStatusWidget) { + const activeSessionResourceObs = derived(this, reader => this.sessionManagementService.activeSession.read(reader)?.resource); + const ciModelObs = derived(this, reader => { + const session = this.sessionManagementService.activeSession.read(reader); + if (!session) { + return undefined; + } + const context = this.sessionManagementService.getGitHubContextForSession(session.resource); + if (!context || context.prNumber === undefined) { + return undefined; + } + // Use the PR's headRef from the PR model to get CI checks + const prModel = this.gitHubService.getPullRequest(context.owner, context.repo, context.prNumber); + const pr = prModel.pullRequest.read(reader); + if (!pr) { + // Trigger a refresh if PR data isn't loaded yet + prModel.refresh(); + return undefined; + } + const ciModel = this.gitHubService.getPullRequestCI(context.owner, context.repo, pr.headRef); + ciModel.refresh(); + return ciModel; + }); + this.renderDisposables.add(this.ciStatusWidget.bind(ciModelObs, activeSessionResourceObs)); + } + // Update tree data with combined entries this.renderDisposables.add(autorun(reader => { const entries = combinedEntriesObs.read(reader); @@ -990,8 +1044,10 @@ export class ChangesViewPane extends ViewPane { const overviewHeight = this.overviewContainer?.offsetHeight ?? 0; const containerPadding = 8; // 4px top + 4px bottom from .chat-editing-session-container const containerBorder = 2; // 1px top + 1px bottom border + const ciWidgetHeight = this.ciStatusWidget?.element.offsetHeight ?? 0; + const ciWidgetMargin = ciWidgetHeight > 0 ? 8 : 0; // margin-top on CI widget - const usedHeight = bodyPadding + actionsHeight + actionsMargin + overviewHeight + containerPadding + containerBorder; + const usedHeight = bodyPadding + actionsHeight + actionsMargin + overviewHeight + containerPadding + containerBorder + ciWidgetHeight + ciWidgetMargin; const availableHeight = Math.max(0, bodyHeight - usedHeight); // Limit height to the content so the tree doesn't exceed its items diff --git a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts new file mode 100644 index 00000000000..78023d8bff4 --- /dev/null +++ b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts @@ -0,0 +1,519 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/ciStatusWidget.css'; +import * as dom from '../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; +import { Action } from '../../../../base/common/actions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js'; +import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; +import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from '../../../../workbench/browser/labels.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { GitHubCheckConclusion, GitHubCheckStatus, GitHubCIOverallStatus, IGitHubCICheck } from '../../github/common/types.js'; +import { GitHubPullRequestCIModel } from '../../github/browser/models/githubPullRequestCIModel.js'; + +const $ = dom.$; + +const enum CICheckGroup { + Running, + Pending, + Failed, + Successful, +} + +interface ICICheckListItem { + readonly check: IGitHubCICheck; + readonly group: CICheckGroup; +} + +interface ICICheckCounts { + readonly running: number; + readonly pending: number; + readonly failed: number; + readonly successful: number; +} + +class CICheckListDelegate implements IListVirtualDelegate { + static readonly ITEM_HEIGHT = 24; + + getHeight(_element: ICICheckListItem): number { + return CICheckListDelegate.ITEM_HEIGHT; + } + + getTemplateId(_element: ICICheckListItem): string { + return CICheckListRenderer.TEMPLATE_ID; + } +} + +interface ICICheckTemplateData { + readonly row: HTMLElement; + readonly label: IResourceLabel; + readonly actionBar: ActionBar; + readonly templateDisposables: DisposableStore; + readonly elementDisposables: DisposableStore; +} + +class CICheckListRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'ciCheck'; + readonly templateId = CICheckListRenderer.TEMPLATE_ID; + + constructor( + private readonly _labels: ResourceLabels, + private readonly _openerService: IOpenerService, + ) { } + + renderTemplate(container: HTMLElement): ICICheckTemplateData { + const templateDisposables = new DisposableStore(); + const row = dom.append(container, $('.ci-status-widget-check')); + + const labelContainer = dom.append(row, $('.ci-status-widget-check-label')); + const label = templateDisposables.add(this._labels.create(labelContainer, { supportIcons: true })); + + const actionBarContainer = dom.append(row, $('.ci-status-widget-check-actions')); + const actionBar = templateDisposables.add(new ActionBar(actionBarContainer)); + + return { + row, + label, + actionBar, + templateDisposables, + elementDisposables: templateDisposables.add(new DisposableStore()), + }; + } + + renderElement(element: ICICheckListItem, _index: number, templateData: ICICheckTemplateData): void { + templateData.elementDisposables.clear(); + templateData.actionBar.clear(); + + templateData.row.className = `ci-status-widget-check ${getCheckStatusClass(element.check)}`; + + const title = localize('ci.checkTitle', "{0}: {1}", element.check.name, getCheckStateLabel(element.check)); + templateData.label.setResource({ + name: element.check.name, + resource: URI.from({ scheme: 'github-check', path: `/${element.check.id}/${element.check.name}` }), + }, { + icon: getCheckIcon(element.check), + title, + }); + + const actions: Action[] = []; + + if (element.check.detailsUrl) { + actions.push(templateData.elementDisposables.add(new Action( + 'ci.openOnGitHub', + localize('ci.openOnGitHub', "Open on GitHub"), + ThemeIcon.asClassName(Codicon.linkExternal), + true, + async () => { + await this._openerService.open(URI.parse(element.check.detailsUrl!)); + }, + ))); + } + + templateData.actionBar.push(actions, { icon: true, label: false }); + } + + disposeElement(_element: ICICheckListItem, _index: number, templateData: ICICheckTemplateData): void { + templateData.elementDisposables.clear(); + templateData.actionBar.clear(); + } + + disposeTemplate(templateData: ICICheckTemplateData): void { + templateData.templateDisposables.dispose(); + } +} + +/** + * A collapsible widget that shows the CI status of a PR. + * Rendered beneath the changes tree in the changes view. + */ +export class CIStatusWidget extends Disposable { + + private readonly _domNode: HTMLElement; + private readonly _headerNode: HTMLElement; + private readonly _titleNode: HTMLElement; + private readonly _titleLabel: IResourceLabel; + private readonly _headerActionBarContainer: HTMLElement; + private readonly _headerActionBar: ActionBar; + private readonly _twistieNode: HTMLElement; + private readonly _bodyNode: HTMLElement; + private readonly _list: WorkbenchList; + private readonly _labels: ResourceLabels; + private readonly _headerActionDisposables = this._register(new DisposableStore()); + + private _collapsed = true; + private _model: GitHubPullRequestCIModel | undefined; + private _sessionResource: URI | undefined; + + get element(): HTMLElement { + return this._domNode; + } + + constructor( + container: HTMLElement, + @IOpenerService private readonly _openerService: IOpenerService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + this._labels = this._register(this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); + + this._domNode = dom.append(container, $('.ci-status-widget')); + this._domNode.style.display = 'none'; + + // Header (always visible) + this._headerNode = dom.append(this._domNode, $('.ci-status-widget-header')); + this._titleNode = dom.append(this._headerNode, $('.ci-status-widget-title')); + this._titleLabel = this._register(this._labels.create(this._titleNode, { supportIcons: true })); + this._headerActionBarContainer = dom.append(this._headerNode, $('.ci-status-widget-header-actions')); + this._headerActionBar = this._register(new ActionBar(this._headerActionBarContainer)); + this._headerActionBarContainer.style.display = 'none'; + this._register(dom.addDisposableListener(this._headerActionBarContainer, dom.EventType.CLICK, e => { + e.preventDefault(); + e.stopPropagation(); + })); + this._twistieNode = dom.append(this._headerNode, $('.ci-status-widget-twistie')); + this._updateTwistie(); + + this._register(dom.addDisposableListener(this._headerNode, 'click', () => this._toggle())); + + // Body (collapsible list of checks) + this._bodyNode = dom.append(this._domNode, $('.ci-status-widget-body')); + this._bodyNode.style.display = 'none'; + + const listContainer = $('.ci-status-widget-list'); + this._list = this._register(this._instantiationService.createInstance( + WorkbenchList, + 'CIStatusWidget', + listContainer, + new CICheckListDelegate(), + [new CICheckListRenderer(this._labels, this._openerService)], + { + multipleSelectionSupport: false, + openOnSingleClick: false, + accessibilityProvider: { + getWidgetAriaLabel: () => localize('ci.checksListAriaLabel', "Checks"), + getAriaLabel: item => localize('ci.checkAriaLabel', "{0}, {1}", item.check.name, getCheckStateLabel(item.check)), + }, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: item => item.check.name, + }, + }, + )); + this._bodyNode.appendChild(this._list.getHTMLElement()); + } + + /** + * Bind to a CI model. When `ciModel` is undefined, the widget hides. + * Returns a disposable that stops observation. + */ + bind(ciModel: IObservable, sessionResource: IObservable): IDisposable { + return autorun(reader => { + const model = ciModel.read(reader); + this._sessionResource = sessionResource.read(reader); + this._model = model; + if (!model) { + this._renderBody([]); + this._renderHeaderActions([]); + this._domNode.style.display = 'none'; + return; + } + + const checks = model.checks.read(reader); + const overallStatus = model.overallStatus.read(reader); + + if (checks.length === 0) { + this._renderBody([]); + this._renderHeaderActions([]); + this._domNode.style.display = 'none'; + return; + } + + this._domNode.style.display = ''; + this._renderHeader(checks, overallStatus); + this._renderHeaderActions(getFailedChecks(checks)); + this._renderBody(sortChecks(checks)); + }); + } + + private _toggle(): void { + this._collapsed = !this._collapsed; + this._bodyNode.style.display = this._collapsed ? 'none' : ''; + this._updateTwistie(); + } + + private _updateTwistie(): void { + dom.clearNode(this._twistieNode); + this._twistieNode.appendChild(renderIcon(this._collapsed ? Codicon.chevronRight : Codicon.chevronDown)); + } + + private _renderHeader(checks: readonly IGitHubCICheck[], overallStatus: GitHubCIOverallStatus): void { + const { icon, className } = getHeaderIconAndClass(checks, overallStatus); + this._titleNode.className = `ci-status-widget-title ${className}`; + + const summary = getChecksSummary(checks); + const title = localize('ci.headerTitle', "Checks: {0}", summary); + this._titleLabel.setResource({ + name: title, + resource: URI.from({ scheme: 'github-checks', path: '/summary' }), + }, { + icon: icon, + title, + }); + } + + private _renderHeaderActions(failedChecks: readonly IGitHubCICheck[]): void { + this._headerActionDisposables.clear(); + this._headerActionBar.clear(); + + if (failedChecks.length === 0) { + this._headerActionBarContainer.style.display = 'none'; + return; + } + + const fixChecksAction = this._headerActionDisposables.add(new Action( + 'ci.fixChecks', + localize('ci.fixChecks', "Fix Checks"), + ThemeIcon.asClassName(Codicon.sparkle), + true, + async () => { + await this._sendFixChecksPrompt(failedChecks); + }, + )); + + this._headerActionBar.push([fixChecksAction], { icon: true, label: false }); + this._headerActionBarContainer.style.display = 'flex'; + } + + private _renderBody(checks: readonly ICICheckListItem[]): void { + const height = checks.length * CICheckListDelegate.ITEM_HEIGHT; + this._list.getHTMLElement().style.height = `${height}px`; + this._list.layout(height); + this._list.splice(0, this._list.length, checks); + } + + private async _sendFixChecksPrompt(failedChecks: readonly IGitHubCICheck[]): Promise { + const model = this._model; + const sessionResource = this._sessionResource; + if (!model || !sessionResource || failedChecks.length === 0) { + return; + } + + const failedCheckDetails = await Promise.all(failedChecks.map(async check => { + const annotations = await model.getCheckRunAnnotations(check.id); + return { + check, + annotations, + }; + })); + + const prompt = buildFixChecksPrompt(failedCheckDetails); + const chatWidget = this._chatWidgetService.getWidgetBySessionResource(sessionResource) + ?? await this._chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); + if (!chatWidget) { + return; + } + + await chatWidget.acceptInput(prompt, { noCommandDetection: true }); + } +} + +function sortChecks(checks: readonly IGitHubCICheck[]): ICICheckListItem[] { + return [...checks] + .sort(compareChecks) + .map(check => ({ check, group: getCheckGroup(check) })); +} + +function compareChecks(a: IGitHubCICheck, b: IGitHubCICheck): number { + const groupDiff = getCheckGroup(a) - getCheckGroup(b); + if (groupDiff !== 0) { + return groupDiff; + } + + return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); +} + +function getCheckGroup(check: IGitHubCICheck): CICheckGroup { + switch (check.status) { + case GitHubCheckStatus.InProgress: + return CICheckGroup.Running; + case GitHubCheckStatus.Queued: + return CICheckGroup.Pending; + case GitHubCheckStatus.Completed: + return isFailedConclusion(check.conclusion) ? CICheckGroup.Failed : CICheckGroup.Successful; + } +} + +function getCheckCounts(checks: readonly IGitHubCICheck[]): ICICheckCounts { + let running = 0; + let pending = 0; + let failed = 0; + let successful = 0; + + for (const check of checks) { + switch (getCheckGroup(check)) { + case CICheckGroup.Running: + running++; + break; + case CICheckGroup.Pending: + pending++; + break; + case CICheckGroup.Failed: + failed++; + break; + case CICheckGroup.Successful: + successful++; + break; + } + } + + return { running, pending, failed, successful }; +} + +function getFailedChecks(checks: readonly IGitHubCICheck[]): readonly IGitHubCICheck[] { + return checks.filter(check => getCheckGroup(check) === CICheckGroup.Failed); +} + +function getChecksSummary(checks: readonly IGitHubCICheck[]): string { + const counts = getCheckCounts(checks); + const parts: string[] = []; + + if (counts.running > 0) { + parts.push(counts.running === 1 + ? localize('ci.oneRunning', "1 running") + : localize('ci.manyRunning', "{0} running", counts.running)); + } + + if (counts.pending > 0) { + parts.push(counts.pending === 1 + ? localize('ci.onePending', "1 pending") + : localize('ci.manyPending', "{0} pending", counts.pending)); + } + + if (counts.failed > 0) { + parts.push(counts.failed === 1 + ? localize('ci.oneFailed', "1 failed") + : localize('ci.manyFailed', "{0} failed", counts.failed)); + } + + if (counts.successful > 0) { + parts.push(counts.successful === 1 + ? localize('ci.oneSuccessful', "1 successful") + : localize('ci.manySuccessful', "{0} successful", counts.successful)); + } + + return parts.join(', '); +} + +function buildFixChecksPrompt(failedChecks: ReadonlyArray<{ check: IGitHubCICheck; annotations: string }>): string { + const sections = failedChecks.map(({ check, annotations }) => { + const parts = [ + `Check: ${check.name}`, + `Status: ${getCheckStateLabel(check)}`, + `Conclusion: ${check.conclusion ?? 'unknown'}`, + ]; + + if (check.detailsUrl) { + parts.push(`Details: ${check.detailsUrl}`); + } + + parts.push('', 'Annotations and output:', annotations || 'No output available for this check run.'); + return parts.join('\n'); + }); + + return [ + 'Please fix the failed CI checks for this session immediately.', + 'Use the failed check information below, including annotations and check output, to identify the root causes and make the necessary code changes.', + 'Focus on resolving these CI failures. Avoid unrelated changes unless they are required to fix the checks.', + '', + 'Failed CI checks:', + '', + sections.join('\n\n---\n\n'), + ].join('\n'); +} + +function getHeaderIconAndClass(checks: readonly IGitHubCICheck[], overallStatus: GitHubCIOverallStatus): { icon: ThemeIcon; className: string } { + const counts = getCheckCounts(checks); + if (counts.running > 0) { + return { icon: spinningLoading, className: 'ci-status-running' }; + } + + switch (overallStatus) { + case GitHubCIOverallStatus.Success: + return { icon: Codicon.passFilled, className: 'ci-status-success' }; + case GitHubCIOverallStatus.Failure: + return { icon: Codicon.error, className: 'ci-status-failure' }; + case GitHubCIOverallStatus.Pending: + return { icon: Codicon.circle, className: 'ci-status-pending' }; + default: + return { icon: Codicon.circleFilled, className: 'ci-status-neutral' }; + } +} + +function getCheckIcon(check: IGitHubCICheck): ThemeIcon { + switch (check.status) { + case GitHubCheckStatus.InProgress: + return spinningLoading; + case GitHubCheckStatus.Queued: + return Codicon.circle; + case GitHubCheckStatus.Completed: + switch (check.conclusion) { + case GitHubCheckConclusion.Success: + return Codicon.passFilled; + case GitHubCheckConclusion.Failure: + case GitHubCheckConclusion.TimedOut: + case GitHubCheckConclusion.ActionRequired: + return Codicon.error; + case GitHubCheckConclusion.Cancelled: + return Codicon.circleSlash; + case GitHubCheckConclusion.Skipped: + return Codicon.debugStepOver; + default: + return Codicon.circleFilled; + } + } +} + +function getCheckStatusClass(check: IGitHubCICheck): string { + switch (getCheckGroup(check)) { + case CICheckGroup.Running: + return 'ci-status-running'; + case CICheckGroup.Pending: + return 'ci-status-pending'; + case CICheckGroup.Failed: + return 'ci-status-failure'; + case CICheckGroup.Successful: + return 'ci-status-success'; + } +} + +function getCheckStateLabel(check: IGitHubCICheck): string { + switch (getCheckGroup(check)) { + case CICheckGroup.Running: + return localize('ci.runningState', "running"); + case CICheckGroup.Pending: + return localize('ci.pendingState', "pending"); + case CICheckGroup.Failed: + return localize('ci.failedState', "failed"); + case CICheckGroup.Successful: + return localize('ci.successfulState', "successful"); + } +} + +function isFailedConclusion(conclusion: GitHubCheckConclusion | undefined): boolean { + return conclusion === GitHubCheckConclusion.Failure + || conclusion === GitHubCheckConclusion.TimedOut + || conclusion === GitHubCheckConclusion.ActionRequired; +} diff --git a/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css b/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css new file mode 100644 index 00000000000..2acaa1fa18d --- /dev/null +++ b/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* CI Status Widget - beneath the changes tree */ +.ci-status-widget { + margin-top: 8px; + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 4px; + background-color: var(--vscode-editor-background); + overflow: hidden; + font-size: 12px; +} + +/* Header - always visible, clickable */ +.ci-status-widget-header { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 4px; + cursor: pointer; + -webkit-user-select: none; + user-select: none; + min-height: 22px; +} + +.ci-status-widget-header:hover { + background-color: var(--vscode-list-hoverBackground); +} + +/* Title - single line, overflow ellipsis */ +.ci-status-widget-title { + flex: 1; + overflow: hidden; + color: var(--vscode-foreground); +} + +.ci-status-widget-title .monaco-icon-label { + width: 100%; +} + +.ci-status-widget-title .monaco-icon-label-container, +.ci-status-widget-title .monaco-icon-name-container { + display: block; + overflow: hidden; +} + +.ci-status-widget-title .label-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ci-status-widget-header-actions { + flex: 0 0 auto; + display: none; + align-items: center; + margin-left: auto; +} + +.ci-status-widget-header-actions .monaco-action-bar { + display: flex; + align-items: center; +} + +.ci-status-widget-header-actions .action-item .action-label { + width: 16px; + height: 16px; +} + +/* Twistie icon on the right */ +.ci-status-widget-twistie { + flex: 0 0 auto; + display: flex; + align-items: center; + color: var(--vscode-foreground); + opacity: 0.7; +} + +/* Body - collapsible list */ +.ci-status-widget-body { + border-top: 1px solid var(--vscode-input-border, transparent); +} + +.ci-status-widget-list { + background-color: transparent; +} + +.ci-status-widget-list > .monaco-list, +.ci-status-widget-list > .monaco-list > .monaco-scrollable-element { + background-color: transparent; +} + +/* Individual check row */ +.ci-status-widget-check { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 4px; + height: 100%; + width: 100%; + box-sizing: border-box; + min-width: 0; +} + +.ci-status-widget-list .monaco-list-row:hover .ci-status-widget-check, +.ci-status-widget-list .monaco-list-row.focused .ci-status-widget-check, +.ci-status-widget-list .monaco-list-row.selected .ci-status-widget-check { + background-color: var(--vscode-list-hoverBackground); +} + +.ci-status-widget-check-label { + display: flex; + flex: 1; + min-width: 0; + overflow: hidden; +} + + +.ci-status-widget-check-label .monaco-icon-label { + display: flex; + flex: 1; + min-width: 0; + width: 100%; +} + +.ci-status-widget-check-label .monaco-icon-label-container, +.ci-status-widget-check-label .monaco-icon-name-container { + display: block; + min-width: 0; + overflow: hidden; +} + +.ci-status-widget-check-label .label-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vscode-foreground); +} + +.ci-status-widget-title.ci-status-success .monaco-icon-label::before, +.ci-status-widget-check.ci-status-success .monaco-icon-label::before { + color: var(--vscode-testing-iconPassed, #73c991); +} + +.ci-status-widget-title.ci-status-failure .monaco-icon-label::before, +.ci-status-widget-check.ci-status-failure .monaco-icon-label::before { + color: var(--vscode-testing-iconFailed, #f14c4c); +} + +.ci-status-widget-title.ci-status-running .monaco-icon-label::before, +.ci-status-widget-check.ci-status-running .monaco-icon-label::before, +.ci-status-widget-title.ci-status-pending .monaco-icon-label::before, +.ci-status-widget-check.ci-status-pending .monaco-icon-label::before { + color: var(--vscode-testing-iconQueued, var(--vscode-editorWarning-foreground)); +} + +.ci-status-widget-title.ci-status-neutral .monaco-icon-label::before, +.ci-status-widget-check.ci-status-neutral .monaco-icon-label::before { + color: var(--vscode-descriptionForeground); +} + +/* Actions - float to the right, visible on hover */ +.ci-status-widget-check-actions { + display: none; + flex: 0 0 auto; + flex-shrink: 0; + margin-left: auto; +} + +.ci-status-widget-list .monaco-list-row:hover .ci-status-widget-check-actions, +.ci-status-widget-list .monaco-list-row.focused .ci-status-widget-check-actions, +.ci-status-widget-list .monaco-list-row.selected .ci-status-widget-check-actions, +.ci-status-widget-check:hover .ci-status-widget-check-actions { + display: flex; +} + +.ci-status-widget-check-actions .monaco-action-bar { + display: flex; + align-items: center; +} + +.ci-status-widget-check-actions .action-bar .action-item .action-label { + width: 16px; + height: 16px; +} diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts index bb3818e8a46..75a9ffaad22 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts @@ -18,10 +18,10 @@ import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actio import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from './codeReviewService.js'; +import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, PRReviewStateKind } from './codeReviewService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js'; -import { SessionEditorCommentSource, toSessionEditorCommentId } from '../../agentFeedback/browser/sessionEditorComments.js'; +import { getSessionEditorComments } from '../../agentFeedback/browser/sessionEditorComments.js'; registerSingleton(ICodeReviewService, CodeReviewService, InstantiationType.Delayed); @@ -62,7 +62,6 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp const resource = URI.isUri(sessionResource) ? sessionResource : sessionManagementService.getActiveSession()?.resource; - if (!resource) { return; } @@ -75,15 +74,27 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp const files = getCodeReviewFilesFromSessionChanges(session.changes); const version = getCodeReviewVersion(files); - // If a review already exists with comments, navigate to the first comment + // If there are existing comments (code review or PR review), navigate to the first one const reviewState = codeReviewService.getReviewState(resource).get(); - if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version && reviewState.comments.length > 0) { - const firstComment = reviewState.comments[0]; - const commentId = toSessionEditorCommentId(SessionEditorCommentSource.CodeReview, firstComment.id); - await agentFeedbackService.revealSessionComment(resource, commentId, firstComment.uri, firstComment.range); + const prReviewState = codeReviewService.getPRReviewState(resource).get(); + const codeReviewCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version ? reviewState.comments.length : 0; + const prReviewCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0; + + if (codeReviewCount > 0 || prReviewCount > 0) { + const comments = getSessionEditorComments( + resource, + agentFeedbackService.getFeedback(resource), + reviewState, + prReviewState, + ); + const first = agentFeedbackService.getNextNavigableItem(resource, comments, true); + if (first) { + await agentFeedbackService.revealSessionComment(resource, first.id, first.resourceUri, first.range); + } return; } + codeReviewService.requestReview(resource, version, files); } } @@ -130,6 +141,11 @@ class CodeReviewToolbarContribution extends Disposable implements IWorkbenchCont const files = getCodeReviewFilesFromSessionChanges(session.changes); const version = getCodeReviewVersion(files); const reviewState = this._codeReviewService.getReviewState(sessionResource).read(reader); + const prReviewState = this._codeReviewService.getPRReviewState(sessionResource).read(reader); + + const codeReviewCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version ? reviewState.comments.length : 0; + const prReviewCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0; + const totalCommentCount = codeReviewCount + prReviewCount; let canRunCodeReview = true; let tooltip = localize('sessions.runCodeReview.tooltip.default', "Run Code Review"); @@ -138,19 +154,17 @@ class CodeReviewToolbarContribution extends Disposable implements IWorkbenchCont if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === version) { canRunCodeReview = false; tooltip = localize('sessions.runCodeReview.tooltip.loading', "Creating code review..."); - icon = Codicon.codeReview; + icon = Codicon.commentDraft; + } else if (totalCommentCount > 0) { + canRunCodeReview = true; + icon = Codicon.commentUnresolved; + tooltip = totalCommentCount === 1 + ? localize('sessions.runCodeReview.tooltip.oneUnresolved', "1 review comment unresolved.") + : localize('sessions.runCodeReview.tooltip.manyUnresolved', "{0} review comments unresolved.", totalCommentCount); } else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version) { - if (reviewState.comments.length === 0) { - canRunCodeReview = false; - tooltip = localize('sessions.runCodeReview.tooltip.allResolved', "All review comments have been addressed."); - icon = Codicon.comment; - } else { - canRunCodeReview = true; - icon = Codicon.commentUnresolved; - tooltip = reviewState.comments.length === 1 - ? localize('sessions.runCodeReview.tooltip.oneUnresolved', "1 review comment unresolved.") - : localize('sessions.runCodeReview.tooltip.manyUnresolved', "{0} review comments unresolved.", reviewState.comments.length); - } + canRunCodeReview = false; + tooltip = localize('sessions.runCodeReview.tooltip.allResolved', "All review comments have been addressed."); + icon = Codicon.comment; } canRunCodeReviewContext.set(canRunCodeReview); diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts index 8230f01c789..689c409ffe4 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts @@ -3,18 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { hash } from '../../../../base/common/hash.js'; import { hasKey } from '../../../../base/common/types.js'; import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { IGitHubService } from '../../github/browser/githubService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; // --- Types ------------------------------------------------------------------- @@ -80,6 +83,29 @@ export type ICodeReviewState = | { readonly kind: CodeReviewStateKind.Result; readonly version: string; readonly comments: readonly ICodeReviewComment[] } | { readonly kind: CodeReviewStateKind.Error; readonly version: string; readonly reason: string }; +// --- PR Review Types --------------------------------------------------------- + +export const enum PRReviewStateKind { + None = 'none', + Loading = 'loading', + Loaded = 'loaded', + Error = 'error', +} + +export type IPRReviewState = + | { readonly kind: PRReviewStateKind.None } + | { readonly kind: PRReviewStateKind.Loading } + | { readonly kind: PRReviewStateKind.Loaded; readonly comments: readonly IPRReviewComment[] } + | { readonly kind: PRReviewStateKind.Error; readonly reason: string }; + +export interface IPRReviewComment { + readonly id: string; + readonly uri: URI; + readonly range: IRange; + readonly body: string; + readonly author: string; +} + /** Shape of a single comment as returned by the code review command. */ interface IRawCodeReviewComment { readonly uri: IRawCodeReviewUri; @@ -156,6 +182,17 @@ export interface ICodeReviewService { * Dismiss/clear the review for a session entirely. */ dismissReview(sessionResource: URI): void; + + /** + * Get the observable PR review state for a session. + * Returns unresolved review comments from the PR associated with the session. + */ + getPRReviewState(sessionResource: URI): IObservable; + + /** + * Resolve a PR review thread on GitHub and remove it from local state. + */ + resolvePRReviewThread(sessionResource: URI, threadId: string): Promise; } // --- Storage Types ----------------------------------------------------------- @@ -181,6 +218,12 @@ interface ISessionReviewData { readonly state: ReturnType>; } +interface IPRSessionReviewData { + readonly state: ReturnType>; + readonly disposables: DisposableStore; + initialized: boolean; +} + function isRawCodeReviewRangeWithPositions(range: IRawCodeReviewRange): range is IRawCodeReviewRangeWithPositions { return typeof range === 'object' && range !== null && hasKey(range, { start: true, end: true }); } @@ -247,15 +290,40 @@ export class CodeReviewService extends Disposable implements ICodeReviewService private static readonly _STORAGE_KEY = 'codeReview.reviews'; private readonly _reviewsBySession = new Map(); + private readonly _prReviewBySession = new Map(); constructor( @ICommandService private readonly _commandService: ICommandService, + @ILogService private readonly _logService: ILogService, @IStorageService private readonly _storageService: IStorageService, + @IGitHubService private readonly _gitHubService: IGitHubService, + @ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, ) { super(); this._loadFromStorage(); this._registerSessionListeners(); + + this._register(autorun(reader => { + const activeSession = this._sessionsManagementService.activeSession.read(reader); + if (activeSession) { + this._ensurePRReviewInitialized(activeSession.resource); + } + })); + + this._register(this._agentSessionsService.model.onDidChangeSessions(() => { + for (const session of this._agentSessionsService.model.sessions) { + if (!session.isArchived()) { + this._ensurePRReviewInitialized(session.resource); + } + } + })); + + this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(e => { + if (e.isArchived()) { + this._disposePRReview(e.resource); + } + })); } getReviewState(sessionResource: URI): IObservable { @@ -483,4 +551,112 @@ export class CodeReviewService extends Disposable implements ICodeReviewService } })); } + + getPRReviewState(sessionResource: URI): IObservable { + return this._getOrCreatePRReviewData(sessionResource).state; + } + + async resolvePRReviewThread(sessionResource: URI, threadId: string): Promise { + const context = this._sessionsManagementService.getGitHubContextForSession(sessionResource); + if (context?.prNumber !== undefined) { + const prModel = this._gitHubService.getPullRequest(context.owner, context.repo, context.prNumber); + try { + await prModel.resolveThread(threadId); + } catch (err) { + this._logService.warn('[CodeReviewService] Failed to resolve PR thread on GitHub:', err); + } + } + + // Remove from local state regardless of GitHub success + const data = this._prReviewBySession.get(sessionResource.toString()); + if (data) { + const currentState = data.state.get(); + if (currentState.kind === PRReviewStateKind.Loaded) { + const filtered = currentState.comments.filter(c => c.id !== threadId); + data.state.set({ kind: PRReviewStateKind.Loaded, comments: filtered }, undefined); + } + } + } + + private _getOrCreatePRReviewData(sessionResource: URI): IPRSessionReviewData { + const key = sessionResource.toString(); + let data = this._prReviewBySession.get(key); + if (!data) { + data = { + state: observableValue(`prReview.state.${key}`, { kind: PRReviewStateKind.None }), + disposables: new DisposableStore(), + initialized: false, + }; + this._prReviewBySession.set(key, data); + } + return data; + } + + private _ensurePRReviewInitialized(sessionResource: URI): void { + const data = this._getOrCreatePRReviewData(sessionResource); + if (data.initialized) { + return; + } + + const context = this._sessionsManagementService.getGitHubContextForSession(sessionResource); + if (!context || context.prNumber === undefined) { + return; + } + + data.initialized = true; + data.state.set({ kind: PRReviewStateKind.Loading }, undefined); + + const prModel = this._gitHubService.getPullRequest(context.owner, context.repo, context.prNumber); + + // Watch the PR model's review threads and map to local state + data.disposables.add(autorun(reader => { + const threads = prModel.reviewThreads.read(reader); + const comments: IPRReviewComment[] = []; + + for (const thread of threads) { + if (thread.isResolved) { + continue; + } + const fileUri = this._sessionsManagementService.resolveSessionFileUri(sessionResource, thread.path); + if (!fileUri) { + continue; + } + const line = thread.line ?? 1; + const firstComment = thread.comments[0]; + comments.push({ + id: String(thread.id), + uri: fileUri, + range: new Range(line, 1, line, 1), + body: firstComment?.body ?? '', + author: firstComment?.author.login ?? '', + }); + } + + data.state.set({ kind: PRReviewStateKind.Loaded, comments }, undefined); + })); + + // Start polling and initial fetch + prModel.refreshThreads().catch(err => { + this._logService.error('[CodeReviewService] Failed to fetch PR review threads:', err); + data.state.set({ kind: PRReviewStateKind.Error, reason: String(err) }, undefined); + }); + prModel.startPolling(); + } + + private _disposePRReview(sessionResource: URI): void { + const key = sessionResource.toString(); + const data = this._prReviewBySession.get(key); + if (data) { + data.disposables.dispose(); + this._prReviewBySession.delete(key); + } + } + + override dispose(): void { + for (const data of this._prReviewBySession.values()) { + data.disposables.dispose(); + } + this._prReviewBySession.clear(); + super.dispose(); + } } diff --git a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts index 000bf528d12..193a55925b2 100644 --- a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts +++ b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts @@ -6,16 +6,21 @@ import assert from 'assert'; import { URI } from '../../../../../base/common/uri.js'; import { Range } from '../../../../../editor/common/core/range.js'; +import { observableValue } from '../../../../../base/common/observable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { InMemoryStorageService, IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { IAgentSession, IAgentSessionsModel } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IChatSessionFileChange2 } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../browser/codeReviewService.js'; +import { IGitHubService } from '../../../github/browser/githubService.js'; +import { IActiveSessionItem, ISessionsManagementService } from '../../../sessions/browser/sessionsManagementService.js'; suite('CodeReviewService', () => { @@ -162,6 +167,11 @@ suite('CodeReviewService', () => { commandService = new MockCommandService(); instantiationService.stub(ICommandService, commandService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IGitHubService, new class extends mock() { }()); + instantiationService.stub(ISessionsManagementService, new class extends mock() { + override readonly activeSession = observableValue('test.activeSession', undefined); + }()); storageService = store.add(new InMemoryStorageService()); instantiationService.stub(IStorageService, storageService); diff --git a/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts b/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts new file mode 100644 index 00000000000..d667c6a3a3e --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GitHubCheckConclusion, GitHubCheckStatus, GitHubCIOverallStatus, IGitHubCICheck } from '../../common/types.js'; +import { GitHubApiClient } from '../githubApiClient.js'; + +//#region GitHub API response types + +interface IGitHubCheckRunResponse { + readonly id: number; + readonly name: string; + readonly status: string; + readonly conclusion: string | null; + readonly started_at: string | null; + readonly completed_at: string | null; + readonly details_url: string | null; +} + +interface IGitHubCheckRunsListResponse { + readonly total_count: number; + readonly check_runs: readonly IGitHubCheckRunResponse[]; +} + +interface IGitHubCheckRunAnnotationResponse { + readonly path: string; + readonly start_line: number; + readonly end_line: number; + readonly annotation_level: string; + readonly message: string; + readonly title: string | null; +} + +interface IGitHubCheckRunDetailResponse { + readonly id: number; + readonly name: string; + readonly details_url: string | null; + readonly app: { + readonly slug: string; + } | null; + readonly output: { + readonly title: string | null; + readonly summary: string | null; + readonly text: string | null; + readonly annotations_count: number; + }; +} + +//#endregion + +/** + * Stateless fetcher for GitHub CI check data (check runs, check suites). + * All methods return raw typed data with no caching or state. + */ +export class GitHubPRCIFetcher { + + constructor( + private readonly _apiClient: GitHubApiClient, + ) { } + + async getCheckRuns(owner: string, repo: string, ref: string): Promise { + const data = await this._apiClient.request( + 'GET', + `/repos/${e(owner)}/${e(repo)}/commits/${e(ref)}/check-runs`, + ); + return data.check_runs.map(mapCheckRun); + } + + /** + * Get logs/output for a specific check run. + * + * Tries multiple sources in order: + * 1. The check run's own output fields (title, summary, text) — set by the + * check run creator via the Checks API. + * 2. Annotations attached to the check run. + * 3. GitHub Actions job logs (only works for GitHub Actions workflows). + */ + async getCheckRunAnnotations(owner: string, repo: string, checkRunId: number): Promise { + const sections: string[] = []; + let detail: IGitHubCheckRunDetailResponse | undefined; + + // 1. Fetch check run detail for output fields + try { + detail = await this._apiClient.request( + 'GET', + `/repos/${e(owner)}/${e(repo)}/check-runs/${checkRunId}`, + ); + const output = detail.output; + if (output.title) { + sections.push(`# ${output.title}`); + } + if (output.summary) { + sections.push(output.summary); + } + if (output.text) { + sections.push(output.text); + } + } catch { + // Ignore — output may not be available + } + + // 2. Fetch annotations + try { + const annotations = await this._apiClient.request( + 'GET', + `/repos/${e(owner)}/${e(repo)}/check-runs/${checkRunId}/annotations`, + ); + if (annotations.length > 0) { + sections.push( + annotations.map(a => + `[${a.annotation_level}] ${a.path}:${a.start_line}${a.end_line !== a.start_line ? `-${a.end_line}` : ''} ${a.title ? `(${a.title}) ` : ''}${a.message}` + ).join('\n') + ); + } + } catch { + // Ignore — annotations may not be available + } + + if (sections.length > 0) { + return sections.join('\n\n'); + } + + return 'No output available for this check run.'; + } +} + +//#region Helpers + +function e(value: string): string { + return encodeURIComponent(value); +} + +function mapCheckRun(data: IGitHubCheckRunResponse): IGitHubCICheck { + return { + id: data.id, + name: data.name, + status: mapCheckStatus(data.status), + conclusion: data.conclusion ? mapCheckConclusion(data.conclusion) : undefined, + startedAt: data.started_at ?? undefined, + completedAt: data.completed_at ?? undefined, + detailsUrl: data.details_url ?? undefined, + }; +} + +function mapCheckStatus(status: string): GitHubCheckStatus { + switch (status) { + case 'queued': return GitHubCheckStatus.Queued; + case 'in_progress': return GitHubCheckStatus.InProgress; + case 'completed': return GitHubCheckStatus.Completed; + default: return GitHubCheckStatus.Queued; + } +} + +function mapCheckConclusion(conclusion: string): GitHubCheckConclusion { + switch (conclusion) { + case 'success': return GitHubCheckConclusion.Success; + case 'failure': return GitHubCheckConclusion.Failure; + case 'neutral': return GitHubCheckConclusion.Neutral; + case 'cancelled': return GitHubCheckConclusion.Cancelled; + case 'skipped': return GitHubCheckConclusion.Skipped; + case 'timed_out': return GitHubCheckConclusion.TimedOut; + case 'action_required': return GitHubCheckConclusion.ActionRequired; + case 'stale': return GitHubCheckConclusion.Stale; + default: return GitHubCheckConclusion.Neutral; + } +} + +/** + * Compute an overall CI status from a list of check runs. + */ +export function computeOverallCIStatus(checks: readonly IGitHubCICheck[]): GitHubCIOverallStatus { + if (checks.length === 0) { + return GitHubCIOverallStatus.Neutral; + } + + let hasFailure = false; + let hasPending = false; + + for (const check of checks) { + if (check.status !== GitHubCheckStatus.Completed) { + hasPending = true; + continue; + } + if (check.conclusion === GitHubCheckConclusion.Failure || + check.conclusion === GitHubCheckConclusion.TimedOut || + check.conclusion === GitHubCheckConclusion.ActionRequired) { + hasFailure = true; + } + } + + if (hasFailure) { + return GitHubCIOverallStatus.Failure; + } + if (hasPending) { + return GitHubCIOverallStatus.Pending; + } + return GitHubCIOverallStatus.Success; +} + +//#endregion diff --git a/src/vs/sessions/contrib/github/browser/fetchers/githubPRFetcher.ts b/src/vs/sessions/contrib/github/browser/fetchers/githubPRFetcher.ts new file mode 100644 index 00000000000..5711a3d9ff8 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/fetchers/githubPRFetcher.ts @@ -0,0 +1,362 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + GitHubPullRequestState, + IGitHubPRComment, + IGitHubPRReviewThread, + IGitHubPullRequest, + IGitHubPullRequestMergeability, + IGitHubUser, + IMergeBlocker, + MergeBlockerKind, +} from '../../common/types.js'; +import { GitHubApiClient } from '../githubApiClient.js'; + +//#region GitHub API response types + +interface IGitHubPRResponse { + readonly number: number; + readonly title: string; + readonly body: string | null; + readonly state: 'open' | 'closed'; + readonly draft: boolean; + readonly user: { readonly login: string; readonly avatar_url: string }; + readonly head: { readonly ref: string }; + readonly base: { readonly ref: string }; + readonly created_at: string; + readonly updated_at: string; + readonly merged_at: string | null; + readonly mergeable: boolean | null; + readonly mergeable_state: string; + readonly merged: boolean; +} + +interface IGitHubReviewResponse { + readonly id: number; + readonly user: { readonly login: string; readonly avatar_url: string }; + readonly state: string; + readonly submitted_at: string; +} + +interface IGitHubReviewCommentResponse { + readonly id: number; + readonly body: string; + readonly user: { readonly login: string; readonly avatar_url: string }; + readonly created_at: string; + readonly updated_at: string; + readonly path: string; + readonly line: number | null; + readonly original_line: number | null; + readonly in_reply_to_id?: number; +} + +interface IGitHubIssueCommentResponse { + readonly id: number; + readonly body: string | null; + readonly user: { readonly login: string; readonly avatar_url: string }; + readonly created_at: string; + readonly updated_at: string; +} + +interface IGitHubGraphQLPullRequestReviewThreadsResponse { + readonly repository: { + readonly pullRequest: { + readonly reviewThreads: { + readonly nodes: readonly IGitHubGraphQLReviewThreadNode[]; + }; + } | null; + } | null; +} + +interface IGitHubGraphQLReviewThreadNode { + readonly id: string; + readonly isResolved: boolean; + readonly path: string; + readonly line: number | null; + readonly comments: { + readonly nodes: readonly IGitHubGraphQLReviewCommentNode[]; + }; +} + +interface IGitHubGraphQLReviewCommentNode { + readonly databaseId: number | null; + readonly body: string; + readonly createdAt: string; + readonly updatedAt: string; + readonly path: string | null; + readonly line: number | null; + readonly originalLine: number | null; + readonly replyTo: { readonly databaseId: number | null } | null; + readonly author: { readonly login: string; readonly avatarUrl: string } | null; +} + +interface IGitHubGraphQLResolveReviewThreadResponse { + readonly resolveReviewThread: { + readonly thread: { + readonly isResolved: boolean; + } | null; + } | null; +} + +//#endregion + +const GET_REVIEW_THREADS_QUERY = [ + 'query GetReviewThreads($owner: String!, $repo: String!, $prNumber: Int!) {', + ' repository(owner: $owner, name: $repo) {', + ' pullRequest(number: $prNumber) {', + ' reviewThreads(first: 100) {', + ' nodes {', + ' id', + ' isResolved', + ' path', + ' line', + ' comments(first: 100) {', + ' nodes {', + ' databaseId', + ' body', + ' createdAt', + ' updatedAt', + ' path', + ' line', + ' originalLine', + ' replyTo {', + ' databaseId', + ' }', + ' author {', + ' login', + ' avatarUrl', + ' }', + ' }', + ' }', + ' }', + ' }', + ' }', + ' }', + '}', +].join('\n'); + +const RESOLVE_REVIEW_THREAD_MUTATION = [ + 'mutation ResolveReviewThread($threadId: ID!) {', + ' resolveReviewThread(input: { threadId: $threadId }) {', + ' thread {', + ' isResolved', + ' }', + ' }', + '}', +].join('\n'); + +/** + * Stateless fetcher for GitHub pull request data. + * Handles all PR-related REST API calls including reviews, comments, and mergeability. + */ +export class GitHubPRFetcher { + + constructor( + private readonly _apiClient: GitHubApiClient, + ) { } + + async getPullRequest(owner: string, repo: string, prNumber: number): Promise { + const data = await this._apiClient.request( + 'GET', + `/repos/${e(owner)}/${e(repo)}/pulls/${prNumber}`, + ); + return mapPullRequest(data); + } + + async getMergeability(owner: string, repo: string, prNumber: number): Promise { + const [pr, reviews] = await Promise.all([ + this._apiClient.request('GET', `/repos/${e(owner)}/${e(repo)}/pulls/${prNumber}`), + this._apiClient.request('GET', `/repos/${e(owner)}/${e(repo)}/pulls/${prNumber}/reviews`), + ]); + + const blockers: IMergeBlocker[] = []; + + // Draft + if (pr.draft) { + blockers.push({ kind: MergeBlockerKind.Draft, description: 'Pull request is a draft' }); + } + + // Merge conflicts + if (pr.mergeable === false) { + blockers.push({ kind: MergeBlockerKind.Conflicts, description: 'Pull request has merge conflicts' }); + } + + // Changes requested — check most recent review per reviewer + const latestReviewByUser = new Map(); + for (const review of reviews) { + if (review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED' || review.state === 'DISMISSED') { + latestReviewByUser.set(review.user.login, review.state); + } + } + const hasChangesRequested = [...latestReviewByUser.values()].some(s => s === 'CHANGES_REQUESTED'); + if (hasChangesRequested) { + blockers.push({ kind: MergeBlockerKind.ChangesRequested, description: 'Changes have been requested' }); + } + + // Approval needed — check mergeable_state + if (pr.mergeable_state === 'blocked') { + const hasApproval = [...latestReviewByUser.values()].some(s => s === 'APPROVED'); + if (!hasApproval) { + blockers.push({ kind: MergeBlockerKind.ApprovalNeeded, description: 'Approval is required' }); + } + } + + // CI failures — mergeable_state 'unstable' indicates check failures + if (pr.mergeable_state === 'unstable') { + blockers.push({ kind: MergeBlockerKind.CIFailed, description: 'CI checks have failed' }); + } + + return { + canMerge: blockers.length === 0 && pr.mergeable !== false && pr.state === 'open', + blockers, + }; + } + + async getReviewThreads(owner: string, repo: string, prNumber: number): Promise { + const data = await this._apiClient.graphql( + GET_REVIEW_THREADS_QUERY, + { owner, repo, prNumber }, + ); + + const reviewThreads = data.repository?.pullRequest?.reviewThreads.nodes; + if (!reviewThreads) { + throw new Error(`Pull request not found: ${owner}/${repo}#${prNumber}`); + } + + return reviewThreads.map(mapReviewThread); + } + + async postReviewComment( + owner: string, + repo: string, + prNumber: number, + body: string, + inReplyTo: number, + ): Promise { + const data = await this._apiClient.request( + 'POST', + `/repos/${e(owner)}/${e(repo)}/pulls/${prNumber}/comments`, + { body, in_reply_to: inReplyTo }, + ); + return mapReviewComment(data); + } + + async postIssueComment( + owner: string, + repo: string, + prNumber: number, + body: string, + ): Promise { + const data = await this._apiClient.request( + 'POST', + `/repos/${e(owner)}/${e(repo)}/issues/${prNumber}/comments`, + { body }, + ); + return { + id: data.id, + body: data.body ?? '', + author: mapUser(data.user), + createdAt: data.created_at, + updatedAt: data.updated_at, + path: undefined, + line: undefined, + threadId: String(data.id), + inReplyToId: undefined, + }; + } + + async resolveThread(_owner: string, _repo: string, threadId: string): Promise { + const data = await this._apiClient.graphql( + RESOLVE_REVIEW_THREAD_MUTATION, + { threadId }, + ); + + if (!data.resolveReviewThread?.thread?.isResolved) { + throw new Error(`Failed to resolve review thread ${threadId}`); + } + } +} + +//#region Helpers + +function e(value: string): string { + return encodeURIComponent(value); +} + +function mapUser(user: { readonly login: string; readonly avatar_url: string }): IGitHubUser { + return { login: user.login, avatarUrl: user.avatar_url }; +} + +function mapPullRequest(data: IGitHubPRResponse): IGitHubPullRequest { + let state: GitHubPullRequestState; + if (data.merged) { + state = GitHubPullRequestState.Merged; + } else if (data.state === 'closed') { + state = GitHubPullRequestState.Closed; + } else { + state = GitHubPullRequestState.Open; + } + + return { + number: data.number, + title: data.title, + body: data.body ?? '', + state, + author: mapUser(data.user), + headRef: data.head.ref, + baseRef: data.base.ref, + isDraft: data.draft, + createdAt: data.created_at, + updatedAt: data.updated_at, + mergedAt: data.merged_at ?? undefined, + mergeable: data.mergeable ?? undefined, + mergeableState: data.mergeable_state, + }; +} + +function mapReviewComment(data: IGitHubReviewCommentResponse): IGitHubPRComment { + return { + id: data.id, + body: data.body, + author: mapUser(data.user), + createdAt: data.created_at, + updatedAt: data.updated_at, + path: data.path, + line: data.line ?? data.original_line ?? undefined, + threadId: String(data.in_reply_to_id ?? data.id), + inReplyToId: data.in_reply_to_id, + }; +} + +function mapReviewThread(thread: IGitHubGraphQLReviewThreadNode): IGitHubPRReviewThread { + return { + id: thread.id, + isResolved: thread.isResolved, + path: thread.path, + line: thread.line ?? undefined, + comments: thread.comments.nodes.flatMap(comment => mapGraphQLReviewComment(comment, thread)), + }; +} + +function mapGraphQLReviewComment(comment: IGitHubGraphQLReviewCommentNode, thread: IGitHubGraphQLReviewThreadNode): readonly IGitHubPRComment[] { + if (comment.databaseId === null || comment.author === null) { + return []; + } + + return [{ + id: comment.databaseId, + body: comment.body, + author: { login: comment.author.login, avatarUrl: comment.author.avatarUrl }, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + path: comment.path ?? thread.path, + line: comment.line ?? comment.originalLine ?? thread.line ?? undefined, + threadId: thread.id, + inReplyToId: comment.replyTo?.databaseId ?? undefined, + }]; +} + +//#endregion diff --git a/src/vs/sessions/contrib/github/browser/fetchers/githubRepositoryFetcher.ts b/src/vs/sessions/contrib/github/browser/fetchers/githubRepositoryFetcher.ts new file mode 100644 index 00000000000..2b57fbd3db3 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/fetchers/githubRepositoryFetcher.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IGitHubRepository } from '../../common/types.js'; +import { GitHubApiClient } from '../githubApiClient.js'; + +interface IGitHubRepoResponse { + readonly name: string; + readonly full_name: string; + readonly owner: { readonly login: string }; + readonly default_branch: string; + readonly private: boolean; + readonly description: string | null; +} + +/** + * Stateless fetcher for GitHub repository data. + * All methods return raw typed data with no caching or state. + */ +export class GitHubRepositoryFetcher { + + constructor( + private readonly _apiClient: GitHubApiClient, + ) { } + + async getRepository(owner: string, repo: string): Promise { + const data = await this._apiClient.request( + 'GET', + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, + ); + return { + owner: data.owner.login, + name: data.name, + fullName: data.full_name, + defaultBranch: data.default_branch, + isPrivate: data.private, + description: data.description ?? '', + }; + } +} diff --git a/src/vs/sessions/contrib/github/browser/github.contribution.ts b/src/vs/sessions/contrib/github/browser/github.contribution.ts new file mode 100644 index 00000000000..af80df37852 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/github.contribution.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { GitHubService, IGitHubService } from './githubService.js'; + +registerSingleton(IGitHubService, GitHubService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/github/browser/githubApiClient.ts b/src/vs/sessions/contrib/github/browser/githubApiClient.ts new file mode 100644 index 00000000000..e5279b02196 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/githubApiClient.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 { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IRequestService, asJson } from '../../../../platform/request/common/request.js'; +import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js'; + +const LOG_PREFIX = '[GitHubApiClient]'; +const GITHUB_API_BASE = 'https://api.github.com'; +const GITHUB_GRAPHQL_ENDPOINT = `${GITHUB_API_BASE}/graphql`; + +interface IGitHubGraphQLError { + readonly message: string; +} + +interface IGitHubGraphQLResponse { + readonly data?: T; + readonly errors?: readonly IGitHubGraphQLError[]; +} + +export class GitHubApiError extends Error { + constructor( + message: string, + readonly statusCode: number, + readonly rateLimitRemaining: number | undefined, + ) { + super(message); + this.name = 'GitHubApiError'; + } +} + +/** + * Low-level GitHub REST API client. Handles authentication, + * request construction, and error classification. + * + * This class is stateless with respect to domain data — it only + * manages auth tokens and raw HTTP communication. + */ +export class GitHubApiClient extends Disposable { + + constructor( + @IRequestService private readonly _requestService: IRequestService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + } + + async request(method: string, path: string, body?: unknown): Promise { + return this._request(method, `${GITHUB_API_BASE}${path}`, path, 'application/vnd.github.v3+json', body); + } + + async graphql(query: string, variables?: Record): Promise { + const response = await this._request>( + 'POST', + GITHUB_GRAPHQL_ENDPOINT, + '/graphql', + 'application/vnd.github+json', + { query, variables }, + ); + + if (response.errors?.length) { + throw new GitHubApiError( + response.errors.map(error => error.message).join('; '), + 200, + undefined, + ); + } + + if (!response.data) { + throw new GitHubApiError('GitHub GraphQL response did not include data', 200, undefined); + } + + return response.data; + } + + private async _request(method: string, url: string, pathForLogging: string, accept: string, body?: unknown): Promise { + const token = await this._getAuthToken(); + + this._logService.trace(`${LOG_PREFIX} ${method} ${pathForLogging}`); + + const response = await this._requestService.request({ + type: method, + url, + headers: { + 'Authorization': `token ${token}`, + 'Accept': accept, + 'User-Agent': 'VSCode-Sessions-GitHub', + ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}), + }, + data: body !== undefined ? JSON.stringify(body) : undefined, + }, CancellationToken.None); + + const rateLimitRemaining = parseRateLimitHeader(response.res.headers?.['x-ratelimit-remaining']); + if (rateLimitRemaining !== undefined && rateLimitRemaining < 100) { + this._logService.warn(`${LOG_PREFIX} GitHub API rate limit low: ${rateLimitRemaining} remaining`); + } + + const statusCode = response.res.statusCode ?? 0; + if (statusCode < 200 || statusCode >= 300) { + const errorBody = await asJson<{ message?: string }>(response).catch(() => undefined); + throw new GitHubApiError( + errorBody?.message ?? `GitHub API request failed: ${method} ${pathForLogging} (${statusCode})`, + statusCode, + rateLimitRemaining, + ); + } + + if (statusCode === 204) { + return undefined as unknown as T; + } + + const data = await asJson(response); + if (!data) { + throw new GitHubApiError( + `Failed to parse response for ${method} ${pathForLogging}`, + statusCode, + rateLimitRemaining, + ); + } + + return data; + } + + private async _getAuthToken(): Promise { + let sessions = await this._authenticationService.getSessions('github', [], { silent: true }); + if (!sessions || sessions.length === 0) { + sessions = await this._authenticationService.getSessions('github', [], { createIfNone: true }); + } + if (!sessions || sessions.length === 0) { + throw new Error('No GitHub authentication sessions available'); + } + return sessions[0].accessToken ?? ''; + } +} + +function parseRateLimitHeader(value: string | string[] | undefined): number | undefined { + if (value === undefined) { + return undefined; + } + const str = Array.isArray(value) ? value[0] : value; + const parsed = parseInt(str, 10); + return isNaN(parsed) ? undefined : parsed; +} diff --git a/src/vs/sessions/contrib/github/browser/githubService.ts b/src/vs/sessions/contrib/github/browser/githubService.ts new file mode 100644 index 00000000000..ac6a5ab7de8 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/githubService.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; +import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { GitHubApiClient } from './githubApiClient.js'; +import { GitHubRepositoryFetcher } from './fetchers/githubRepositoryFetcher.js'; +import { GitHubPRFetcher } from './fetchers/githubPRFetcher.js'; +import { GitHubPRCIFetcher } from './fetchers/githubPRCIFetcher.js'; +import { GitHubRepositoryModel } from './models/githubRepositoryModel.js'; +import { GitHubPullRequestModel } from './models/githubPullRequestModel.js'; +import { GitHubPullRequestCIModel } from './models/githubPullRequestCIModel.js'; + +export interface IGitHubService { + readonly _serviceBrand: undefined; + + /** + * Get or create a reactive model for a GitHub repository. + * The model is cached by owner/repo key and disposed when the service is disposed. + */ + getRepository(owner: string, repo: string): GitHubRepositoryModel; + + /** + * Get or create a reactive model for a GitHub pull request. + * The model is cached by owner/repo/prNumber key and disposed when the service is disposed. + */ + getPullRequest(owner: string, repo: string, prNumber: number): GitHubPullRequestModel; + + /** + * Get or create a reactive model for CI checks on a pull request head ref. + * The model is cached by owner/repo/headRef key and disposed when the service is disposed. + */ + getPullRequestCI(owner: string, repo: string, headRef: string): GitHubPullRequestCIModel; +} + +export const IGitHubService = createDecorator('sessionsGitHubService'); + +const LOG_PREFIX = '[GitHubService]'; + +export class GitHubService extends Disposable implements IGitHubService { + + declare readonly _serviceBrand: undefined; + + private readonly _apiClient: GitHubApiClient; + private readonly _repoFetcher: GitHubRepositoryFetcher; + private readonly _prFetcher: GitHubPRFetcher; + private readonly _ciFetcher: GitHubPRCIFetcher; + + private readonly _repositories = this._register(new DisposableMap()); + private readonly _pullRequests = this._register(new DisposableMap()); + private readonly _ciModels = this._register(new DisposableMap()); + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + this._apiClient = this._register(instantiationService.createInstance(GitHubApiClient)); + this._repoFetcher = new GitHubRepositoryFetcher(this._apiClient); + this._prFetcher = new GitHubPRFetcher(this._apiClient); + this._ciFetcher = new GitHubPRCIFetcher(this._apiClient); + } + + getRepository(owner: string, repo: string): GitHubRepositoryModel { + const key = `${owner}/${repo}`; + let model = this._repositories.get(key); + if (!model) { + this._logService.trace(`${LOG_PREFIX} Creating repository model for ${key}`); + model = new GitHubRepositoryModel(owner, repo, this._repoFetcher, this._logService); + this._repositories.set(key, model); + } + return model; + } + + getPullRequest(owner: string, repo: string, prNumber: number): GitHubPullRequestModel { + const key = `${owner}/${repo}/${prNumber}`; + let model = this._pullRequests.get(key); + if (!model) { + this._logService.trace(`${LOG_PREFIX} Creating PR model for ${key}`); + model = new GitHubPullRequestModel(owner, repo, prNumber, this._prFetcher, this._logService); + this._pullRequests.set(key, model); + } + return model; + } + + getPullRequestCI(owner: string, repo: string, headRef: string): GitHubPullRequestCIModel { + const key = `${owner}/${repo}/${headRef}`; + let model = this._ciModels.get(key); + if (!model) { + this._logService.trace(`${LOG_PREFIX} Creating CI model for ${key}`); + model = new GitHubPullRequestCIModel(owner, repo, headRef, this._ciFetcher, this._logService); + this._ciModels.set(key, model); + } + return model; + } +} diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts new file mode 100644 index 00000000000..6a1dd490aaf --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { GitHubCIOverallStatus, IGitHubCICheck } from '../../common/types.js'; +import { computeOverallCIStatus, GitHubPRCIFetcher } from '../fetchers/githubPRCIFetcher.js'; + +const LOG_PREFIX = '[GitHubPullRequestCIModel]'; +const DEFAULT_POLL_INTERVAL_MS = 60_000; + +/** + * Reactive model for CI check status on a pull request head ref. + * Wraps fetcher data in observables and supports periodic polling. + */ +export class GitHubPullRequestCIModel extends Disposable { + + private readonly _checks = observableValue(this, []); + readonly checks: IObservable = this._checks; + + private readonly _overallStatus = observableValue(this, GitHubCIOverallStatus.Neutral); + readonly overallStatus: IObservable = this._overallStatus; + + private readonly _pollScheduler: RunOnceScheduler; + private _disposed = false; + + constructor( + readonly owner: string, + readonly repo: string, + readonly headRef: string, + private readonly _fetcher: GitHubPRCIFetcher, + private readonly _logService: ILogService, + ) { + super(); + + this._pollScheduler = this._register(new RunOnceScheduler(() => this._poll(), DEFAULT_POLL_INTERVAL_MS)); + } + + /** + * Refresh all CI check data. + */ + async refresh(): Promise { + try { + const checks = await this._fetcher.getCheckRuns(this.owner, this.repo, this.headRef); + this._checks.set(checks, undefined); + this._overallStatus.set(computeOverallCIStatus(checks), undefined); + } catch (err) { + this._logService.error(`${LOG_PREFIX} Failed to refresh CI checks for ${this.owner}/${this.repo}@${this.headRef}:`, err); + } + } + + /** + * Get annotations (structured logs) for a specific check run. + */ + async getCheckRunAnnotations(checkRunId: number): Promise { + return this._fetcher.getCheckRunAnnotations(this.owner, this.repo, checkRunId); + } + + /** + * Start periodic polling. Each cycle refreshes CI check data. + */ + startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): void { + this._pollScheduler.cancel(); + this._pollScheduler.schedule(intervalMs); + } + + /** + * Stop periodic polling. + */ + stopPolling(): void { + this._pollScheduler.cancel(); + } + + private async _poll(): Promise { + await this.refresh(); + // Re-schedule if not disposed (RunOnceScheduler is one-shot) + if (!this._disposed) { + this._pollScheduler.schedule(); + } + } + + override dispose(): void { + this._disposed = true; + super.dispose(); + } +} diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts new file mode 100644 index 00000000000..8c5a667460c --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IGitHubPRComment, IGitHubPRReviewThread, IGitHubPullRequest, IGitHubPullRequestMergeability } from '../../common/types.js'; +import { GitHubPRFetcher } from '../fetchers/githubPRFetcher.js'; + +const LOG_PREFIX = '[GitHubPullRequestModel]'; +const DEFAULT_POLL_INTERVAL_MS = 30_000; + +/** + * Reactive model for a GitHub pull request. Wraps fetcher data in + * observables, supports on-demand refresh, and can poll periodically. + */ +export class GitHubPullRequestModel extends Disposable { + + private readonly _pullRequest = observableValue(this, undefined); + readonly pullRequest: IObservable = this._pullRequest; + + private readonly _mergeability = observableValue(this, undefined); + readonly mergeability: IObservable = this._mergeability; + + private readonly _reviewThreads = observableValue(this, []); + readonly reviewThreads: IObservable = this._reviewThreads; + + private readonly _pollScheduler: RunOnceScheduler; + private _disposed = false; + + constructor( + readonly owner: string, + readonly repo: string, + readonly prNumber: number, + private readonly _fetcher: GitHubPRFetcher, + private readonly _logService: ILogService, + ) { + super(); + + this._pollScheduler = this._register(new RunOnceScheduler(() => this._poll(), DEFAULT_POLL_INTERVAL_MS)); + } + + /** + * Refresh all PR data: pull request info, mergeability, and review threads. + */ + async refresh(): Promise { + await Promise.all([ + this._refreshPullRequest(), + this._refreshMergeability(), + this._refreshThreads(), + ]); + } + + /** + * Refresh only the review threads. + */ + async refreshThreads(): Promise { + await this._refreshThreads(); + } + + /** + * Post a reply to an existing review thread and refresh threads. + */ + async postReviewComment(body: string, inReplyTo: number): Promise { + const comment = await this._fetcher.postReviewComment(this.owner, this.repo, this.prNumber, body, inReplyTo); + await this._refreshThreads(); + return comment; + } + + /** + * Post a top-level issue comment on the PR. + */ + async postIssueComment(body: string): Promise { + return this._fetcher.postIssueComment(this.owner, this.repo, this.prNumber, body); + } + + /** + * Resolve a review thread and refresh the thread list. + */ + async resolveThread(threadId: string): Promise { + await this._fetcher.resolveThread(this.owner, this.repo, threadId); + await this._refreshThreads(); + } + + /** + * Start periodic polling. Each cycle refreshes all PR data. + */ + startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): void { + this._pollScheduler.cancel(); + this._pollScheduler.schedule(intervalMs); + } + + /** + * Stop periodic polling. + */ + stopPolling(): void { + this._pollScheduler.cancel(); + } + + private async _poll(): Promise { + await this.refresh(); + // Re-schedule for next poll cycle (RunOnceScheduler is one-shot) + if (!this._disposed) { + this._pollScheduler.schedule(); + } + } + + override dispose(): void { + this._disposed = true; + super.dispose(); + } + + private async _refreshPullRequest(): Promise { + try { + const data = await this._fetcher.getPullRequest(this.owner, this.repo, this.prNumber); + this._pullRequest.set(data, undefined); + } catch (err) { + this._logService.error(`${LOG_PREFIX} Failed to refresh PR #${this.prNumber}:`, err); + } + } + + private async _refreshMergeability(): Promise { + try { + const data = await this._fetcher.getMergeability(this.owner, this.repo, this.prNumber); + this._mergeability.set(data, undefined); + } catch (err) { + this._logService.error(`${LOG_PREFIX} Failed to refresh mergeability for PR #${this.prNumber}:`, err); + } + } + + private async _refreshThreads(): Promise { + try { + const data = await this._fetcher.getReviewThreads(this.owner, this.repo, this.prNumber); + this._reviewThreads.set(data, undefined); + } catch (err) { + this._logService.error(`${LOG_PREFIX} Failed to refresh threads for PR #${this.prNumber}:`, err); + } + } +} diff --git a/src/vs/sessions/contrib/github/browser/models/githubRepositoryModel.ts b/src/vs/sessions/contrib/github/browser/models/githubRepositoryModel.ts new file mode 100644 index 00000000000..9e2c368a329 --- /dev/null +++ b/src/vs/sessions/contrib/github/browser/models/githubRepositoryModel.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IGitHubRepository } from '../../common/types.js'; +import { GitHubRepositoryFetcher } from '../fetchers/githubRepositoryFetcher.js'; + +const LOG_PREFIX = '[GitHubRepositoryModel]'; + +/** + * Reactive model for a GitHub repository. Wraps fetcher data + * in observables and supports on-demand refresh. + */ +export class GitHubRepositoryModel extends Disposable { + + private readonly _repository = observableValue(this, undefined); + readonly repository: IObservable = this._repository; + + constructor( + readonly owner: string, + readonly repo: string, + private readonly _fetcher: GitHubRepositoryFetcher, + private readonly _logService: ILogService, + ) { + super(); + } + + async refresh(): Promise { + try { + const data = await this._fetcher.getRepository(this.owner, this.repo); + this._repository.set(data, undefined); + } catch (err) { + this._logService.error(`${LOG_PREFIX} Failed to refresh repository ${this.owner}/${this.repo}:`, err); + } + } +} diff --git a/src/vs/sessions/contrib/github/common/types.ts b/src/vs/sessions/contrib/github/common/types.ts new file mode 100644 index 00000000000..bc447c21c90 --- /dev/null +++ b/src/vs/sessions/contrib/github/common/types.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//#region Session Context + +/** + * GitHub context derived from an active session, providing + * the owner/repo and optionally the PR number. + */ +export interface IGitHubSessionContext { + readonly owner: string; + readonly repo: string; + readonly prNumber: number | undefined; +} + +//#endregion + +//#region Repository + +export interface IGitHubRepository { + readonly owner: string; + readonly name: string; + readonly fullName: string; + readonly defaultBranch: string; + readonly isPrivate: boolean; + readonly description: string; +} + +//#endregion + +//#region Pull Request + +export const enum GitHubPullRequestState { + Open = 'open', + Closed = 'closed', + Merged = 'merged', +} + +export interface IGitHubUser { + readonly login: string; + readonly avatarUrl: string; +} + +export interface IGitHubPullRequest { + readonly number: number; + readonly title: string; + readonly body: string; + readonly state: GitHubPullRequestState; + readonly author: IGitHubUser; + readonly headRef: string; + readonly baseRef: string; + readonly isDraft: boolean; + readonly createdAt: string; + readonly updatedAt: string; + readonly mergedAt: string | undefined; + readonly mergeable: boolean | undefined; + readonly mergeableState: string; +} + +export const enum MergeBlockerKind { + ChangesRequested = 'changesRequested', + CIFailed = 'ciFailed', + ApprovalNeeded = 'approvalNeeded', + Conflicts = 'conflicts', + Draft = 'draft', + Unknown = 'unknown', +} + +export interface IMergeBlocker { + readonly kind: MergeBlockerKind; + readonly description: string; +} + +export interface IGitHubPullRequestMergeability { + readonly canMerge: boolean; + readonly blockers: readonly IMergeBlocker[]; +} + +//#endregion + +//#region Review Comments & Threads + +export interface IGitHubPRComment { + readonly id: number; + readonly body: string; + readonly author: IGitHubUser; + readonly createdAt: string; + readonly updatedAt: string; + /** File path the comment is attached to (undefined for issue-level comments). */ + readonly path: string | undefined; + /** Line number in the diff the comment is attached to. */ + readonly line: number | undefined; + /** The id of the thread this comment belongs to. */ + readonly threadId: string; + /** Whether this is a reply to another comment in the thread. */ + readonly inReplyToId: number | undefined; +} + +export interface IGitHubPRReviewThread { + readonly id: string; + readonly isResolved: boolean; + readonly path: string; + readonly line: number | undefined; + readonly comments: readonly IGitHubPRComment[]; +} + +//#endregion + +//#region CI Checks + +export const enum GitHubCheckStatus { + Queued = 'queued', + InProgress = 'in_progress', + Completed = 'completed', +} + +export const enum GitHubCheckConclusion { + Success = 'success', + Failure = 'failure', + Neutral = 'neutral', + Cancelled = 'cancelled', + Skipped = 'skipped', + TimedOut = 'timed_out', + ActionRequired = 'action_required', + Stale = 'stale', +} + +export interface IGitHubCICheck { + readonly id: number; + readonly name: string; + readonly status: GitHubCheckStatus; + readonly conclusion: GitHubCheckConclusion | undefined; + readonly startedAt: string | undefined; + readonly completedAt: string | undefined; + readonly detailsUrl: string | undefined; +} + +export const enum GitHubCIOverallStatus { + Pending = 'pending', + Success = 'success', + Failure = 'failure', + Neutral = 'neutral', +} + +//#endregion diff --git a/src/vs/sessions/contrib/github/test/browser/githubFetchers.test.ts b/src/vs/sessions/contrib/github/test/browser/githubFetchers.test.ts new file mode 100644 index 00000000000..baa4318b991 --- /dev/null +++ b/src/vs/sessions/contrib/github/test/browser/githubFetchers.test.ts @@ -0,0 +1,446 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { GitHubPRFetcher } from '../../browser/fetchers/githubPRFetcher.js'; +import { GitHubPRCIFetcher, computeOverallCIStatus } from '../../browser/fetchers/githubPRCIFetcher.js'; +import { GitHubRepositoryFetcher } from '../../browser/fetchers/githubRepositoryFetcher.js'; +import { GitHubApiClient, GitHubApiError } from '../../browser/githubApiClient.js'; +import { GitHubCheckConclusion, GitHubCheckStatus, GitHubCIOverallStatus, GitHubPullRequestState, MergeBlockerKind } from '../../common/types.js'; + +class MockApiClient { + + private _nextResponse: unknown; + private _nextError: Error | undefined; + readonly requestCalls: { method: string; path: string; body?: unknown }[] = []; + readonly graphqlCalls: { query: string; variables?: Record }[] = []; + + setNextResponse(data: unknown): void { + this._nextResponse = data; + this._nextError = undefined; + } + + setNextError(error: Error): void { + this._nextError = error; + this._nextResponse = undefined; + } + + async request(_method: string, _path: string, _body?: unknown): Promise { + this.requestCalls.push({ method: _method, path: _path, body: _body }); + if (this._nextError) { + throw this._nextError; + } + return this._nextResponse as T; + } + + async graphql(query: string, variables?: Record): Promise { + this.graphqlCalls.push({ query, variables }); + if (this._nextError) { + throw this._nextError; + } + return this._nextResponse as T; + } +} + +suite('GitHubRepositoryFetcher', () => { + + const store = new DisposableStore(); + let mockApi: MockApiClient; + let fetcher: GitHubRepositoryFetcher; + + setup(() => { + mockApi = new MockApiClient(); + fetcher = new GitHubRepositoryFetcher(mockApi as unknown as GitHubApiClient); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('getRepository returns mapped data', async () => { + mockApi.setNextResponse({ + name: 'vscode', + full_name: 'microsoft/vscode', + owner: { login: 'microsoft' }, + default_branch: 'main', + private: false, + description: 'Visual Studio Code', + }); + + const repo = await fetcher.getRepository('microsoft', 'vscode'); + assert.deepStrictEqual(repo, { + owner: 'microsoft', + name: 'vscode', + fullName: 'microsoft/vscode', + defaultBranch: 'main', + isPrivate: false, + description: 'Visual Studio Code', + }); + assert.strictEqual(mockApi.requestCalls[0].path, '/repos/microsoft/vscode'); + }); + + test('getRepository handles null description', async () => { + mockApi.setNextResponse({ + name: 'test', + full_name: 'owner/test', + owner: { login: 'owner' }, + default_branch: 'main', + private: true, + description: null, + }); + + const repo = await fetcher.getRepository('owner', 'test'); + assert.strictEqual(repo.description, ''); + }); + + test('getRepository propagates API errors', async () => { + mockApi.setNextError(new GitHubApiError('Not found', 404, undefined)); + await assert.rejects( + () => fetcher.getRepository('owner', 'nonexistent'), + (err: Error) => err instanceof GitHubApiError && (err as GitHubApiError).statusCode === 404, + ); + }); +}); + +suite('GitHubPRFetcher', () => { + + const store = new DisposableStore(); + let mockApi: MockApiClient; + let fetcher: GitHubPRFetcher; + + setup(() => { + mockApi = new MockApiClient(); + fetcher = new GitHubPRFetcher(mockApi as unknown as GitHubApiClient); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('getPullRequest maps open PR', async () => { + mockApi.setNextResponse(makePRResponse({ state: 'open', merged: false, draft: false })); + + const pr = await fetcher.getPullRequest('owner', 'repo', 1); + assert.strictEqual(pr.state, GitHubPullRequestState.Open); + assert.strictEqual(pr.isDraft, false); + assert.strictEqual(pr.number, 1); + assert.strictEqual(pr.title, 'Test PR'); + }); + + test('getPullRequest maps merged PR', async () => { + mockApi.setNextResponse(makePRResponse({ state: 'closed', merged: true, draft: false })); + + const pr = await fetcher.getPullRequest('owner', 'repo', 1); + assert.strictEqual(pr.state, GitHubPullRequestState.Merged); + assert.ok(pr.mergedAt); + }); + + test('getPullRequest maps closed PR', async () => { + mockApi.setNextResponse(makePRResponse({ state: 'closed', merged: false, draft: false })); + + const pr = await fetcher.getPullRequest('owner', 'repo', 1); + assert.strictEqual(pr.state, GitHubPullRequestState.Closed); + }); + + test('getReviewThreads returns GraphQL thread metadata', async () => { + mockApi.setNextResponse(makeGraphQLReviewThreadsResponse([ + makeGraphQLReviewThread({ + id: 'thread-a', + path: 'src/a.ts', + line: 10, + isResolved: false, + comments: [ + makeGraphQLReviewComment({ databaseId: 100, path: 'src/a.ts', line: 10 }), + makeGraphQLReviewComment({ databaseId: 101, path: 'src/a.ts', line: 10, replyToDatabaseId: 100 }), + ], + }), + makeGraphQLReviewThread({ + id: 'thread-b', + path: 'src/b.ts', + line: 20, + isResolved: true, + comments: [makeGraphQLReviewComment({ databaseId: 200, path: 'src/b.ts', line: 20 })], + }), + ])); + + const threads = await fetcher.getReviewThreads('owner', 'repo', 1); + assert.strictEqual(threads.length, 2); + + const thread1 = threads.find(t => t.id === 'thread-a')!; + assert.ok(thread1); + assert.strictEqual(thread1.comments.length, 2); + assert.strictEqual(thread1.path, 'src/a.ts'); + assert.strictEqual(thread1.line, 10); + assert.strictEqual(thread1.comments[0].threadId, 'thread-a'); + + const thread2 = threads.find(t => t.id === 'thread-b')!; + assert.ok(thread2); + assert.strictEqual(thread2.comments.length, 1); + assert.strictEqual(thread2.path, 'src/b.ts'); + assert.strictEqual(thread2.isResolved, true); + }); + + test('resolveThread uses GraphQL mutation', async () => { + mockApi.setNextResponse({ + resolveReviewThread: { + thread: { + isResolved: true, + }, + }, + }); + + await fetcher.resolveThread('owner', 'repo', 'thread-a'); + assert.strictEqual(mockApi.graphqlCalls.length, 1); + assert.deepStrictEqual(mockApi.graphqlCalls[0].variables, { threadId: 'thread-a' }); + }); + + test('getMergeability detects draft blocker', async () => { + // getMergeability makes two requests (PR then reviews) + // Use a counter to return different responses + let callCount = 0; + const originalRequest = mockApi.request.bind(mockApi); + mockApi.request = async function (_method: string, _path: string, _body?: unknown): Promise { + if (callCount++ === 0) { + return makePRResponse({ state: 'open', merged: false, draft: true, mergeable: true, mergeable_state: 'clean' }) as T; + } + return [] as unknown as T; + }; + + const result = await fetcher.getMergeability('owner', 'repo', 1); + assert.strictEqual(result.canMerge, false); + assert.ok(result.blockers.some(b => b.kind === MergeBlockerKind.Draft)); + + // Restore + mockApi.request = originalRequest; + }); + + test('getMergeability detects conflicts blocker', async () => { + let callCount = 0; + const originalRequest = mockApi.request.bind(mockApi); + mockApi.request = async function (): Promise { + if (callCount++ === 0) { + return makePRResponse({ state: 'open', merged: false, draft: false, mergeable: false, mergeable_state: 'dirty' }) as T; + } + return [] as unknown as T; + }; + + const result = await fetcher.getMergeability('owner', 'repo', 1); + assert.strictEqual(result.canMerge, false); + assert.ok(result.blockers.some(b => b.kind === MergeBlockerKind.Conflicts)); + + mockApi.request = originalRequest; + }); + + test('getMergeability detects changes requested blocker', async () => { + let callCount = 0; + const originalRequest = mockApi.request.bind(mockApi); + mockApi.request = async function (): Promise { + if (callCount++ === 0) { + return makePRResponse({ state: 'open', merged: false, draft: false, mergeable: true, mergeable_state: 'clean' }) as T; + } + return [ + { id: 1, user: { login: 'reviewer', avatar_url: '' }, state: 'CHANGES_REQUESTED', submitted_at: '2024-01-01T00:00:00Z' }, + ] as unknown as T; + }; + + const result = await fetcher.getMergeability('owner', 'repo', 1); + assert.strictEqual(result.canMerge, false); + assert.ok(result.blockers.some(b => b.kind === MergeBlockerKind.ChangesRequested)); + + mockApi.request = originalRequest; + }); +}); + +suite('GitHubPRCIFetcher', () => { + + const store = new DisposableStore(); + let mockApi: MockApiClient; + let fetcher: GitHubPRCIFetcher; + + setup(() => { + mockApi = new MockApiClient(); + fetcher = new GitHubPRCIFetcher(mockApi as unknown as GitHubApiClient); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('getCheckRuns maps check runs', async () => { + mockApi.setNextResponse({ + total_count: 2, + check_runs: [ + { id: 1, name: 'build', status: 'completed', conclusion: 'success', started_at: '2024-01-01T00:00:00Z', completed_at: '2024-01-01T00:10:00Z', details_url: 'https://example.com/1' }, + { id: 2, name: 'test', status: 'in_progress', conclusion: null, started_at: '2024-01-01T00:00:00Z', completed_at: null, details_url: null }, + ], + }); + + const checks = await fetcher.getCheckRuns('owner', 'repo', 'abc123'); + assert.strictEqual(checks.length, 2); + assert.deepStrictEqual(checks[0], { + id: 1, + name: 'build', + status: GitHubCheckStatus.Completed, + conclusion: GitHubCheckConclusion.Success, + startedAt: '2024-01-01T00:00:00Z', + completedAt: '2024-01-01T00:10:00Z', + detailsUrl: 'https://example.com/1', + }); + assert.strictEqual(checks[1].conclusion, undefined); + }); + + test('getCheckRunAnnotations returns formatted annotations', async () => { + mockApi.setNextResponse([ + { path: 'src/a.ts', start_line: 10, end_line: 10, annotation_level: 'failure', message: 'type error', title: 'TS2345' }, + { path: 'src/b.ts', start_line: 5, end_line: 8, annotation_level: 'warning', message: 'unused var', title: null }, + ]); + + const result = await fetcher.getCheckRunAnnotations('owner', 'repo', 1); + assert.ok(result.includes('[failure] src/a.ts:10')); + assert.ok(result.includes('(TS2345)')); + assert.ok(result.includes('[warning] src/b.ts:5-8')); + }); +}); + +suite('computeOverallCIStatus', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('returns neutral for empty checks', () => { + assert.strictEqual(computeOverallCIStatus([]), GitHubCIOverallStatus.Neutral); + }); + + test('returns success when all completed successfully', () => { + const checks = [ + makeCheck({ status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Success }), + makeCheck({ status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Neutral }), + ]; + assert.strictEqual(computeOverallCIStatus(checks), GitHubCIOverallStatus.Success); + }); + + test('returns failure when any check failed', () => { + const checks = [ + makeCheck({ status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Success }), + makeCheck({ status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Failure }), + ]; + assert.strictEqual(computeOverallCIStatus(checks), GitHubCIOverallStatus.Failure); + }); + + test('returns pending when any check is in progress', () => { + const checks = [ + makeCheck({ status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Success }), + makeCheck({ status: GitHubCheckStatus.InProgress, conclusion: undefined }), + ]; + assert.strictEqual(computeOverallCIStatus(checks), GitHubCIOverallStatus.Pending); + }); + + test('failure takes precedence over pending', () => { + const checks = [ + makeCheck({ status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Failure }), + makeCheck({ status: GitHubCheckStatus.InProgress, conclusion: undefined }), + ]; + assert.strictEqual(computeOverallCIStatus(checks), GitHubCIOverallStatus.Failure); + }); +}); + + +//#region Test Helpers + +function makePRResponse(overrides: { + state: 'open' | 'closed'; + merged: boolean; + draft: boolean; + mergeable?: boolean | null; + mergeable_state?: string; +}): unknown { + return { + number: 1, + title: 'Test PR', + body: 'Test body', + state: overrides.state, + draft: overrides.draft, + user: { login: 'author', avatar_url: 'https://example.com/avatar' }, + head: { ref: 'feature-branch' }, + base: { ref: 'main' }, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-02T00:00:00Z', + merged_at: overrides.merged ? '2024-01-02T00:00:00Z' : null, + mergeable: overrides.mergeable ?? true, + mergeable_state: overrides.mergeable_state ?? 'clean', + merged: overrides.merged, + }; +} + +function makeGraphQLReviewThreadsResponse(threads: readonly ReturnType[]): unknown { + return { + repository: { + pullRequest: { + reviewThreads: { + nodes: threads, + }, + }, + }, + }; +} + +function makeGraphQLReviewThread(overrides: Partial<{ + id: string; + isResolved: boolean; + path: string; + line: number; + comments: readonly ReturnType[]; +}> = {}): unknown { + return { + id: overrides.id ?? 'thread-1', + isResolved: overrides.isResolved ?? false, + path: overrides.path ?? 'src/a.ts', + line: overrides.line ?? 10, + comments: { + nodes: overrides.comments ?? [makeGraphQLReviewComment()], + }, + }; +} + +function makeGraphQLReviewComment(overrides: Partial<{ + databaseId: number; + body: string; + path: string; + line: number; + replyToDatabaseId: number; +}> = {}): unknown { + return { + databaseId: overrides.databaseId ?? 100, + body: overrides.body ?? 'Test comment', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + path: overrides.path ?? 'src/a.ts', + line: overrides.line ?? 10, + originalLine: overrides.line ?? 10, + replyTo: overrides.replyToDatabaseId !== undefined ? { databaseId: overrides.replyToDatabaseId } : null, + author: { + login: 'reviewer', + avatarUrl: 'https://example.com/avatar', + }, + }; +} + +function makeCheck(overrides: { + status: GitHubCheckStatus; + conclusion: GitHubCheckConclusion | undefined; +}): { id: number; name: string; status: GitHubCheckStatus; conclusion: GitHubCheckConclusion | undefined; startedAt: string | undefined; completedAt: string | undefined; detailsUrl: string | undefined } { + return { + id: 1, + name: 'test-check', + status: overrides.status, + conclusion: overrides.conclusion, + startedAt: undefined, + completedAt: undefined, + detailsUrl: undefined, + }; +} + +//#endregion diff --git a/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts b/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts new file mode 100644 index 00000000000..24ce18ead0b --- /dev/null +++ b/src/vs/sessions/contrib/github/test/browser/githubModels.test.ts @@ -0,0 +1,279 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { GitHubPullRequestModel } from '../../browser/models/githubPullRequestModel.js'; +import { GitHubPullRequestCIModel } from '../../browser/models/githubPullRequestCIModel.js'; +import { GitHubRepositoryModel } from '../../browser/models/githubRepositoryModel.js'; +import { GitHubPRFetcher } from '../../browser/fetchers/githubPRFetcher.js'; +import { GitHubPRCIFetcher } from '../../browser/fetchers/githubPRCIFetcher.js'; +import { GitHubRepositoryFetcher } from '../../browser/fetchers/githubRepositoryFetcher.js'; +import { GitHubCIOverallStatus, GitHubCheckConclusion, GitHubCheckStatus, GitHubPullRequestState, IGitHubCICheck, IGitHubPRComment, IGitHubPRReviewThread, IGitHubPullRequest, IGitHubPullRequestMergeability, IGitHubRepository } from '../../common/types.js'; + +//#region Mock Fetchers + +class MockRepositoryFetcher { + nextResult: IGitHubRepository | undefined; + + async getRepository(_owner: string, _repo: string): Promise { + if (!this.nextResult) { + throw new Error('No mock result'); + } + return this.nextResult; + } +} + +class MockPRFetcher { + nextPR: IGitHubPullRequest | undefined; + nextMergeability: IGitHubPullRequestMergeability | undefined; + nextThreads: IGitHubPRReviewThread[] = []; + postReviewCommentCalls: { body: string; inReplyTo: number }[] = []; + postIssueCommentCalls: { body: string }[] = []; + + async getPullRequest(_owner: string, _repo: string, _prNumber: number): Promise { + if (!this.nextPR) { + throw new Error('No mock PR'); + } + return this.nextPR; + } + + async getMergeability(_owner: string, _repo: string, _prNumber: number): Promise { + if (!this.nextMergeability) { + throw new Error('No mock mergeability'); + } + return this.nextMergeability; + } + + async getReviewThreads(_owner: string, _repo: string, _prNumber: number): Promise { + return this.nextThreads; + } + + async postReviewComment(_owner: string, _repo: string, _prNumber: number, body: string, inReplyTo: number): Promise { + this.postReviewCommentCalls.push({ body, inReplyTo }); + return makeComment(999, body); + } + + async postIssueComment(_owner: string, _repo: string, _prNumber: number, body: string): Promise { + this.postIssueCommentCalls.push({ body }); + return makeComment(998, body); + } + + async resolveThread(): Promise { + throw new Error('Not implemented'); + } +} + +class MockCIFetcher { + nextChecks: IGitHubCICheck[] = []; + + async getCheckRuns(_owner: string, _repo: string, _ref: string): Promise { + return this.nextChecks; + } + + async getCheckRunAnnotations(_owner: string, _repo: string, _checkRunId: number): Promise { + return 'mock annotations'; + } +} + +//#endregion + +suite('GitHubRepositoryModel', () => { + + const store = new DisposableStore(); + let mockFetcher: MockRepositoryFetcher; + const logService = new NullLogService(); + + setup(() => { + mockFetcher = new MockRepositoryFetcher(); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('initial state is undefined', () => { + const model = store.add(new GitHubRepositoryModel('owner', 'repo', mockFetcher as unknown as GitHubRepositoryFetcher, logService)); + assert.strictEqual(model.repository.get(), undefined); + }); + + test('refresh populates repository observable', async () => { + const model = store.add(new GitHubRepositoryModel('owner', 'repo', mockFetcher as unknown as GitHubRepositoryFetcher, logService)); + mockFetcher.nextResult = { + owner: 'owner', + name: 'repo', + fullName: 'owner/repo', + defaultBranch: 'main', + isPrivate: false, + description: 'test', + }; + + await model.refresh(); + assert.deepStrictEqual(model.repository.get(), mockFetcher.nextResult); + }); + + test('refresh handles errors gracefully', async () => { + const model = store.add(new GitHubRepositoryModel('owner', 'repo', mockFetcher as unknown as GitHubRepositoryFetcher, logService)); + // No nextResult set, will throw + await model.refresh(); + assert.strictEqual(model.repository.get(), undefined); + }); +}); + +suite('GitHubPullRequestModel', () => { + + const store = new DisposableStore(); + let mockFetcher: MockPRFetcher; + const logService = new NullLogService(); + + setup(() => { + mockFetcher = new MockPRFetcher(); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('initial state has empty observables', () => { + const model = store.add(new GitHubPullRequestModel('owner', 'repo', 1, mockFetcher as unknown as GitHubPRFetcher, logService)); + assert.strictEqual(model.pullRequest.get(), undefined); + assert.strictEqual(model.mergeability.get(), undefined); + assert.deepStrictEqual(model.reviewThreads.get(), []); + }); + + test('refresh populates all observables', async () => { + const model = store.add(new GitHubPullRequestModel('owner', 'repo', 1, mockFetcher as unknown as GitHubPRFetcher, logService)); + mockFetcher.nextPR = makePR(); + mockFetcher.nextMergeability = { canMerge: true, blockers: [] }; + mockFetcher.nextThreads = [makeThread('thread-100', 'src/a.ts')]; + + await model.refresh(); + assert.strictEqual(model.pullRequest.get()?.number, 1); + assert.strictEqual(model.mergeability.get()?.canMerge, true); + assert.strictEqual(model.reviewThreads.get().length, 1); + }); + + test('refreshThreads only updates threads', async () => { + const model = store.add(new GitHubPullRequestModel('owner', 'repo', 1, mockFetcher as unknown as GitHubPRFetcher, logService)); + mockFetcher.nextThreads = [makeThread('thread-100', 'src/a.ts'), makeThread('thread-200', 'src/b.ts')]; + + await model.refreshThreads(); + assert.strictEqual(model.pullRequest.get(), undefined); // not refreshed + assert.strictEqual(model.reviewThreads.get().length, 2); + }); + + test('postReviewComment calls fetcher and refreshes threads', async () => { + const model = store.add(new GitHubPullRequestModel('owner', 'repo', 1, mockFetcher as unknown as GitHubPRFetcher, logService)); + mockFetcher.nextThreads = []; + + const comment = await model.postReviewComment('LGTM', 100); + assert.strictEqual(comment.body, 'LGTM'); + assert.strictEqual(mockFetcher.postReviewCommentCalls.length, 1); + assert.strictEqual(mockFetcher.postReviewCommentCalls[0].body, 'LGTM'); + }); + + test('postIssueComment calls fetcher', async () => { + const model = store.add(new GitHubPullRequestModel('owner', 'repo', 1, mockFetcher as unknown as GitHubPRFetcher, logService)); + + const comment = await model.postIssueComment('Great work!'); + assert.strictEqual(comment.body, 'Great work!'); + assert.strictEqual(mockFetcher.postIssueCommentCalls.length, 1); + }); + + test('polling can be started and stopped', () => { + const model = store.add(new GitHubPullRequestModel('owner', 'repo', 1, mockFetcher as unknown as GitHubPRFetcher, logService)); + // Just ensure no errors; actual polling behavior is timer-based + model.startPolling(60_000); + model.stopPolling(); + }); +}); + +suite('GitHubPullRequestCIModel', () => { + + const store = new DisposableStore(); + let mockFetcher: MockCIFetcher; + const logService = new NullLogService(); + + setup(() => { + mockFetcher = new MockCIFetcher(); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('initial state is empty', () => { + const model = store.add(new GitHubPullRequestCIModel('owner', 'repo', 'abc', mockFetcher as unknown as GitHubPRCIFetcher, logService)); + assert.deepStrictEqual(model.checks.get(), []); + assert.strictEqual(model.overallStatus.get(), GitHubCIOverallStatus.Neutral); + }); + + test('refresh populates checks and computes overall status', async () => { + const model = store.add(new GitHubPullRequestCIModel('owner', 'repo', 'abc', mockFetcher as unknown as GitHubPRCIFetcher, logService)); + mockFetcher.nextChecks = [ + { id: 1, name: 'build', status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Success, startedAt: undefined, completedAt: undefined, detailsUrl: undefined }, + { id: 2, name: 'test', status: GitHubCheckStatus.Completed, conclusion: GitHubCheckConclusion.Failure, startedAt: undefined, completedAt: undefined, detailsUrl: undefined }, + ]; + + await model.refresh(); + assert.strictEqual(model.checks.get().length, 2); + assert.strictEqual(model.overallStatus.get(), GitHubCIOverallStatus.Failure); + }); + + test('getCheckRunAnnotations delegates to fetcher', async () => { + const model = store.add(new GitHubPullRequestCIModel('owner', 'repo', 'abc', mockFetcher as unknown as GitHubPRCIFetcher, logService)); + const result = await model.getCheckRunAnnotations(1); + assert.strictEqual(result, 'mock annotations'); + }); +}); + + +//#region Test Helpers + +function makePR(): IGitHubPullRequest { + return { + number: 1, + title: 'Test PR', + body: 'Test body', + state: GitHubPullRequestState.Open, + author: { login: 'author', avatarUrl: '' }, + headRef: 'feature', + baseRef: 'main', + isDraft: false, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + mergedAt: undefined, + mergeable: true, + mergeableState: 'clean', + }; +} + +function makeThread(id: string, path: string): IGitHubPRReviewThread { + return { + id, + isResolved: false, + path, + line: 10, + comments: [makeComment(100, `Comment on ${path}`, id)], + }; +} + +function makeComment(id: number, body: string, threadId: string = String(id)): IGitHubPRComment { + return { + id, + body, + author: { login: 'reviewer', avatarUrl: '' }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + path: undefined, + line: undefined, + threadId, + inReplyToId: undefined, + }; +} + +//#endregion diff --git a/src/vs/sessions/contrib/github/test/browser/githubService.test.ts b/src/vs/sessions/contrib/github/test/browser/githubService.test.ts new file mode 100644 index 00000000000..71f6e64130a --- /dev/null +++ b/src/vs/sessions/contrib/github/test/browser/githubService.test.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { NullLogService, ILogService } from '../../../../../platform/log/common/log.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { GitHubService } from '../../browser/githubService.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { GITHUB_REMOTE_FILE_SCHEME } from '../../../fileTreeView/browser/githubFileSystemProvider.js'; +import { IActiveSessionItem } from '../../../sessions/browser/sessionsManagementService.js'; + +suite('GitHubService', () => { + + const store = new DisposableStore(); + let service: GitHubService; + + setup(() => { + const instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(ILogService, new NullLogService()); + + service = store.add(instantiationService.createInstance(GitHubService)); + }); + + teardown(() => store.clear()); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('getRepository returns cached model for same key', () => { + const model1 = service.getRepository('owner', 'repo'); + const model2 = service.getRepository('owner', 'repo'); + assert.strictEqual(model1, model2); + }); + + test('getRepository returns different models for different repos', () => { + const model1 = service.getRepository('owner', 'repo1'); + const model2 = service.getRepository('owner', 'repo2'); + assert.notStrictEqual(model1, model2); + }); + + test('getPullRequest returns cached model for same key', () => { + const model1 = service.getPullRequest('owner', 'repo', 1); + const model2 = service.getPullRequest('owner', 'repo', 1); + assert.strictEqual(model1, model2); + }); + + test('getPullRequest returns different models for different PRs', () => { + const model1 = service.getPullRequest('owner', 'repo', 1); + const model2 = service.getPullRequest('owner', 'repo', 2); + assert.notStrictEqual(model1, model2); + }); + + test('getPullRequestCI returns cached model for same key', () => { + const model1 = service.getPullRequestCI('owner', 'repo', 'abc123'); + const model2 = service.getPullRequestCI('owner', 'repo', 'abc123'); + assert.strictEqual(model1, model2); + }); + + test('getPullRequestCI returns different models for different refs', () => { + const model1 = service.getPullRequestCI('owner', 'repo', 'abc'); + const model2 = service.getPullRequestCI('owner', 'repo', 'def'); + assert.notStrictEqual(model1, model2); + }); + + test('disposing service does not throw', () => { + service.getRepository('owner', 'repo'); + service.getPullRequest('owner', 'repo', 1); + service.getPullRequestCI('owner', 'repo', 'abc'); + + // Disposing the service should not throw and should clean up models + assert.doesNotThrow(() => service.dispose()); + }); +}); + +suite('getGitHubContext', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function makeSession(overrides: Partial): IActiveSessionItem { + return { + resource: URI.parse('test://session/1'), + isUntitled: false, + label: 'Test Session', + repository: undefined, + worktree: undefined, + worktreeBranchName: undefined, + providerType: 'copilot-cloud-agent', + ...overrides, + }; + } + + test('parses owner/repo from github-remote-file URI', () => { + const session = makeSession({ + repository: URI.from({ + scheme: GITHUB_REMOTE_FILE_SCHEME, + authority: 'github', + path: '/microsoft/vscode/main' + }), + }); + + const parts = session.repository!.path.split('/').filter(Boolean); + assert.strictEqual(parts.length >= 2, true); + assert.strictEqual(decodeURIComponent(parts[0]), 'microsoft'); + assert.strictEqual(decodeURIComponent(parts[1]), 'vscode'); + }); + + test('parses PR number from pullRequestUrl', () => { + const url = 'https://github.com/microsoft/vscode/pull/12345'; + const match = /\/pull\/(\d+)/.exec(url); + assert.ok(match); + assert.strictEqual(parseInt(match![1], 10), 12345); + }); + + test('parses owner/repo from repositoryNwo', () => { + const nwo = 'microsoft/vscode'; + const parts = nwo.split('/'); + assert.strictEqual(parts.length, 2); + assert.strictEqual(parts[0], 'microsoft'); + assert.strictEqual(parts[1], 'vscode'); + }); + + test('returns undefined for non-GitHub file URI', () => { + const session = makeSession({ + repository: URI.file('/local/path/to/repo'), + }); + + // file:// scheme is not github-remote-file + assert.notStrictEqual(session.repository!.scheme, GITHUB_REMOTE_FILE_SCHEME); + }); +}); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 8f365a6eb87..99a317c80c3 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -26,6 +26,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { isUntitledChatSession } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; +import { IGitHubSessionContext } from '../../github/common/types.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -99,6 +100,23 @@ export interface ISessionsManagementService { * so the Changes view reflects the update. */ commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]): Promise; + + /** + * Derive a GitHub context (owner, repo, prNumber) from an active session. + * Returns `undefined` if the session is not associated with a GitHub repository. + */ + getGitHubContext(session: IActiveSessionItem): IGitHubSessionContext | undefined; + + /** + * Derive a GitHub context from a session resource URI. + * Looks up the agent session internally and resolves repository info. + */ + getGitHubContextForSession(sessionResource: URI): IGitHubSessionContext | undefined; + + /** + * Resolve a relative file path to a full URI based on the session's repository/worktree. + */ + resolveSessionFileUri(sessionResource: URI, relativePath: string): URI | undefined; } export const ISessionsManagementService = createDecorator('sessionsManagementService'); @@ -531,6 +549,104 @@ export class SessionsManagementService extends Disposable implements ISessionsMa await this.agentSessionsService.model.resolve(AgentSessionProviders.Background); } + getGitHubContext(session: IActiveSessionItem): IGitHubSessionContext | undefined { + // 1. Try parsing a github-remote-file URI (Cloud sessions) + const repoUri = session.repository; + if (repoUri && repoUri.scheme === GITHUB_REMOTE_FILE_SCHEME) { + const parts = repoUri.path.split('/').filter(Boolean); + if (parts.length >= 2) { + const owner = decodeURIComponent(parts[0]); + const repo = decodeURIComponent(parts[1]); + const prNumber = this._parsePRNumberFromSession(session); + return { owner, repo, prNumber }; + } + } + + // 2. Try from agent session metadata (Background sessions) + const agentSession = this.agentSessionsService.model.getSession(session.resource); + if (agentSession?.metadata) { + const metadata = agentSession.metadata; + + // owner + name fields + if (typeof metadata.owner === 'string' && typeof metadata.name === 'string') { + const prNumber = this._parsePRNumberFromSession(session); + return { owner: metadata.owner, repo: metadata.name, prNumber }; + } + + // repositoryNwo: "owner/repo" + if (typeof metadata.repositoryNwo === 'string') { + const parts = (metadata.repositoryNwo as string).split('/'); + if (parts.length === 2) { + const prNumber = this._parsePRNumberFromSession(session); + return { owner: parts[0], repo: parts[1], prNumber }; + } + } + + // pullRequestUrl: "https://github.com/{owner}/{repo}/pull/{number}" + if (typeof metadata.pullRequestUrl === 'string') { + const match = /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/.exec(metadata.pullRequestUrl as string); + if (match) { + return { owner: match[1], repo: match[2], prNumber: parseInt(match[3], 10) }; + } + } + } + + return undefined; + } + + getGitHubContextForSession(sessionResource: URI): IGitHubSessionContext | undefined { + const agentSession = this.agentSessionsService.model.getSession(sessionResource); + if (!agentSession) { + return undefined; + } + const [repository, worktree] = this.getRepositoryFromMetadata(agentSession); + return this.getGitHubContext({ + resource: sessionResource, + isUntitled: false, + label: agentSession.label, + repository, + worktree, + worktreeBranchName: undefined, + providerType: agentSession.providerType, + }); + } + + resolveSessionFileUri(sessionResource: URI, relativePath: string): URI | undefined { + const agentSession = this.agentSessionsService.model.getSession(sessionResource); + if (!agentSession) { + return undefined; + } + const [repository, worktree] = this.getRepositoryFromMetadata(agentSession); + const baseUri = worktree ?? repository; + if (!baseUri) { + return undefined; + } + return URI.joinPath(baseUri, relativePath); + } + + private _parsePRNumberFromSession(session: IActiveSessionItem): number | undefined { + const agentSession = this.agentSessionsService.model.getSession(session.resource); + const metadata = agentSession?.metadata; + if (!metadata) { + return undefined; + } + + // Direct prNumber field + if (typeof metadata.pullRequestNumber === 'number') { + return metadata.pullRequestNumber as number; + } + + // Parse from pullRequestUrl: https://github.com/{owner}/{repo}/pull/{number} + if (typeof metadata.pullRequestUrl === 'string') { + const match = /\/pull\/(\d+)/.exec(metadata.pullRequestUrl as string); + if (match) { + return parseInt(match[1], 10); + } + } + + return undefined; + } + private loadLastSelectedSession(): URI | undefined { const cached = this.storageService.get(LAST_SELECTED_SESSION_KEY, StorageScope.WORKSPACE); if (!cached) { diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 7e1174067c5..f8f88d37215 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -209,6 +209,7 @@ import './contrib/changes/browser/changesView.contribution.js'; import './contrib/codeReview/browser/codeReview.contributions.js'; import './contrib/files/browser/files.contribution.js'; import './contrib/git/browser/git.contribution.js'; +import './contrib/github/browser/github.contribution.js'; import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; From 40e2da7a672358cacefb931b4122dde133a4ff04 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:20:08 +0100 Subject: [PATCH 413/448] Update src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../sessions/contrib/codeReview/browser/codeReviewService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts index 689c409ffe4..7476f600c1b 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts @@ -641,6 +641,9 @@ export class CodeReviewService extends Disposable implements ICodeReviewService data.state.set({ kind: PRReviewStateKind.Error, reason: String(err) }, undefined); }); prModel.startPolling(); + data.disposables.add(new Disposable(() => { + prModel.stopPolling(); + })); } private _disposePRReview(sessionResource: URI): void { From 7cfab2369d8e960c0cd90e9c6640de1a27f9bf86 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 10 Mar 2026 14:31:04 +0100 Subject: [PATCH 414/448] revert copilot suggestion --- .../sessions/contrib/codeReview/browser/codeReviewService.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts index 7476f600c1b..689c409ffe4 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReviewService.ts @@ -641,9 +641,6 @@ export class CodeReviewService extends Disposable implements ICodeReviewService data.state.set({ kind: PRReviewStateKind.Error, reason: String(err) }, undefined); }); prModel.startPolling(); - data.disposables.add(new Disposable(() => { - prModel.stopPolling(); - })); } private _disposePRReview(sessionResource: URI): void { From 54b762dd8483d72bf62baa39f2cdff050702f6a8 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:59:01 +0100 Subject: [PATCH 415/448] Rerun the failed API version check (#300443) * Rerun the failed API version check * CCR comments --- .../workflows/api-proposal-version-check.yml | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/.github/workflows/api-proposal-version-check.yml b/.github/workflows/api-proposal-version-check.yml index 1edfc19028f..df8ce9ba977 100644 --- a/.github/workflows/api-proposal-version-check.yml +++ b/.github/workflows/api-proposal-version-check.yml @@ -14,7 +14,6 @@ permissions: contents: read pull-requests: write actions: write - checks: write concurrency: group: api-proposal-${{ github.event.pull_request.number || github.event.issue.number }} @@ -94,9 +93,15 @@ jobs: core.setOutput('override_found', 'false'); } - # If triggered by the override comment, create a successful check run on the PR head - - name: Pass on override comment - if: steps.check_override.outputs.override_found == 'true' + # If triggered by the override comment, re-run the failed workflow to update its status + # Only allow trusted users to trigger re-runs to prevent spam + - name: Re-run failed workflow on override + if: | + steps.check_override.outputs.override_found == 'true' && + github.event_name == 'issue_comment' && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR') uses: actions/github-script@v7 with: script: | @@ -104,20 +109,40 @@ jobs: console.log(`Override comment found by ${{ steps.check_override.outputs.override_user }}`); console.log('API proposal version change has been acknowledged.'); - // Create a successful check run on the PR head SHA so the status updates - await github.rest.checks.create({ + // Find the failed workflow run for this PR's head SHA + const { data: runs } = await github.rest.actions.listWorkflowRunsForWorkflow({ owner: context.repo.owner, repo: context.repo.repo, - name: 'Check API Proposal Version Changes', + workflow_id: 'api-proposal-version-check.yml', head_sha: headSha, status: 'completed', - conclusion: 'success', - output: { - title: 'API Proposal Version Change Acknowledged', - summary: `Override approved by @${{ steps.check_override.outputs.override_user }}` - } + per_page: 10 }); + // Find the most recent failed run + const failedRun = runs.workflow_runs.find(run => + run.conclusion === 'failure' && run.event === 'pull_request' + ); + + if (failedRun) { + console.log(`Re-running failed workflow run ${failedRun.id}`); + await github.rest.actions.reRunWorkflow({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: failedRun.id + }); + console.log('Workflow re-run triggered successfully'); + } else { + console.log('No failed pull_request workflow run found to re-run'); + // The check will pass on this run since override exists + } + + - name: Pass on override comment + if: steps.check_override.outputs.override_found == 'true' + run: | + echo "Override comment found by ${{ steps.check_override.outputs.override_user }}" + echo "API proposal version change has been acknowledged." + # Only continue checking if no override found - name: Checkout repository if: steps.check_override.outputs.override_found != 'true' From 526c24b569a5d6530e701894136d1612baf04cf2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 10 Mar 2026 15:00:17 +0100 Subject: [PATCH 416/448] chat: refactor model picker delegate and improve picker UX (#300436) * refactor: update model management methods to use grouped model picker * refactor: remove showManageModelsAction parameter from model picker functions * refactor: streamline model picker item construction and improve readability * refactor: reorganize model picker logic to support grouped model selection * refactor: disable showManageModelsAction in NewChatWidget and simplify useGroupedModelPicker logic * fix: increase viewport max height calculation in ActionList class * feat: add hover position support to model picker and related components --- .../actionWidget/browser/actionList.ts | 2 +- .../contrib/chat/browser/newChatViewPane.ts | 3 +- .../browser/widget/input/chatInputPart.ts | 3 +- .../browser/widget/input/chatModelPicker.ts | 401 +++++++++--------- .../widget/input/modelPickerActionItem.ts | 3 +- .../widget/input/modelPickerActionItem2.ts | 2 +- 6 files changed, 211 insertions(+), 203 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 30aa721ddd5..5a6cf722ccc 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -810,7 +810,7 @@ export class ActionList extends Disposable { availableHeight = widgetTop > 0 ? windowHeight - widgetTop - padding : windowHeight * 0.7; } - const viewportMaxHeight = Math.floor(targetWindow.innerHeight * 0.4); + const viewportMaxHeight = Math.floor(targetWindow.innerHeight * 0.6); const maxHeight = Math.min(Math.max(availableHeight, this._actionLineHeight * 3 + filterHeight), viewportMaxHeight); const height = Math.min(listHeight + filterHeight, maxHeight); return height - filterHeight; diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index f4b76e4ffac..389144ba3c9 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -686,7 +686,8 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._focusEditor(); }, getModels: () => this._getAvailableModels(), - canManageModels: () => true, + useGroupedModelPicker: () => true, + showManageModelsAction: () => false, }; const pickerOptions: IChatInputPickerOptions = { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index cf7862b0d25..86ba0bd181c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2225,7 +2225,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.renderAttachedContext(); }, getModels: () => this.getModels(), - canManageModels: () => { + useGroupedModelPicker: () => true, + showManageModelsAction: () => { const sessionType = this.getCurrentSessionType(); return !sessionType || sessionType === localChatSessionType; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 2c5f04d1e39..5a4968973f9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -16,6 +16,7 @@ import { autorun, IObservable } from '../../../../../../base/common/observable.j import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; import { ActionListItemKind, IActionListItem } from '../../../../../../platform/actionWidget/browser/actionList.js'; +import { IHoverPositionOptions } from '../../../../../../base/browser/ui/hover/hover.js'; import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; @@ -76,6 +77,7 @@ type ChatModelChangeEvent = { function createModelItem( action: IActionWidgetDropdownAction & { section?: string }, model?: ILanguageModelChatMetadataAndIdentifier, + hoverPosition?: IHoverPositionOptions, ): IActionListItem { return { item: action, @@ -85,7 +87,7 @@ function createModelItem( group: { title: '', icon: action.icon ?? ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) }, hideIcon: false, section: action.section, - hover: model ? { content: getModelHoverContent(model) } : undefined, + hover: model ? { content: getModelHoverContent(model), position: hoverPosition } : undefined, }; } @@ -150,9 +152,10 @@ export function buildModelPickerItems( updateStateType: StateType, onSelect: (model: ILanguageModelChatMetadataAndIdentifier) => void, manageSettingsUrl: string | undefined, - canManageModels: boolean, + useGroupedModelPicker: boolean, manageModelsAction: IActionWidgetDropdownAction | undefined, chatEntitlementService: IChatEntitlementService, + hoverPosition?: IHoverPositionOptions, ): IActionListItem[] { const items: IActionListItem[] = []; if (models.length === 0) { @@ -167,11 +170,200 @@ export function buildModelPickerItems( })); } - if (!canManageModels) { + if (useGroupedModelPicker) { + const isPro = isProUser(chatEntitlementService.entitlement); + let otherModels: ILanguageModelChatMetadataAndIdentifier[] = []; + if (models.length) { + // Collect all available models into lookup maps + const allModelsMap = new Map(); + const modelsByMetadataId = new Map(); + for (const model of models) { + allModelsMap.set(model.identifier, model); + modelsByMetadataId.set(model.metadata.id, model); + } + + const placed = new Set(); + + const markPlaced = (identifierOrId: string, metadataId?: string) => { + placed.add(identifierOrId); + if (metadataId) { + placed.add(metadataId); + } + }; + + const resolveModel = (id: string) => allModelsMap.get(id) ?? modelsByMetadataId.get(id); + + const getUnavailableReason = (entry: IModelControlEntry): 'upgrade' | 'update' | 'admin' => { + if (!isPro) { + return 'upgrade'; + } + if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + return 'update'; + } + return 'admin'; + }; + + // --- 1. Auto --- + const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); + if (autoModel) { + markPlaced(autoModel.identifier, autoModel.metadata.id); + items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel, hoverPosition)); + } + + // --- 2. Promoted section (selected + recently used + featured) --- + type PromotedItem = + | { kind: 'available'; model: ILanguageModelChatMetadataAndIdentifier } + | { kind: 'unavailable'; id: string; entry: IModelControlEntry; reason: 'upgrade' | 'update' | 'admin' }; + + const promotedItems: PromotedItem[] = []; + + // Try to place a model by id. Returns true if handled. + const tryPlaceModel = (id: string): boolean => { + if (placed.has(id)) { + return false; + } + const model = resolveModel(id); + if (model && !placed.has(model.identifier)) { + markPlaced(model.identifier, model.metadata.id); + const entry = controlModels[model.metadata.id]; + if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + promotedItems.push({ kind: 'unavailable', id: model.metadata.id, entry, reason: 'update' }); + } else { + promotedItems.push({ kind: 'available', model }); + } + return true; + } + if (!model) { + const entry = controlModels[id]; + if (entry && !entry.exists) { + markPlaced(id); + promotedItems.push({ kind: 'unavailable', id, entry, reason: getUnavailableReason(entry) }); + return true; + } + } + return false; + }; + + // Selected model + if (selectedModelId && selectedModelId !== autoModel?.identifier) { + tryPlaceModel(selectedModelId); + } + + // Recently used models + for (const id of recentModelIds) { + tryPlaceModel(id); + } + + // Featured models from control manifest + for (const [entryId, entry] of Object.entries(controlModels)) { + if (!entry.featured || placed.has(entryId)) { + continue; + } + const model = resolveModel(entryId); + if (model && !placed.has(model.identifier)) { + markPlaced(model.identifier, model.metadata.id); + if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: 'update' }); + } else { + promotedItems.push({ kind: 'available', model }); + } + } else if (!model && !entry.exists) { + markPlaced(entryId); + promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: getUnavailableReason(entry) }); + } + } + + // Render promoted section: available first, then sorted alphabetically by name + if (promotedItems.length > 0) { + promotedItems.sort((a, b) => { + const aAvail = a.kind === 'available' ? 0 : 1; + const bAvail = b.kind === 'available' ? 0 : 1; + if (aAvail !== bAvail) { + return aAvail - bAvail; + } + const aName = a.kind === 'available' ? a.model.metadata.name : a.entry.label; + const bName = b.kind === 'available' ? b.model.metadata.name : b.entry.label; + return aName.localeCompare(bName); + }); + + for (const item of promotedItems) { + if (item.kind === 'available') { + items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model, hoverPosition)); + } else { + items.push(createUnavailableModelItem(item.id, item.entry, item.reason, manageSettingsUrl, updateStateType, undefined, hoverPosition)); + } + } + } + + // --- 3. Other Models (collapsible) --- + otherModels = models + .filter(m => !placed.has(m.identifier) && !placed.has(m.metadata.id)) + .sort((a, b) => { + const aEntry = controlModels[a.metadata.id] ?? controlModels[a.identifier]; + const bEntry = controlModels[b.metadata.id] ?? controlModels[b.identifier]; + const aAvail = aEntry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, aEntry.minVSCodeVersion) ? 1 : 0; + const bAvail = bEntry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, bEntry.minVSCodeVersion) ? 1 : 0; + if (aAvail !== bAvail) { + return aAvail - bAvail; + } + const aCopilot = a.metadata.vendor === 'copilot' ? 0 : 1; + const bCopilot = b.metadata.vendor === 'copilot' ? 0 : 1; + if (aCopilot !== bCopilot) { + return aCopilot - bCopilot; + } + const vendorCmp = a.metadata.vendor.localeCompare(b.metadata.vendor); + return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); + }); + + if (otherModels.length > 0) { + if (items.length > 0) { + items.push({ kind: ActionListItemKind.Separator }); + } + items.push({ + item: { + id: 'otherModels', + enabled: true, + checked: false, + class: undefined, + tooltip: localize('chat.modelPicker.otherModels', "Other Models"), + label: localize('chat.modelPicker.otherModels', "Other Models"), + run: () => { /* toggle handled by isSectionToggle */ } + }, + kind: ActionListItemKind.Action, + label: localize('chat.modelPicker.otherModels', "Other Models"), + group: { title: '', icon: Codicon.chevronDown }, + hideIcon: false, + section: ModelPickerSection.Other, + isSectionToggle: true, + }); + for (const model of otherModels) { + const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; + if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', manageSettingsUrl, updateStateType, ModelPickerSection.Other, hoverPosition)); + } else { + items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model, hoverPosition)); + } + } + } + } + + if (manageModelsAction) { + items.push({ kind: ActionListItemKind.Separator, section: otherModels.length ? ModelPickerSection.Other : undefined }); + items.push({ + item: manageModelsAction, + kind: ActionListItemKind.Action, + label: manageModelsAction.label, + group: { title: '', icon: Codicon.blank }, + hideIcon: false, + section: otherModels.length ? ModelPickerSection.Other : undefined, + showAlways: true, + }); + } + } else { // Flat list: auto first, then all models sorted alphabetically const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); if (autoModel) { - items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel)); + items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel, hoverPosition)); } const sortedModels = models .filter(m => m !== autoModel) @@ -180,198 +372,8 @@ export function buildModelPickerItems( return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); }); for (const model of sortedModels) { - items.push(createModelItem(createModelAction(model, selectedModelId, onSelect), model)); + items.push(createModelItem(createModelAction(model, selectedModelId, onSelect), model, hoverPosition)); } - return items; - } - - const isPro = isProUser(chatEntitlementService.entitlement); - let otherModels: ILanguageModelChatMetadataAndIdentifier[] = []; - if (models.length) { - // Collect all available models into lookup maps - const allModelsMap = new Map(); - const modelsByMetadataId = new Map(); - for (const model of models) { - allModelsMap.set(model.identifier, model); - modelsByMetadataId.set(model.metadata.id, model); - } - - const placed = new Set(); - - const markPlaced = (identifierOrId: string, metadataId?: string) => { - placed.add(identifierOrId); - if (metadataId) { - placed.add(metadataId); - } - }; - - const resolveModel = (id: string) => allModelsMap.get(id) ?? modelsByMetadataId.get(id); - - const getUnavailableReason = (entry: IModelControlEntry): 'upgrade' | 'update' | 'admin' => { - if (!isPro) { - return 'upgrade'; - } - if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - return 'update'; - } - return 'admin'; - }; - - // --- 1. Auto --- - const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); - if (autoModel) { - markPlaced(autoModel.identifier, autoModel.metadata.id); - items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel)); - } - - // --- 2. Promoted section (selected + recently used + featured) --- - type PromotedItem = - | { kind: 'available'; model: ILanguageModelChatMetadataAndIdentifier } - | { kind: 'unavailable'; id: string; entry: IModelControlEntry; reason: 'upgrade' | 'update' | 'admin' }; - - const promotedItems: PromotedItem[] = []; - - // Try to place a model by id. Returns true if handled. - const tryPlaceModel = (id: string): boolean => { - if (placed.has(id)) { - return false; - } - const model = resolveModel(id); - if (model && !placed.has(model.identifier)) { - markPlaced(model.identifier, model.metadata.id); - const entry = controlModels[model.metadata.id]; - if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - promotedItems.push({ kind: 'unavailable', id: model.metadata.id, entry, reason: 'update' }); - } else { - promotedItems.push({ kind: 'available', model }); - } - return true; - } - if (!model) { - const entry = controlModels[id]; - if (entry && !entry.exists) { - markPlaced(id); - promotedItems.push({ kind: 'unavailable', id, entry, reason: getUnavailableReason(entry) }); - return true; - } - } - return false; - }; - - // Selected model - if (selectedModelId && selectedModelId !== autoModel?.identifier) { - tryPlaceModel(selectedModelId); - } - - // Recently used models - for (const id of recentModelIds) { - tryPlaceModel(id); - } - - // Featured models from control manifest - for (const [entryId, entry] of Object.entries(controlModels)) { - if (!entry.featured || placed.has(entryId)) { - continue; - } - const model = resolveModel(entryId); - if (model && !placed.has(model.identifier)) { - markPlaced(model.identifier, model.metadata.id); - if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: 'update' }); - } else { - promotedItems.push({ kind: 'available', model }); - } - } else if (!model && !entry.exists) { - markPlaced(entryId); - promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: getUnavailableReason(entry) }); - } - } - - // Render promoted section: available first, then sorted alphabetically by name - if (promotedItems.length > 0) { - promotedItems.sort((a, b) => { - const aAvail = a.kind === 'available' ? 0 : 1; - const bAvail = b.kind === 'available' ? 0 : 1; - if (aAvail !== bAvail) { - return aAvail - bAvail; - } - const aName = a.kind === 'available' ? a.model.metadata.name : a.entry.label; - const bName = b.kind === 'available' ? b.model.metadata.name : b.entry.label; - return aName.localeCompare(bName); - }); - - for (const item of promotedItems) { - if (item.kind === 'available') { - items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); - } else { - items.push(createUnavailableModelItem(item.id, item.entry, item.reason, manageSettingsUrl, updateStateType)); - } - } - } - - // --- 3. Other Models (collapsible) --- - otherModels = models - .filter(m => !placed.has(m.identifier) && !placed.has(m.metadata.id)) - .sort((a, b) => { - const aEntry = controlModels[a.metadata.id] ?? controlModels[a.identifier]; - const bEntry = controlModels[b.metadata.id] ?? controlModels[b.identifier]; - const aAvail = aEntry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, aEntry.minVSCodeVersion) ? 1 : 0; - const bAvail = bEntry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, bEntry.minVSCodeVersion) ? 1 : 0; - if (aAvail !== bAvail) { - return aAvail - bAvail; - } - const aCopilot = a.metadata.vendor === 'copilot' ? 0 : 1; - const bCopilot = b.metadata.vendor === 'copilot' ? 0 : 1; - if (aCopilot !== bCopilot) { - return aCopilot - bCopilot; - } - const vendorCmp = a.metadata.vendor.localeCompare(b.metadata.vendor); - return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); - }); - - if (otherModels.length > 0) { - if (items.length > 0) { - items.push({ kind: ActionListItemKind.Separator }); - } - items.push({ - item: { - id: 'otherModels', - enabled: true, - checked: false, - class: undefined, - tooltip: localize('chat.modelPicker.otherModels', "Other Models"), - label: localize('chat.modelPicker.otherModels', "Other Models"), - run: () => { /* toggle handled by isSectionToggle */ } - }, - kind: ActionListItemKind.Action, - label: localize('chat.modelPicker.otherModels', "Other Models"), - group: { title: '', icon: Codicon.chevronDown }, - hideIcon: false, - section: ModelPickerSection.Other, - isSectionToggle: true, - }); - for (const model of otherModels) { - const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; - if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', manageSettingsUrl, updateStateType, ModelPickerSection.Other)); - } else { - items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model)); - } - } - } - } - - if (manageModelsAction) { - items.push({ kind: ActionListItemKind.Separator, section: otherModels.length ? ModelPickerSection.Other : undefined }); - items.push({ - item: manageModelsAction, - kind: ActionListItemKind.Action, - label: manageModelsAction.label, - group: { title: '', icon: Codicon.blank }, - hideIcon: false, - section: otherModels.length ? ModelPickerSection.Other : undefined, - showAlways: true, - }); } return items; @@ -400,6 +402,7 @@ function createUnavailableModelItem( manageSettingsUrl: string | undefined, updateStateType: StateType, section?: string, + hoverPosition?: IHoverPositionOptions, ): IActionListItem { let description: string | MarkdownString | undefined; @@ -443,7 +446,7 @@ function createUnavailableModelItem( hideIcon: false, className: 'chat-model-picker-unavailable', section, - hover: { content: hoverContent }, + hover: { content: hoverContent, position: hoverPosition }, }; } @@ -481,6 +484,7 @@ export class ModelPickerWidget extends Disposable { constructor( private readonly _delegate: IModelPickerDelegate, + private readonly _hoverPosition: IHoverPositionOptions | undefined, @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, @ICommandService private readonly _commandService: ICommandService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -572,7 +576,7 @@ export class ModelPickerWidget extends Disposable { const isPro = isProUser(this._entitlementService.entitlement); const manifest = this._languageModelsService.getModelsControlManifest(); const controlModelsForTier = isPro ? manifest.paid : manifest.free; - const canShowManageModelsAction = this._delegate.canManageModels() && shouldShowManageModelsAction(this._entitlementService); + const canShowManageModelsAction = this._delegate.showManageModelsAction() && shouldShowManageModelsAction(this._entitlementService); const manageModelsAction = canShowManageModelsAction ? createManageModelsAction(this._commandService) : undefined; const items = buildModelPickerItems( models, @@ -583,9 +587,10 @@ export class ModelPickerWidget extends Disposable { this._updateService.state.type, onSelect, this._productService.defaultChatAgent?.manageSettingsUrl, - this._delegate.canManageModels(), + this._delegate.useGroupedModelPicker(), !showFilter ? manageModelsAction : undefined, this._entitlementService, + this._hoverPosition, ); const listOptions = { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index af61812b3a9..bae01a07ca3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -29,7 +29,8 @@ export interface IModelPickerDelegate { readonly currentModel: IObservable; setModel(model: ILanguageModelChatMetadataAndIdentifier): void; getModels(): ILanguageModelChatMetadataAndIdentifier[]; - canManageModels(): boolean; + useGroupedModelPicker(): boolean; + showManageModelsAction(): boolean; } type ChatModelChangeClassification = { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts index 5a4d402e790..dfc1510a2c8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts @@ -39,7 +39,7 @@ export class EnhancedModelPickerActionItem extends BaseActionViewItem { ) { super(undefined, action); - this._pickerWidget = this._register(instantiationService.createInstance(ModelPickerWidget, delegate)); + this._pickerWidget = this._register(instantiationService.createInstance(ModelPickerWidget, delegate, pickerOptions.hoverPosition)); this._pickerWidget.setSelectedModel(delegate.currentModel.get()); this._pickerWidget.setHideChevrons(pickerOptions.hideChevrons); From c56c7bc071d43dd0756cb18bbddcd0a1ec91e578 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:01:11 +0100 Subject: [PATCH 417/448] Revert "Git - adopt the new package to use copy-on-write for the worktree include files (#299583)" (#300448) This reverts commit 950ab0704be211929e5f1bc9a0809201edc7ea1c. --- extensions/git/package-lock.json | 20 --------- extensions/git/package.json | 1 - extensions/git/src/repository.ts | 76 ++++++++++++-------------------- 3 files changed, 29 insertions(+), 68 deletions(-) diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index 6353cbc2753..b552ce9fa5b 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", - "@vscode/fs-copyfile": "2.0.0", "byline": "^5.0.0", "file-type": "16.5.4", "picomatch": "2.3.1", @@ -219,19 +218,6 @@ "vscode": "^1.75.0" } }, - "node_modules/@vscode/fs-copyfile": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@vscode/fs-copyfile/-/fs-copyfile-2.0.0.tgz", - "integrity": "sha512-ARb4+9rN905WjJtQ2mSBG/q4pjJkSRun/MkfCeRkk7h/5J8w4vd18NCePFJ/ZucIwXx/7mr9T6nz9Vtt1tk7hg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">=22.6.0" - } - }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -288,12 +274,6 @@ "node": ">=16" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", diff --git a/extensions/git/package.json b/extensions/git/package.json index f0e49309944..1fbac49569f 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -4346,7 +4346,6 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", - "@vscode/fs-copyfile": "2.0.0", "byline": "^5.0.0", "file-type": "16.5.4", "picomatch": "2.3.1", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 53db3e48495..bd6b6a5c7ff 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { cp } from '@vscode/fs-copyfile'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import * as fs from 'fs'; @@ -1931,76 +1930,59 @@ export class Repository implements Disposable { gitIgnoredFiles.delete(uri.fsPath); } - // Compute the base directory for each glob pattern (the fixed - // prefix before any wildcard characters). This will be used to - // optimize the upward traversal when adding parent directories. - const filePatternBases = new Set(); - for (const pattern of worktreeIncludeFiles) { - const segments = pattern.split(/[\/\\]/); - const fixedSegments: string[] = []; - for (const seg of segments) { - if (/[*?{}[\]]/.test(seg)) { - break; - } - fixedSegments.push(seg); - } - filePatternBases.add(path.join(this.root, ...fixedSegments)); - } - - // Add the folder paths for git ignored files, walking - // up only to the nearest file pattern base directory. + // Add the folder paths for git ignored files const gitIgnoredPaths = new Set(gitIgnoredFiles); for (const filePath of gitIgnoredFiles) { let dir = path.dirname(filePath); - while (dir !== this.root && !gitIgnoredPaths.has(dir)) { + while (dir !== this.root && !gitIgnoredFiles.has(dir)) { gitIgnoredPaths.add(dir); - if (filePatternBases.has(dir)) { - break; - } dir = path.dirname(dir); } } - // Find minimal set of paths (folders and files) to copy. Keep only topmost - // paths — if a directory is already in the set, all its descendants are - // implicitly included and don't need separate entries. - let lastTopmost: string | undefined; - const pathsToCopy = new Set(); - for (const p of Array.from(gitIgnoredPaths).sort()) { - if (lastTopmost && (p === lastTopmost || p.startsWith(lastTopmost + path.sep))) { - continue; - } - pathsToCopy.add(p); - lastTopmost = p; - } - - return pathsToCopy; + return gitIgnoredPaths; } private async _copyWorktreeIncludeFiles(worktreePath: string): Promise { - const worktreeIncludePaths = await this._getWorktreeIncludePaths(); - if (worktreeIncludePaths.size === 0) { + const gitIgnoredPaths = await this._getWorktreeIncludePaths(); + if (gitIgnoredPaths.size === 0) { return; } try { - const startTime = performance.now(); + // Find minimal set of paths (folders and files) to copy. + // The goal is to reduce the number of copy operations + // needed. + const pathsToCopy = new Set(); + for (const filePath of gitIgnoredPaths) { + const relativePath = path.relative(this.root, filePath); + const firstSegment = relativePath.split(path.sep)[0]; + pathsToCopy.add(path.join(this.root, firstSegment)); + } + + const startTime = Date.now(); const limiter = new Limiter(15); - const files = Array.from(worktreeIncludePaths); + const files = Array.from(pathsToCopy); // Copy files - const results = await Promise.allSettled(files.map(sourceFile => { - return limiter.queue(async () => { + const results = await Promise.allSettled(files.map(sourceFile => + limiter.queue(async () => { const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); - await cp(sourceFile, targetFile, { force: true, recursive: true, verbatimSymlinks: true }); - }); - })); + await fsPromises.cp(sourceFile, targetFile, { + filter: src => gitIgnoredPaths.has(src), + force: true, + mode: fs.constants.COPYFILE_FICLONE, + recursive: true, + verbatimSymlinks: true + }); + }) + )); // Log any failed operations const failedOperations = results.filter(r => r.status === 'rejected'); - this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${(performance.now() - startTime).toFixed(2)}ms]`); + this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${Date.now() - startTime}ms]`); if (failedOperations.length > 0) { window.showWarningMessage(l10n.t('Failed to copy {0} folder(s)/file(s) to the worktree.', failedOperations.length)); From 62fcd7cac0f0074afd725df028ac0b90273e58e0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 10 Mar 2026 15:56:16 +0100 Subject: [PATCH 418/448] Mode picker in Sessions app (#298199) * feat: add mode picker functionality and integrate mode management in chat sessions * feat: integrate IChatSessionsService and enhance mode picker with custom agent target * feat: update mode picker to use MenuItemAction and remove setMode callback * fix: update available modes logic to use effective target for custom agent selection * feat: enhance mode picker to support configuration of custom agents and update item structure * feat: integrate ILanguageModelToolsService and enhance mode handling in SessionsManagementService * feat: add group property to configure custom agents action in mode picker * feat: update session mode handling to reflect selected mode in input model * fix setting agent in session options * fix * fix checking untitled session * Update src/vs/sessions/contrib/chat/browser/modePicker.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/sessions/contrib/chat/browser/newSession.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/sessions/contrib/chat/browser/modePicker.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/sessions/contrib/chat/browser/modePicker.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feedback * open new ai customisations editor --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/modePicker.ts | 243 ++++++++++++++++++ .../contrib/chat/browser/newChatViewPane.ts | 25 +- .../contrib/chat/browser/newSession.ts | 23 +- .../browser/sessionsManagementService.ts | 40 ++- 4 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 src/vs/sessions/contrib/chat/browser/modePicker.ts diff --git a/src/vs/sessions/contrib/chat/browser/modePicker.ts b/src/vs/sessions/contrib/chat/browser/modePicker.ts new file mode 100644 index 00000000000..d7cc8df61b3 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/modePicker.ts @@ -0,0 +1,243 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { ChatMode, IChatMode, IChatModeService } from '../../../../workbench/contrib/chat/common/chatModes.js'; +import { IGitRepository } from '../../../../workbench/contrib/git/common/gitService.js'; +import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { Target } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; +import { AICustomizationManagementCommands } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; + +interface IModePickerItem { + readonly kind: 'mode'; + readonly mode: IChatMode; +} + +interface IConfigurePickerItem { + readonly kind: 'configure'; +} + +type ModePickerItem = IModePickerItem | IConfigurePickerItem; + +/** + * A self-contained widget for selecting a chat mode (Agent, custom agents) + * for local/Background sessions. Shows only modes whose target matches + * the Background session type's customAgentTarget. + */ +export class ModePicker extends Disposable { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private _triggerElement: HTMLElement | undefined; + private _slotElement: HTMLElement | undefined; + private readonly _renderDisposables = this._register(new DisposableStore()); + + private _selectedMode: IChatMode = ChatMode.Agent; + + get selectedMode(): IChatMode { + return this._selectedMode; + } + + constructor( + @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, + @IChatModeService private readonly chatModeService: IChatModeService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @ICommandService private readonly commandService: ICommandService, + ) { + super(); + + this._register(this.chatModeService.onDidChangeChatModes(() => { + // Refresh the trigger label when available chat modes change + if (this._triggerElement) { + this._updateTriggerLabel(); + } + })); + } + + /** + * Sets the git repository. When the repository changes, resets the selected mode + * back to the default Agent mode. + */ + setRepository(repository: IGitRepository | undefined): void { + this._selectedMode = ChatMode.Agent; + this._updateTriggerLabel(); + } + + /** + * Renders the mode picker trigger button into the given container. + */ + render(container: HTMLElement): HTMLElement { + this._renderDisposables.clear(); + + const slot = dom.append(container, dom.$('.sessions-chat-picker-slot')); + this._slotElement = slot; + this._renderDisposables.add({ dispose: () => slot.remove() }); + + const trigger = dom.append(slot, dom.$('a.action-label')); + trigger.tabIndex = 0; + trigger.role = 'button'; + trigger.setAttribute('aria-label', localize('sessions.modePicker.ariaLabel', "Select chat mode")); + this._triggerElement = trigger; + + this._updateTriggerLabel(); + + this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.CLICK, (e) => { + dom.EventHelper.stop(e, true); + this._showPicker(); + })); + + this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + dom.EventHelper.stop(e, true); + this._showPicker(); + } + })); + + return slot; + } + + /** + * Shows or hides the picker. + */ + setVisible(visible: boolean): void { + if (this._slotElement) { + this._slotElement.style.display = visible ? '' : 'none'; + } + } + + private _getAvailableModes(): IChatMode[] { + const customAgentTarget = this.chatSessionsService.getCustomAgentTargetForSessionType(AgentSessionProviders.Background); + const effectiveTarget = customAgentTarget && customAgentTarget !== Target.Undefined ? customAgentTarget : Target.GitHubCopilot; + const modes = this.chatModeService.getModes(); + + // Always include the default Agent mode + const result: IChatMode[] = [ChatMode.Agent]; + + // Add custom modes matching the target and visible to users + for (const mode of modes.custom) { + const target = mode.target.get(); + if (target === effectiveTarget || target === Target.Undefined) { + const visibility = mode.visibility?.get(); + if (visibility && !visibility.userInvocable) { + continue; + } + result.push(mode); + } + } + + return result; + } + + private _showPicker(): void { + if (!this._triggerElement || this.actionWidgetService.isVisible) { + return; + } + + const modes = this._getAvailableModes(); + + const items = this._buildItems(modes); + + const triggerElement = this._triggerElement; + const delegate: IActionListDelegate = { + onSelect: (item) => { + this.actionWidgetService.hide(); + if (item.kind === 'mode') { + this._selectMode(item.mode); + } else { + this.commandService.executeCommand(AICustomizationManagementCommands.OpenEditor); + } + }, + onHide: () => { triggerElement.focus(); }, + }; + + this.actionWidgetService.show( + 'localModePicker', + false, + items, + delegate, + this._triggerElement, + undefined, + [], + { + getAriaLabel: (item) => item.label ?? '', + getWidgetAriaLabel: () => localize('modePicker.ariaLabel', "Mode Picker"), + }, + ); + } + + private _buildItems(modes: IChatMode[]): IActionListItem[] { + const items: IActionListItem[] = []; + + // Default Agent mode + const agentMode = modes[0]; + items.push({ + kind: ActionListItemKind.Action, + label: agentMode.label.get(), + group: { title: '', icon: this._selectedMode.id === agentMode.id ? Codicon.check : Codicon.blank }, + item: { kind: 'mode', mode: agentMode }, + }); + + // Custom modes (with separator if any exist) + const customModes = modes.slice(1); + if (customModes.length > 0) { + items.push({ kind: ActionListItemKind.Separator, label: '' }); + for (const mode of customModes) { + items.push({ + kind: ActionListItemKind.Action, + label: mode.label.get(), + group: { title: '', icon: this._selectedMode.id === mode.id ? Codicon.check : Codicon.blank }, + item: { kind: 'mode', mode }, + }); + } + } + + // Configure Custom Agents action + items.push({ kind: ActionListItemKind.Separator, label: '' }); + items.push({ + kind: ActionListItemKind.Action, + label: localize('configureCustomAgents', "Configure Custom Agents..."), + group: { title: '', icon: Codicon.blank }, + item: { kind: 'configure' }, + }); + + return items; + } + + private _selectMode(mode: IChatMode): void { + this._selectedMode = mode; + this._updateTriggerLabel(); + this._onDidChange.fire(mode); + } + + private _updateTriggerLabel(): void { + if (!this._triggerElement) { + return; + } + + dom.clearNode(this._triggerElement); + + const icon = this._selectedMode.icon.get(); + if (icon) { + dom.append(this._triggerElement, renderIcon(icon)); + } + + const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label')); + labelSpan.textContent = this._selectedMode.label.get(); + dom.append(this._triggerElement, renderIcon(Codicon.chevronDown)); + + const modes = this._getAvailableModes(); + this._slotElement?.classList.toggle('disabled', modes.length <= 1); + } +} diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 389144ba3c9..79f3e56db66 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -63,6 +63,7 @@ import { SyncIndicator } from './syncIndicator.js'; import { INewSession, ISessionOptionGroup, RemoteNewSession } from './newSession.js'; import { RepoPicker } from './repoPicker.js'; import { CloudModelPicker } from './modelPicker.js'; +import { ModePicker } from './modePicker.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { SlashCommandHandler } from './slashCommands.js'; import { IChatModelInputState } from '../../../../workbench/contrib/chat/common/model/chatModel.js'; @@ -152,6 +153,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private readonly _repoPicker: RepoPicker; private _repoPickerContainer: HTMLElement | undefined; private readonly _cloudModelPicker: CloudModelPicker; + private readonly _modePicker: ModePicker; private readonly _toolbarPickerWidgets = new Map(); private readonly _toolbarPickerDisposables = this._register(new DisposableStore()); private readonly _optionEmitters = new Map>(); @@ -199,6 +201,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._permissionPicker = this._register(this.instantiationService.createInstance(NewChatPermissionPicker)); this._repoPicker = this._register(this.instantiationService.createInstance(RepoPicker)); this._cloudModelPicker = this._register(this.instantiationService.createInstance(CloudModelPicker)); + this._modePicker = this._register(this.instantiationService.createInstance(ModePicker)); this._targetPicker = this._register(new SessionTargetPicker(options.allowedTargets, this._resolveDefaultTarget(options))); this._isolationModePicker = this._register(this.instantiationService.createInstance(IsolationModePicker)); this._branchPicker = this._register(this.instantiationService.createInstance(BranchPicker)); @@ -246,6 +249,12 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._focusEditor(); })); + // When mode changes, update the session + this._register(this._modePicker.onDidChange((mode) => { + this._newSession.value?.setMode(mode); + this._focusEditor(); + })); + this._register(this._repoPicker.onDidSelectRepo((repoId) => { if (this._targetPicker.selectedTarget !== AgentSessionProviders.Background) { this._newSession.value?.setRepoUri(this._getRepoUri(repoId)); @@ -392,6 +401,9 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { session.setModelId(currentModel.identifier); } + // Set the current mode on the session (for local sessions) + session.setMode(this._modePicker.selectedMode); + // Open repository for the session's repoUri if (session.repoUri) { this._openRepository(session.repoUri); @@ -433,6 +445,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._branchPicker.setRepository(undefined); this._isolationModePicker.setRepository(undefined); this._syncIndicator.setRepository(undefined); + this._modePicker.setRepository(undefined); this.gitService.openRepository(folderUri).then(repository => { if (cts.token.isCancellationRequested) { @@ -443,6 +456,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._isolationModePicker.setRepository(repository); this._branchPicker.setRepository(repository); this._syncIndicator.setRepository(repository); + this._modePicker.setRepository(repository); }).catch(e => { if (cts.token.isCancellationRequested) { return; @@ -453,6 +467,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._isolationModePicker.setRepository(undefined); this._branchPicker.setRepository(undefined); this._syncIndicator.setRepository(undefined); + this._modePicker.setRepository(undefined); }); } @@ -641,6 +656,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._localModelPickerContainer = dom.append(toolbar, dom.$('.sessions-chat-model-picker')); this._createLocalModelPicker(this._localModelPickerContainer); + // Local mode picker + this._modePicker.render(toolbar); + this._modePicker.setVisible(false); + // Remote model picker (action list dropdown) this._cloudModelPicker.render(toolbar); this._cloudModelPicker.setVisible(false); @@ -763,10 +782,11 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { if (this._extensionPickersLeftContainer) { this._extensionPickersLeftContainer.style.display = 'block'; } - // Show local model picker, hide remote + // Show local model and mode pickers, hide remote if (this._localModelPickerContainer) { this._localModelPickerContainer.style.display = ''; } + this._modePicker.setVisible(true); this._cloudModelPicker.setVisible(false); } @@ -782,10 +802,11 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._folderPickerContainer.style.display = 'none'; } - // Show remote model picker, hide local + // Show remote model picker, hide local pickers if (this._localModelPickerContainer) { this._localModelPickerContainer.style.display = 'none'; } + this._modePicker.setVisible(false); this._cloudModelPicker.setSession(session); this._cloudModelPicker.setVisible(true); diff --git a/src/vs/sessions/contrib/chat/browser/newSession.ts b/src/vs/sessions/contrib/chat/browser/newSession.ts index f8211f04300..232f56251cc 100644 --- a/src/vs/sessions/contrib/chat/browser/newSession.ts +++ b/src/vs/sessions/contrib/chat/browser/newSession.ts @@ -13,8 +13,9 @@ import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browse import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; +import { IChatMode } from '../../../../workbench/contrib/chat/common/chatModes.js'; -export type NewSessionChangeType = 'repoUri' | 'isolationMode' | 'branch' | 'options' | 'disabled'; +export type NewSessionChangeType = 'repoUri' | 'isolationMode' | 'branch' | 'options' | 'disabled' | 'agent'; /** * Represents a resolved option group with its current selected value. @@ -36,6 +37,7 @@ export interface INewSession extends IDisposable { readonly isolationMode: IsolationMode; readonly branch: string | undefined; readonly modelId: string | undefined; + readonly mode: IChatMode | undefined; readonly query: string | undefined; readonly attachedContext: IChatRequestVariableEntry[] | undefined; readonly selectedOptions: ReadonlyMap; @@ -45,6 +47,7 @@ export interface INewSession extends IDisposable { setIsolationMode(mode: IsolationMode): void; setBranch(branch: string | undefined): void; setModelId(modelId: string | undefined): void; + setMode(mode: IChatMode | undefined): void; setQuery(query: string): void; setAttachedContext(context: IChatRequestVariableEntry[] | undefined): void; setOption(optionId: string, value: IChatSessionProviderOptionItem | string): void; @@ -53,6 +56,7 @@ export interface INewSession extends IDisposable { const REPOSITORY_OPTION_ID = 'repository'; const BRANCH_OPTION_ID = 'branch'; const ISOLATION_OPTION_ID = 'isolation'; +const AGENT_OPTION_ID = 'agent'; /** * Local new session for Background agent sessions. @@ -65,6 +69,7 @@ export class LocalNewSession extends Disposable implements INewSession { private _isolationMode: IsolationMode = 'worktree'; private _branch: string | undefined; private _modelId: string | undefined; + private _mode: IChatMode | undefined; private _query: string | undefined; private _attachedContext: IChatRequestVariableEntry[] | undefined; @@ -78,6 +83,7 @@ export class LocalNewSession extends Disposable implements INewSession { get isolationMode(): IsolationMode { return this._isolationMode; } get branch(): string | undefined { return this._branch; } get modelId(): string | undefined { return this._modelId; } + get mode(): IChatMode | undefined { return this._mode; } get query(): string | undefined { return this._query; } get attachedContext(): IChatRequestVariableEntry[] | undefined { return this._attachedContext; } get disabled(): boolean { @@ -134,6 +140,15 @@ export class LocalNewSession extends Disposable implements INewSession { this._modelId = modelId; } + setMode(mode: IChatMode | undefined): void { + if (this._mode?.id !== mode?.id) { + this._mode = mode; + this._onDidChange.fire('agent'); + const modeName = mode?.isBuiltin ? undefined : mode?.name.get(); + this.setOption(AGENT_OPTION_ID, modeName ?? ''); + } + } + setQuery(query: string): void { this._query = query; } @@ -179,6 +194,7 @@ export class RemoteNewSession extends Disposable implements INewSession { get isolationMode(): IsolationMode { return 'worktree'; } get branch(): string | undefined { return undefined; } get modelId(): string | undefined { return this._modelId; } + get mode(): IChatMode | undefined { return undefined; } get query(): string | undefined { return this._query; } get attachedContext(): IChatRequestVariableEntry[] | undefined { return this._attachedContext; } get disabled(): boolean { @@ -230,6 +246,11 @@ export class RemoteNewSession extends Disposable implements INewSession { this._modelId = modelId; } + setMode(_mode: IChatMode | undefined): void { + // Intentionally a no-op: remote sessions do not support client-side mode selection. + // Any mode or behavior differences are determined by the remote session provider/server. + } + setQuery(query: string): void { this._query = query; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 99a317c80c3..cfa0f99049f 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -23,7 +23,9 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { INewSession, LocalNewSession, RemoteNewSession } from '../../chat/browser/newSession.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { isBuiltinChatMode } from '../../../../workbench/contrib/chat/common/chatModes.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; +import { ILanguageModelToolsService } from '../../../../workbench/contrib/chat/common/tools/languageModelToolsService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { isUntitledChatSession } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; import { IGitHubSessionContext } from '../../github/common/types.js'; @@ -146,6 +148,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa @IContextKeyService contextKeyService: IContextKeyService, @ICommandService private readonly commandService: ICommandService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, ) { super(); @@ -343,14 +346,28 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } const contribution = this.chatSessionsService.getChatSessionContribution(session.target); + + // Resolve mode from session's modeId (falls back to Agent) + const modeKind = session.mode?.kind ?? ChatModeKind.Agent; + const modeIsBuiltin = session.mode ? isBuiltinChatMode(session.mode) : true; + const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = modeIsBuiltin ? modeKind : 'custom'; + + const rawModeInstructions = session.mode?.modeInstructions?.get(); + const modeInstructions = rawModeInstructions ? { + name: session.mode!.name.get(), + content: rawModeInstructions.content, + toolReferences: this.toolsService.toToolReferences(rawModeInstructions.toolReferences), + metadata: rawModeInstructions.metadata, + } : undefined; + const sendOptions: IChatSendRequestOptions = { location: ChatAgentLocation.Chat, userSelectedModelId: session.modelId, modeInfo: { - kind: ChatModeKind.Agent, - isBuiltin: true, - modeInstructions: undefined, - modeId: 'agent', + kind: modeKind, + isBuiltin: modeIsBuiltin, + modeInstructions, + modeId, applyCodeBlockSuggestionId: undefined, permissionLevel: options?.permissionLevel ?? ChatPermissionLevel.Default, }, @@ -394,6 +411,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } + // Set the selected mode on the input model so the mode picker reflects it + if (session.mode) { + model.inputModel.setState({ + mode: { id: session.mode.id, kind: session.mode.kind } + }); + } + // Apply selected options (repository, branch, etc.) to the contributed session if (selectedOptions && selectedOptions.size > 0) { const contributedSession = model.contributedChatSession; @@ -444,7 +468,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } if (newSession && !openNewSessionView) { - this.setActiveSession(newSession); + this.setActiveSession(newSession, session); } } @@ -457,7 +481,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.isNewChatSessionContext.set(true); } - private setActiveSession(session: IAgentSession | INewSession | undefined): void { + private setActiveSession(session: IAgentSession | INewSession | undefined, pendingSession?: INewSession): void { let activeSessionItem: IActiveSessionItem | undefined; if (session) { if (isAgentSession(session)) { @@ -467,9 +491,9 @@ export class SessionsManagementService extends Disposable implements ISessionsMa isUntitled: isUntitledChatSession(session.resource), label: session.label, resource: session.resource, - repository, + repository: repository ?? pendingSession?.repoUri, worktree, - worktreeBranchName, + worktreeBranchName: worktreeBranchName ?? pendingSession?.branch, providerType: session.providerType, }; } else { From e9eb2fd0cc2544a7e3e1a5c5c2e67937c03e45bc Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 10 Mar 2026 16:01:17 +0100 Subject: [PATCH 419/448] config: make all settings experiment-aware by default (#300437) * fix: improve experiment handling in configuration defaults * refactor: simplify experiment property structure in configuration schemas * Copilot CLI session f7a6aaaa-477b-497b-bfe8-9a59c2c61ed8 changes * refactor: remove 'experimentMode: auto' from configuration schemas * feat: export ConfigurationDefaultOverridesContribution class and add tests for configuration overrides * fix: update experimentMode assignment logic in configuration properties * fix: ensure experimentMode is always set to 'startup' in property configuration --- .../config/editorConfigurationSchema.ts | 16 -- src/vs/editor/common/config/editorOptions.ts | 18 -- .../common/configurationRegistry.ts | 26 +- src/vs/platform/request/common/request.ts | 6 - .../api/common/configurationExtensionPoint.ts | 6 +- .../browser/workbench.contribution.ts | 6 - .../browserView.contribution.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 41 +-- .../browser/editTelemetry.contribution.ts | 6 - .../browser/extensions.contribution.ts | 3 - .../contrib/inlineChat/common/inlineChat.ts | 16 +- .../performance.contribution.ts | 4 +- .../terminal/common/terminalConfiguration.ts | 9 - .../terminalChatAgentToolsConfiguration.ts | 9 - .../browser/gettingStarted.contribution.ts | 1 - .../browser/configurationService.ts | 8 +- .../configurationDefaultOverrides.test.ts | 261 ++++++++++++++++++ 17 files changed, 279 insertions(+), 159 deletions(-) create mode 100644 src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 2a0641f8c96..762486b5217 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -74,7 +74,6 @@ const editorConfiguration: IConfigurationNode = { nls.localize('wordBasedSuggestions.allDocuments', 'Suggest words from all open documents.'), ], description: nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document and from which documents they are computed."), - experiment: { mode: 'auto' }, }, 'editor.semanticHighlighting.enabled': { enum: [true, false, 'configuredByTheme'], @@ -118,45 +117,30 @@ const editorConfiguration: IConfigurationNode = { default: false, markdownDescription: nls.localize('editor.experimental.treeSitterTelemetry', "Controls whether tree sitter parsing should be turned on and telemetry collected. Setting `#editor.experimental.preferTreeSitter#` for specific languages will take precedence."), tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'editor.experimental.preferTreeSitter.css': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.css', "Controls whether tree sitter parsing should be turned on for css. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for css."), tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'editor.experimental.preferTreeSitter.typescript': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.typescript', "Controls whether tree sitter parsing should be turned on for typescript. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for typescript."), tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'editor.experimental.preferTreeSitter.ini': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.ini', "Controls whether tree sitter parsing should be turned on for ini. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for ini."), tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'editor.experimental.preferTreeSitter.regex': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.regex', "Controls whether tree sitter parsing should be turned on for regex. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for regex."), tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'editor.language.brackets': { type: ['array', 'null'], diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 313793845dd..3a38538bbd5 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -3807,9 +3807,6 @@ class EditorQuickSuggestions extends BaseEditorOption tag.toLowerCase() === 'onexp')) { property.tags = property.tags ?? []; property.tags.push('onExP'); } } else if (property.tags?.some(tag => tag.toLowerCase() === 'onexp')) { - console.error(`Invalid tag 'onExP' found for property '${key}'. Please use 'experiment' property instead.`); - property.experiment = { mode: 'startup' }; + console.error(`Invalid tag 'onExP' found for property '${key}'. Please use 'experimentMode' property instead.`); } const excluded = properties[key].hasOwnProperty('included') && !properties[key].included; diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index df18c523dd7..6a3b81a76b4 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -267,9 +267,6 @@ function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = default: systemCertificatesNodeDefault, markdownDescription: localize('systemCertificatesNode', "Controls whether system certificates should be loaded using Node.js built-in support. Reload the window after changing this setting. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), restricted: true, - experiment: { - mode: 'auto' - } }, 'http.experimental.systemCertificatesV2': { type: 'boolean', @@ -291,9 +288,6 @@ function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = tags: ['experimental'], markdownDescription: localize('networkInterfaceCheckInterval', "Controls the interval in seconds for checking network interface changes to invalidate the proxy cache. Set to -1 to disable. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), restricted: true, - experiment: { - mode: 'auto' - } } } } diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 46304274ef0..8747cf6b679 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -294,11 +294,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { if (extensionConfigurationPolicy?.[key]) { propertyConfiguration.policy = extensionConfigurationPolicy?.[key]; } - if (propertyConfiguration.tags?.some(tag => tag.toLowerCase() === 'onexp')) { - propertyConfiguration.experiment = { - mode: 'startup' - }; - } + propertyConfiguration.experimentMode = 'startup'; seenProperties.add(key); propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 058693c7cfd..685b7a83ce2 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -361,9 +361,6 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('useModal', "Controls whether editors open in a modal overlay."), 'default': product.quality !== 'stable' ? 'some' : 'off', // TODO@bpasero figure out the default tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'workbench.editor.swipeToNavigate': { 'type': 'boolean', @@ -628,9 +625,6 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('workbench.notifications.position.top-right', "Show notifications in the top right corner, similar to OS-level notifications.") ], 'tags': ['experimental'], - 'experiment': { - 'mode': 'auto' - } }, [NotificationsSettings.NOTIFICATIONS_BUTTON]: { 'type': 'boolean', diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts index e4affa6481c..65f7d6ba8d4 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts @@ -161,7 +161,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'workbench.browser.enableChatTools': { type: 'boolean', default: false, - experiment: { mode: 'startup' }, + experimentMode: 'startup', tags: ['experimental'], markdownDescription: localize( { comment: ['This is the description for a setting.'], key: 'browser.enableChatTools' }, diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 93251f797e3..0bef649458f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -263,9 +263,7 @@ configurationRegistry.registerConfiguration({ 'panel': 'always', }, tags: ['experimental'], - experiment: { - mode: 'startup' - } + experimentMode: 'startup' }, 'chat.implicitContext.suggestedContext': { type: 'boolean', @@ -296,9 +294,6 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.editing.explainChanges.enabled', "Controls whether the Explain button in the Chat panel and the Explain Changes context menu in the SCM view are shown. This is an experimental feature."), default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'chat.tips.enabled': { type: 'boolean', @@ -306,9 +301,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.tips.enabled', "Controls whether tips are shown above user messages in chat. New tips are added frequently, so this is a helpful way to stay up to date with the latest features."), default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, 'chat.upvoteAnimation': { type: 'string', @@ -633,9 +625,7 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.mcp.assisted.nuget.enabled.description', "Enables NuGet packages for AI-assisted MCP server installation. Used to install MCP servers by name from the central registry for .NET packages (NuGet.org)."), default: false, tags: ['experimental'], - experiment: { - mode: 'startup' - } + experimentMode: 'startup' }, [ChatConfiguration.ExtensionToolsEnabled]: { type: 'boolean', @@ -726,9 +716,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."), default: true, tags: ['experimental'], - experiment: { - mode: 'auto' - }, policy: { name: 'DeprecatedEditModeHidden', category: PolicyCategory.InteractiveSession, @@ -757,9 +744,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.statusWidget.anonymous.description', "Controls whether anonymous users see the status widget in new chat sessions when rate limited."), default: false, tags: ['experimental', 'advanced'], - experiment: { - mode: 'auto' - } }, [mcpDiscoverySection]: { type: 'object', @@ -970,9 +954,6 @@ configurationRegistry.registerConfiguration({ restricted: true, disallowConfigurationDefault: true, tags: ['experimental', 'prompts', 'reusable prompts', 'prompt snippets', 'instructions'], - experiment: { - mode: 'auto' - } }, [PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS]: { type: 'boolean', @@ -1169,18 +1150,12 @@ configurationRegistry.registerConfiguration({ default: true, markdownDescription: nls.localize('chat.tools.usagesTool.enabled', "Controls whether the usages tool is available for finding references, definitions, and implementations of code symbols."), tags: ['preview'], - experiment: { - mode: 'auto' - } }, 'chat.tools.renameTool.enabled': { type: 'boolean', default: true, markdownDescription: nls.localize('chat.tools.renameTool.enabled', "Controls whether the rename tool is available for renaming code symbols across the workspace."), tags: ['preview'], - experiment: { - mode: 'auto' - } }, [ChatConfiguration.ThinkingPhrases]: { type: 'object', @@ -1222,18 +1197,12 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.allowAnonymousAccess', "Controls whether anonymous access is allowed in chat."), default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [ChatConfiguration.GrowthNotificationEnabled]: { type: 'boolean', description: nls.localize('chat.growthNotification', "Controls whether to show a growth notification in the agent sessions view to encourage new users to try Copilot."), default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [ChatConfiguration.RestoreLastPanelSession]: { type: 'boolean', @@ -1251,17 +1220,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.extensionUnification.enabled', "Enables the unification of GitHub Copilot extensions. When enabled, all GitHub Copilot functionality is served from the GitHub Copilot Chat extension. When disabled, the GitHub Copilot and GitHub Copilot Chat extensions operate independently."), default: true, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [ChatConfiguration.SubagentToolCustomAgents]: { type: 'boolean', description: nls.localize('chat.subagentTool.customAgents', "Whether the runSubagent tool is able to use custom agents. When enabled, the tool can take the name of a custom agent, but it must be given the exact name of the agent."), default: true, - experiment: { - mode: 'auto' - } }, [ChatConfiguration.ChatCustomizationMenuEnabled]: { type: 'boolean', diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts index 170cced5980..5003b49da85 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -35,18 +35,12 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [EDIT_TELEMETRY_DETAILS_SETTING_ID]: { markdownDescription: localize('telemetry.editStats.detailed.enabled', "Controls whether to enable telemetry for detailed edit statistics (only sends statistics if general telemetry is enabled)."), type: 'boolean', default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [EDIT_TELEMETRY_SHOW_STATUS_BAR]: { markdownDescription: localize('telemetry.editStats.showStatusBar', "Controls whether to show the status bar for edit telemetry."), diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 37e6e916e16..3261adc179a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -270,9 +270,6 @@ Registry.as(ConfigurationExtensions.Configuration) description: localize('extensions.allowOpenInModalEditor', "Controls whether extensions and MCP servers open in a modal editor overlay."), default: false, // TODO@bpasero figure out the default for stable and retire this setting tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [VerifyExtensionSignatureConfigKey]: { type: 'boolean', diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 6331855f6a0..39d0117caba 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -37,18 +37,13 @@ Registry.as(Extensions.Configuration).registerConfigurat default: false, type: 'boolean', tags: ['preview'], - experiment: { - mode: 'auto' - } }, [InlineChatConfigKeys.notebookAgent]: { markdownDescription: localize('notebookAgent', "Enable agent-like behavior for inline chat widget in notebooks."), default: false, type: 'boolean', tags: ['experimental'], - experiment: { - mode: 'startup' - } + experimentMode: 'startup' }, [InlineChatConfigKeys.Affordance]: { description: localize('affordance', "Controls whether an inline chat affordance is shown when text is selected."), @@ -60,9 +55,6 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('affordance.gutter', "Show an affordance in the gutter."), localize('affordance.editor', "Show an affordance in the editor at the cursor position."), ], - experiment: { - mode: 'auto' - }, tags: ['experimental'] }, [InlineChatConfigKeys.RenderMode]: { @@ -74,18 +66,12 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('renderMode.zone', "Render inline chat as a zone widget below the current line."), localize('renderMode.hover', "Render inline chat as a hover overlay."), ], - experiment: { - mode: 'auto' - }, tags: ['experimental'] }, [InlineChatConfigKeys.FixDiagnostics]: { description: localize('fixDiagnostics', "Controls whether the Fix action is shown for diagnostics in the editor."), default: true, type: 'boolean', - experiment: { - mode: 'auto' - }, tags: ['experimental'] } } diff --git a/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts index f6dfc81fa52..b0c997ed7f4 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts @@ -42,9 +42,7 @@ Registry.as(ConfigExt.Configuration).registerConfigurati default: false, tags: ['experimental'], markdownDescription: localize('experimental.rendererProfiling', "When enabled, slow renderers are automatically profiled."), - experiment: { - mode: 'startup' - } + experimentMode: 'startup' } } }); diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 132db6d2982..532ddfdf5d7 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -480,9 +480,6 @@ const terminalConfiguration: IStringDictionary = { type: 'boolean', tags: ['preview'], default: false, - experiment: { - mode: 'auto' - }, }, [TerminalSettingId.SplitCwd]: { description: localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."), @@ -600,18 +597,12 @@ const terminalConfiguration: IStringDictionary = { type: 'boolean', default: false, tags: ['experimental', 'advanced'], - experiment: { - mode: 'auto' - } }, [TerminalSettingId.ExperimentalAiProfileGrouping]: { markdownDescription: localize('terminal.integrated.experimental.aiProfileGrouping', "Whether to elevate AI-contributed terminal profiles (for example Copilot CLI and Claude Agent) in the new terminal dropdown."), type: 'boolean', default: false, tags: ['experimental'], - experiment: { - mode: 'auto' - } }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index f50dfea219f..85d6ad2b605 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -509,9 +509,6 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary configuration.preventExperimentOverride); for (const property of properties) { const schema = allProperties[property]; - if (!schema?.experiment) { + if (!schema) { continue; } const defaultValueSource: ConfigurationDefaultSource | undefined = schema.defaultValueSource && !(schema.defaultValueSource instanceof Map) ? schema.defaultValueSource : undefined; @@ -1391,11 +1391,11 @@ class ConfigurationDefaultOverridesContribution extends Disposable implements IW continue; } this.processedExperimentalSettings.add(property); - if (schema.experiment.mode === 'auto') { + if (schema.experimentMode !== 'startup') { this.autoExperimentalSettings.add(property); } try { - const value = await this.workbenchAssignmentService.getTreatment(schema.experiment.name ?? `config.${property}`); + const value = await this.workbenchAssignmentService.getTreatment(`config.${property}`); if (!isUndefined(value) && !equals(value, schema.default)) { overrides[property] = value; } diff --git a/src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts new file mode 100644 index 00000000000..134df762beb --- /dev/null +++ b/src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Extensions, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { IWorkbenchAssignmentService } from '../../../assignment/common/assignmentService.js'; +import { NullExtensionService } from '../../../extensions/common/extensions.js'; +import { mock } from '../../../../../base/test/common/mock.js'; +import { ConfigurationDefaultOverridesContribution, WorkspaceService } from '../../browser/configurationService.js'; + +class MockAssignmentService implements IWorkbenchAssignmentService { + _serviceBrand: undefined; + + private readonly _onDidRefetchAssignments = new Emitter(); + readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event; + + private readonly _onDidCallGetTreatment = new Emitter(); + + private readonly treatments = new Map(); + readonly requestedTreatmentNames: string[] = []; + + setTreatment(name: string, value: unknown): void { + this.treatments.set(name, value); + } + + fireRefetch(): void { + this._onDidRefetchAssignments.fire(); + } + + whenTreatmentRequested(name: string): Promise { + if (this.requestedTreatmentNames.includes(name)) { + return Promise.resolve(); + } + return this.whenNextTreatmentRequested(name); + } + + whenNextTreatmentRequested(name: string): Promise { + return new Promise(resolve => { + const listener = this._onDidCallGetTreatment.event(requestedName => { + if (requestedName === name) { + listener.dispose(); + resolve(); + } + }); + }); + } + + async getCurrentExperiments(): Promise { + return []; + } + + async getTreatment(name: string): Promise { + this.requestedTreatmentNames.push(name); + const value = this.treatments.get(name) as T | undefined; + this._onDidCallGetTreatment.fire(name); + return value; + } + + addTelemetryAssignmentFilter(): void { } + + dispose(): void { + this._onDidRefetchAssignments.dispose(); + this._onDidCallGetTreatment.dispose(); + } +} + +suite('ConfigurationDefaultOverridesContribution', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + const configurationRegistry = Registry.as(Extensions.Configuration); + let assignmentService: MockAssignmentService; + let localDisposables: DisposableStore; + + setup(() => { + localDisposables = disposables.add(new DisposableStore()); + assignmentService = new MockAssignmentService(); + localDisposables.add(assignmentService); + }); + + function createContribution(): ConfigurationDefaultOverridesContribution { + const contribution = new ConfigurationDefaultOverridesContribution( + assignmentService, + new NullExtensionService(), + new class extends mock() { + override reloadConfiguration() { return Promise.resolve(); } + }, + new NullLogService() + ); + localDisposables.add(contribution); + return contribution; + } + + test('applies experiment treatment to a setting without experimentMode', async () => { + configurationRegistry.registerConfiguration({ + id: 'test.experiments', + properties: { + 'test.experiments.noMode': { + type: 'boolean', + default: false, + } + } + }); + localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments', properties: { 'test.experiments.noMode': { type: 'boolean', default: false } } }]) }); + + assignmentService.setTreatment('config.test.experiments.noMode', true); + createContribution(); + + // Wait for the async processing + await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); + + const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); + assert.ok(overrides.has('test.experiments.noMode'), 'The default override should have been registered by experiment'); + assert.strictEqual(overrides.get('test.experiments.noMode')?.value, true); + }); + + test('uses config.{settingId} as the experiment name', async () => { + configurationRegistry.registerConfiguration({ + id: 'test.experiments.naming', + properties: { + 'test.experiments.naming.setting': { + type: 'string', + default: 'original', + } + } + }); + localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.naming', properties: { 'test.experiments.naming.setting': { type: 'string', default: 'original' } } }]) }); + + // Treatment name must be `config.${settingId}` + assignmentService.setTreatment('config.test.experiments.naming.setting', 'experiment-value'); + createContribution(); + + await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); + + assert.ok( + assignmentService.requestedTreatmentNames.includes('config.test.experiments.naming.setting'), + 'Treatment should be looked up using config.{settingId} format' + ); + const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); + assert.ok(overrides.has('test.experiments.naming.setting'), 'The override should have been applied'); + }); + + test('does not apply experiment treatment when value equals default', async () => { + configurationRegistry.registerConfiguration({ + id: 'test.experiments.sameDefault', + properties: { + 'test.experiments.sameDefault.setting': { + type: 'boolean', + default: true, + } + } + }); + localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.sameDefault', properties: { 'test.experiments.sameDefault.setting': { type: 'boolean', default: true } } }]) }); + + // Treatment value same as default + assignmentService.setTreatment('config.test.experiments.sameDefault.setting', true); + createContribution(); + + // Wait for treatment to be processed + await assignmentService.whenTreatmentRequested('config.test.experiments.sameDefault.setting'); + + // Since value equals default, no override should be registered + const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); + assert.ok(!overrides.has('test.experiments.sameDefault.setting'), 'No override should be registered when value equals default'); + }); + + test('setting without experimentMode defaults to auto and re-applies on refetch', async () => { + configurationRegistry.registerConfiguration({ + id: 'test.experiments.autoDefault', + properties: { + 'test.experiments.autoDefault.setting': { + type: 'number', + default: 0, + } + } + }); + localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.autoDefault', properties: { 'test.experiments.autoDefault.setting': { type: 'number', default: 0 } } }]) }); + + assignmentService.setTreatment('config.test.experiments.autoDefault.setting', 42); + createContribution(); + + await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); + + const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); + assert.ok(overrides.has('test.experiments.autoDefault.setting'), 'Override should have been applied on initial load'); + + // Now change the treatment and refetch — auto mode should re-apply + assignmentService.setTreatment('config.test.experiments.autoDefault.setting', 99); + assignmentService.fireRefetch(); + + await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); + + // Verify refetch requested the treatment again + const refetchRequests = assignmentService.requestedTreatmentNames.filter(n => n === 'config.test.experiments.autoDefault.setting'); + assert.ok(refetchRequests.length >= 2, 'Treatment should be requested again on refetch for auto mode settings'); + }); + + test('setting with experimentMode startup does not re-apply on refetch', async () => { + configurationRegistry.registerConfiguration({ + id: 'test.experiments.startupMode', + properties: { + 'test.experiments.startupMode.setting': { + type: 'number', + default: 0, + experimentMode: 'startup', + }, + 'test.experiments.startupMode.sentinel': { + type: 'number', + default: 0, + } + } + }); + localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.startupMode', properties: { 'test.experiments.startupMode.setting': { type: 'number', default: 0, experimentMode: 'startup' }, 'test.experiments.startupMode.sentinel': { type: 'number', default: 0 } } }]) }); + + assignmentService.setTreatment('config.test.experiments.startupMode.setting', 42); + createContribution(); + + await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); + + // Record how many times the startup setting was requested + const countBefore = assignmentService.requestedTreatmentNames.filter(n => n === 'config.test.experiments.startupMode.setting').length; + + // Now change the treatment and refetch — startup mode should NOT re-apply + assignmentService.setTreatment('config.test.experiments.startupMode.setting', 99); + assignmentService.fireRefetch(); + + // Wait for the auto-mode sentinel setting to be re-requested, confirming refetch completed + await assignmentService.whenNextTreatmentRequested('config.test.experiments.startupMode.sentinel'); + + const countAfter = assignmentService.requestedTreatmentNames.filter(n => n === 'config.test.experiments.startupMode.setting').length; + assert.strictEqual(countAfter, countBefore, 'Startup mode setting should not be re-requested on refetch'); + }); + + test('does not apply experiment when treatment returns undefined', async () => { + configurationRegistry.registerConfiguration({ + id: 'test.experiments.noTreatment', + properties: { + 'test.experiments.noTreatment.setting': { + type: 'string', + default: 'original', + } + } + }); + localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.noTreatment', properties: { 'test.experiments.noTreatment.setting': { type: 'string', default: 'original' } } }]) }); + + // No treatment set — getTreatment returns undefined + createContribution(); + + // Wait for the treatment to be requested (even though it returns undefined) + await assignmentService.whenTreatmentRequested('config.test.experiments.noTreatment.setting'); + + const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); + assert.ok(!overrides.has('test.experiments.noTreatment.setting'), 'No override should be registered when treatment is undefined'); + }); +}); From 31fa18a9e4bbe2fc281f311f62dac72d264c97d3 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Tue, 10 Mar 2026 16:13:09 +0100 Subject: [PATCH 420/448] Update @vscode/codicons to version 0.0.45-14 (#300457) chore: update @vscode/codicons version to 0.0.45-14 in package.json and package-lock.json Co-authored-by: mrleemurray --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f782a13bed0..c2a666fcfa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-12", + "@vscode/codicons": "^0.0.45-14", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -3151,9 +3151,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-13", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-13.tgz", - "integrity": "sha512-Q0oIp4r0aBMpvf5MyTqZycs7A7CGQqCmiJvmQ2pEa4HmAqLKHz+o4xzK0zqNfbQB6y35dFY1jJn5QRIi43eTwg==", + "version": "0.0.45-14", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-14.tgz", + "integrity": "sha512-EdrK2NnxNGluUm9ZlU1C5VTLfG1cpO4C0CCXloS+8bDuTbidE1qtwaF5lHPcoDE102WBqBzWA09nVKFoN8RSOA==", "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { diff --git a/package.json b/package.json index db77ce3d628..2080bf15658 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-12", + "@vscode/codicons": "^0.0.45-14", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index ba186896fed..284a4c27dcd 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-12", + "@vscode/codicons": "^0.0.45-14", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-12", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-12.tgz", - "integrity": "sha512-omdtI6hEzpa901Q1s53ndM2vp3ROIVFFCGdz8I6hl4DZ/eKQzEdGYlY09Lnxfh+r9PfSDoyafChGIMIXmNnsRQ==", + "version": "0.0.45-14", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-14.tgz", + "integrity": "sha512-EdrK2NnxNGluUm9ZlU1C5VTLfG1cpO4C0CCXloS+8bDuTbidE1qtwaF5lHPcoDE102WBqBzWA09nVKFoN8RSOA==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index e7561a749a0..1f0bfdd6760 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-12", + "@vscode/codicons": "^0.0.45-14", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", From 4198c6b4c49a9049a0d71c54a622f55a2a61fc6e Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:30:39 +0100 Subject: [PATCH 421/448] Try fix workflow list (#300458) * Try fix workflow list * Fix API name again. --- .github/workflows/api-proposal-version-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api-proposal-version-check.yml b/.github/workflows/api-proposal-version-check.yml index df8ce9ba977..2035ceb2376 100644 --- a/.github/workflows/api-proposal-version-check.yml +++ b/.github/workflows/api-proposal-version-check.yml @@ -110,7 +110,7 @@ jobs: console.log('API proposal version change has been acknowledged.'); // Find the failed workflow run for this PR's head SHA - const { data: runs } = await github.rest.actions.listWorkflowRunsForWorkflow({ + const { data: runs } = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'api-proposal-version-check.yml', From 368df9ea1c33ed67de7a5ffc4936bcc2fcbf19d7 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 10 Mar 2026 16:48:04 +0100 Subject: [PATCH 422/448] hover for attach button in chat widget context action --- src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 79f3e56db66..da219eb37c0 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -609,10 +609,15 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private _createAttachButton(container: HTMLElement): void { const attachButton = dom.append(container, dom.$('.sessions-chat-attach-button')); + const attachButtonLabel = localize('addContext', "Add Context..."); attachButton.tabIndex = 0; attachButton.role = 'button'; - attachButton.title = localize('addContext', "Add Context..."); - attachButton.ariaLabel = localize('addContext', "Add Context..."); + attachButton.ariaLabel = attachButtonLabel; + this._register(this.hoverService.setupDelayedHover(attachButton, { + content: attachButtonLabel, + position: { hoverPosition: HoverPosition.BELOW }, + appearance: { showPointer: true } + })); dom.append(attachButton, renderIcon(Codicon.add)); this._register(dom.addDisposableListener(attachButton, dom.EventType.CLICK, () => { this._contextAttachments.showPicker(this._getContextFolderUri()); From ce4a1cfa8df0fbf155d9eda83d5167d57c085111 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 10 Mar 2026 16:58:40 +0100 Subject: [PATCH 423/448] Revert "config: make all settings experiment-aware by default (#300437)" (#300467) This reverts commit e9eb2fd0cc2544a7e3e1a5c5c2e67937c03e45bc. --- .../config/editorConfigurationSchema.ts | 16 ++ src/vs/editor/common/config/editorOptions.ts | 18 ++ .../common/configurationRegistry.ts | 26 +- src/vs/platform/request/common/request.ts | 6 + .../api/common/configurationExtensionPoint.ts | 6 +- .../browser/workbench.contribution.ts | 6 + .../browserView.contribution.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 41 ++- .../browser/editTelemetry.contribution.ts | 6 + .../browser/extensions.contribution.ts | 3 + .../contrib/inlineChat/common/inlineChat.ts | 16 +- .../performance.contribution.ts | 4 +- .../terminal/common/terminalConfiguration.ts | 9 + .../terminalChatAgentToolsConfiguration.ts | 9 + .../browser/gettingStarted.contribution.ts | 1 + .../browser/configurationService.ts | 8 +- .../configurationDefaultOverrides.test.ts | 261 ------------------ 17 files changed, 159 insertions(+), 279 deletions(-) delete mode 100644 src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 762486b5217..2a0641f8c96 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -74,6 +74,7 @@ const editorConfiguration: IConfigurationNode = { nls.localize('wordBasedSuggestions.allDocuments', 'Suggest words from all open documents.'), ], description: nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document and from which documents they are computed."), + experiment: { mode: 'auto' }, }, 'editor.semanticHighlighting.enabled': { enum: [true, false, 'configuredByTheme'], @@ -117,30 +118,45 @@ const editorConfiguration: IConfigurationNode = { default: false, markdownDescription: nls.localize('editor.experimental.treeSitterTelemetry', "Controls whether tree sitter parsing should be turned on and telemetry collected. Setting `#editor.experimental.preferTreeSitter#` for specific languages will take precedence."), tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'editor.experimental.preferTreeSitter.css': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.css', "Controls whether tree sitter parsing should be turned on for css. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for css."), tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'editor.experimental.preferTreeSitter.typescript': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.typescript', "Controls whether tree sitter parsing should be turned on for typescript. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for typescript."), tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'editor.experimental.preferTreeSitter.ini': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.ini', "Controls whether tree sitter parsing should be turned on for ini. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for ini."), tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'editor.experimental.preferTreeSitter.regex': { type: 'boolean', default: false, markdownDescription: nls.localize('editor.experimental.preferTreeSitter.regex', "Controls whether tree sitter parsing should be turned on for regex. This will take precedence over `#editor.experimental.treeSitterTelemetry#` for regex."), tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'editor.language.brackets': { type: ['array', 'null'], diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3a38538bbd5..313793845dd 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -3807,6 +3807,9 @@ class EditorQuickSuggestions extends BaseEditorOption tag.toLowerCase() === 'onexp')) { property.tags = property.tags ?? []; property.tags.push('onExP'); } } else if (property.tags?.some(tag => tag.toLowerCase() === 'onexp')) { - console.error(`Invalid tag 'onExP' found for property '${key}'. Please use 'experimentMode' property instead.`); + console.error(`Invalid tag 'onExP' found for property '${key}'. Please use 'experiment' property instead.`); + property.experiment = { mode: 'startup' }; } const excluded = properties[key].hasOwnProperty('included') && !properties[key].included; diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 6a3b81a76b4..df18c523dd7 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -267,6 +267,9 @@ function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = default: systemCertificatesNodeDefault, markdownDescription: localize('systemCertificatesNode', "Controls whether system certificates should be loaded using Node.js built-in support. Reload the window after changing this setting. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), restricted: true, + experiment: { + mode: 'auto' + } }, 'http.experimental.systemCertificatesV2': { type: 'boolean', @@ -288,6 +291,9 @@ function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = tags: ['experimental'], markdownDescription: localize('networkInterfaceCheckInterval', "Controls the interval in seconds for checking network interface changes to invalidate the proxy cache. Set to -1 to disable. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), restricted: true, + experiment: { + mode: 'auto' + } } } } diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 8747cf6b679..46304274ef0 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -294,7 +294,11 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { if (extensionConfigurationPolicy?.[key]) { propertyConfiguration.policy = extensionConfigurationPolicy?.[key]; } - propertyConfiguration.experimentMode = 'startup'; + if (propertyConfiguration.tags?.some(tag => tag.toLowerCase() === 'onexp')) { + propertyConfiguration.experiment = { + mode: 'startup' + }; + } seenProperties.add(key); propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 685b7a83ce2..058693c7cfd 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -361,6 +361,9 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('useModal', "Controls whether editors open in a modal overlay."), 'default': product.quality !== 'stable' ? 'some' : 'off', // TODO@bpasero figure out the default tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'workbench.editor.swipeToNavigate': { 'type': 'boolean', @@ -625,6 +628,9 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('workbench.notifications.position.top-right', "Show notifications in the top right corner, similar to OS-level notifications.") ], 'tags': ['experimental'], + 'experiment': { + 'mode': 'auto' + } }, [NotificationsSettings.NOTIFICATIONS_BUTTON]: { 'type': 'boolean', diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts index 65f7d6ba8d4..e4affa6481c 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserView.contribution.ts @@ -161,7 +161,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'workbench.browser.enableChatTools': { type: 'boolean', default: false, - experimentMode: 'startup', + experiment: { mode: 'startup' }, tags: ['experimental'], markdownDescription: localize( { comment: ['This is the description for a setting.'], key: 'browser.enableChatTools' }, diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0bef649458f..93251f797e3 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -263,7 +263,9 @@ configurationRegistry.registerConfiguration({ 'panel': 'always', }, tags: ['experimental'], - experimentMode: 'startup' + experiment: { + mode: 'startup' + } }, 'chat.implicitContext.suggestedContext': { type: 'boolean', @@ -294,6 +296,9 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.editing.explainChanges.enabled', "Controls whether the Explain button in the Chat panel and the Explain Changes context menu in the SCM view are shown. This is an experimental feature."), default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'chat.tips.enabled': { type: 'boolean', @@ -301,6 +306,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.tips.enabled', "Controls whether tips are shown above user messages in chat. New tips are added frequently, so this is a helpful way to stay up to date with the latest features."), default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, 'chat.upvoteAnimation': { type: 'string', @@ -625,7 +633,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.mcp.assisted.nuget.enabled.description', "Enables NuGet packages for AI-assisted MCP server installation. Used to install MCP servers by name from the central registry for .NET packages (NuGet.org)."), default: false, tags: ['experimental'], - experimentMode: 'startup' + experiment: { + mode: 'startup' + } }, [ChatConfiguration.ExtensionToolsEnabled]: { type: 'boolean', @@ -716,6 +726,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."), default: true, tags: ['experimental'], + experiment: { + mode: 'auto' + }, policy: { name: 'DeprecatedEditModeHidden', category: PolicyCategory.InteractiveSession, @@ -744,6 +757,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.statusWidget.anonymous.description', "Controls whether anonymous users see the status widget in new chat sessions when rate limited."), default: false, tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + } }, [mcpDiscoverySection]: { type: 'object', @@ -954,6 +970,9 @@ configurationRegistry.registerConfiguration({ restricted: true, disallowConfigurationDefault: true, tags: ['experimental', 'prompts', 'reusable prompts', 'prompt snippets', 'instructions'], + experiment: { + mode: 'auto' + } }, [PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS]: { type: 'boolean', @@ -1150,12 +1169,18 @@ configurationRegistry.registerConfiguration({ default: true, markdownDescription: nls.localize('chat.tools.usagesTool.enabled', "Controls whether the usages tool is available for finding references, definitions, and implementations of code symbols."), tags: ['preview'], + experiment: { + mode: 'auto' + } }, 'chat.tools.renameTool.enabled': { type: 'boolean', default: true, markdownDescription: nls.localize('chat.tools.renameTool.enabled', "Controls whether the rename tool is available for renaming code symbols across the workspace."), tags: ['preview'], + experiment: { + mode: 'auto' + } }, [ChatConfiguration.ThinkingPhrases]: { type: 'object', @@ -1197,12 +1222,18 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.allowAnonymousAccess', "Controls whether anonymous access is allowed in chat."), default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [ChatConfiguration.GrowthNotificationEnabled]: { type: 'boolean', description: nls.localize('chat.growthNotification', "Controls whether to show a growth notification in the agent sessions view to encourage new users to try Copilot."), default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [ChatConfiguration.RestoreLastPanelSession]: { type: 'boolean', @@ -1220,11 +1251,17 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.extensionUnification.enabled', "Enables the unification of GitHub Copilot extensions. When enabled, all GitHub Copilot functionality is served from the GitHub Copilot Chat extension. When disabled, the GitHub Copilot and GitHub Copilot Chat extensions operate independently."), default: true, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [ChatConfiguration.SubagentToolCustomAgents]: { type: 'boolean', description: nls.localize('chat.subagentTool.customAgents', "Whether the runSubagent tool is able to use custom agents. When enabled, the tool can take the name of a custom agent, but it must be given the exact name of the agent."), default: true, + experiment: { + mode: 'auto' + } }, [ChatConfiguration.ChatCustomizationMenuEnabled]: { type: 'boolean', diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts index 5003b49da85..170cced5980 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -35,12 +35,18 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [EDIT_TELEMETRY_DETAILS_SETTING_ID]: { markdownDescription: localize('telemetry.editStats.detailed.enabled', "Controls whether to enable telemetry for detailed edit statistics (only sends statistics if general telemetry is enabled)."), type: 'boolean', default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [EDIT_TELEMETRY_SHOW_STATUS_BAR]: { markdownDescription: localize('telemetry.editStats.showStatusBar', "Controls whether to show the status bar for edit telemetry."), diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 3261adc179a..37e6e916e16 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -270,6 +270,9 @@ Registry.as(ConfigurationExtensions.Configuration) description: localize('extensions.allowOpenInModalEditor', "Controls whether extensions and MCP servers open in a modal editor overlay."), default: false, // TODO@bpasero figure out the default for stable and retire this setting tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [VerifyExtensionSignatureConfigKey]: { type: 'boolean', diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 39d0117caba..6331855f6a0 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -37,13 +37,18 @@ Registry.as(Extensions.Configuration).registerConfigurat default: false, type: 'boolean', tags: ['preview'], + experiment: { + mode: 'auto' + } }, [InlineChatConfigKeys.notebookAgent]: { markdownDescription: localize('notebookAgent', "Enable agent-like behavior for inline chat widget in notebooks."), default: false, type: 'boolean', tags: ['experimental'], - experimentMode: 'startup' + experiment: { + mode: 'startup' + } }, [InlineChatConfigKeys.Affordance]: { description: localize('affordance', "Controls whether an inline chat affordance is shown when text is selected."), @@ -55,6 +60,9 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('affordance.gutter', "Show an affordance in the gutter."), localize('affordance.editor', "Show an affordance in the editor at the cursor position."), ], + experiment: { + mode: 'auto' + }, tags: ['experimental'] }, [InlineChatConfigKeys.RenderMode]: { @@ -66,12 +74,18 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('renderMode.zone', "Render inline chat as a zone widget below the current line."), localize('renderMode.hover', "Render inline chat as a hover overlay."), ], + experiment: { + mode: 'auto' + }, tags: ['experimental'] }, [InlineChatConfigKeys.FixDiagnostics]: { description: localize('fixDiagnostics', "Controls whether the Fix action is shown for diagnostics in the editor."), default: true, type: 'boolean', + experiment: { + mode: 'auto' + }, tags: ['experimental'] } } diff --git a/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts index b0c997ed7f4..f6dfc81fa52 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/performance.contribution.ts @@ -42,7 +42,9 @@ Registry.as(ConfigExt.Configuration).registerConfigurati default: false, tags: ['experimental'], markdownDescription: localize('experimental.rendererProfiling', "When enabled, slow renderers are automatically profiled."), - experimentMode: 'startup' + experiment: { + mode: 'startup' + } } } }); diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 532ddfdf5d7..132db6d2982 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -480,6 +480,9 @@ const terminalConfiguration: IStringDictionary = { type: 'boolean', tags: ['preview'], default: false, + experiment: { + mode: 'auto' + }, }, [TerminalSettingId.SplitCwd]: { description: localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."), @@ -597,12 +600,18 @@ const terminalConfiguration: IStringDictionary = { type: 'boolean', default: false, tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + } }, [TerminalSettingId.ExperimentalAiProfileGrouping]: { markdownDescription: localize('terminal.integrated.experimental.aiProfileGrouping', "Whether to elevate AI-contributed terminal profiles (for example Copilot CLI and Claude Agent) in the new terminal dropdown."), type: 'boolean', default: false, tags: ['experimental'], + experiment: { + mode: 'auto' + } }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 85d6ad2b605..f50dfea219f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -509,6 +509,9 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary configuration.preventExperimentOverride); for (const property of properties) { const schema = allProperties[property]; - if (!schema) { + if (!schema?.experiment) { continue; } const defaultValueSource: ConfigurationDefaultSource | undefined = schema.defaultValueSource && !(schema.defaultValueSource instanceof Map) ? schema.defaultValueSource : undefined; @@ -1391,11 +1391,11 @@ export class ConfigurationDefaultOverridesContribution extends Disposable implem continue; } this.processedExperimentalSettings.add(property); - if (schema.experimentMode !== 'startup') { + if (schema.experiment.mode === 'auto') { this.autoExperimentalSettings.add(property); } try { - const value = await this.workbenchAssignmentService.getTreatment(`config.${property}`); + const value = await this.workbenchAssignmentService.getTreatment(schema.experiment.name ?? `config.${property}`); if (!isUndefined(value) && !equals(value, schema.default)) { overrides[property] = value; } diff --git a/src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts deleted file mode 100644 index 134df762beb..00000000000 --- a/src/vs/workbench/services/configuration/test/browser/configurationDefaultOverrides.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { Extensions, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; -import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { IWorkbenchAssignmentService } from '../../../assignment/common/assignmentService.js'; -import { NullExtensionService } from '../../../extensions/common/extensions.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { ConfigurationDefaultOverridesContribution, WorkspaceService } from '../../browser/configurationService.js'; - -class MockAssignmentService implements IWorkbenchAssignmentService { - _serviceBrand: undefined; - - private readonly _onDidRefetchAssignments = new Emitter(); - readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event; - - private readonly _onDidCallGetTreatment = new Emitter(); - - private readonly treatments = new Map(); - readonly requestedTreatmentNames: string[] = []; - - setTreatment(name: string, value: unknown): void { - this.treatments.set(name, value); - } - - fireRefetch(): void { - this._onDidRefetchAssignments.fire(); - } - - whenTreatmentRequested(name: string): Promise { - if (this.requestedTreatmentNames.includes(name)) { - return Promise.resolve(); - } - return this.whenNextTreatmentRequested(name); - } - - whenNextTreatmentRequested(name: string): Promise { - return new Promise(resolve => { - const listener = this._onDidCallGetTreatment.event(requestedName => { - if (requestedName === name) { - listener.dispose(); - resolve(); - } - }); - }); - } - - async getCurrentExperiments(): Promise { - return []; - } - - async getTreatment(name: string): Promise { - this.requestedTreatmentNames.push(name); - const value = this.treatments.get(name) as T | undefined; - this._onDidCallGetTreatment.fire(name); - return value; - } - - addTelemetryAssignmentFilter(): void { } - - dispose(): void { - this._onDidRefetchAssignments.dispose(); - this._onDidCallGetTreatment.dispose(); - } -} - -suite('ConfigurationDefaultOverridesContribution', () => { - - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - const configurationRegistry = Registry.as(Extensions.Configuration); - let assignmentService: MockAssignmentService; - let localDisposables: DisposableStore; - - setup(() => { - localDisposables = disposables.add(new DisposableStore()); - assignmentService = new MockAssignmentService(); - localDisposables.add(assignmentService); - }); - - function createContribution(): ConfigurationDefaultOverridesContribution { - const contribution = new ConfigurationDefaultOverridesContribution( - assignmentService, - new NullExtensionService(), - new class extends mock() { - override reloadConfiguration() { return Promise.resolve(); } - }, - new NullLogService() - ); - localDisposables.add(contribution); - return contribution; - } - - test('applies experiment treatment to a setting without experimentMode', async () => { - configurationRegistry.registerConfiguration({ - id: 'test.experiments', - properties: { - 'test.experiments.noMode': { - type: 'boolean', - default: false, - } - } - }); - localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments', properties: { 'test.experiments.noMode': { type: 'boolean', default: false } } }]) }); - - assignmentService.setTreatment('config.test.experiments.noMode', true); - createContribution(); - - // Wait for the async processing - await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); - - const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); - assert.ok(overrides.has('test.experiments.noMode'), 'The default override should have been registered by experiment'); - assert.strictEqual(overrides.get('test.experiments.noMode')?.value, true); - }); - - test('uses config.{settingId} as the experiment name', async () => { - configurationRegistry.registerConfiguration({ - id: 'test.experiments.naming', - properties: { - 'test.experiments.naming.setting': { - type: 'string', - default: 'original', - } - } - }); - localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.naming', properties: { 'test.experiments.naming.setting': { type: 'string', default: 'original' } } }]) }); - - // Treatment name must be `config.${settingId}` - assignmentService.setTreatment('config.test.experiments.naming.setting', 'experiment-value'); - createContribution(); - - await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); - - assert.ok( - assignmentService.requestedTreatmentNames.includes('config.test.experiments.naming.setting'), - 'Treatment should be looked up using config.{settingId} format' - ); - const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); - assert.ok(overrides.has('test.experiments.naming.setting'), 'The override should have been applied'); - }); - - test('does not apply experiment treatment when value equals default', async () => { - configurationRegistry.registerConfiguration({ - id: 'test.experiments.sameDefault', - properties: { - 'test.experiments.sameDefault.setting': { - type: 'boolean', - default: true, - } - } - }); - localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.sameDefault', properties: { 'test.experiments.sameDefault.setting': { type: 'boolean', default: true } } }]) }); - - // Treatment value same as default - assignmentService.setTreatment('config.test.experiments.sameDefault.setting', true); - createContribution(); - - // Wait for treatment to be processed - await assignmentService.whenTreatmentRequested('config.test.experiments.sameDefault.setting'); - - // Since value equals default, no override should be registered - const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); - assert.ok(!overrides.has('test.experiments.sameDefault.setting'), 'No override should be registered when value equals default'); - }); - - test('setting without experimentMode defaults to auto and re-applies on refetch', async () => { - configurationRegistry.registerConfiguration({ - id: 'test.experiments.autoDefault', - properties: { - 'test.experiments.autoDefault.setting': { - type: 'number', - default: 0, - } - } - }); - localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.autoDefault', properties: { 'test.experiments.autoDefault.setting': { type: 'number', default: 0 } } }]) }); - - assignmentService.setTreatment('config.test.experiments.autoDefault.setting', 42); - createContribution(); - - await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); - - const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); - assert.ok(overrides.has('test.experiments.autoDefault.setting'), 'Override should have been applied on initial load'); - - // Now change the treatment and refetch — auto mode should re-apply - assignmentService.setTreatment('config.test.experiments.autoDefault.setting', 99); - assignmentService.fireRefetch(); - - await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); - - // Verify refetch requested the treatment again - const refetchRequests = assignmentService.requestedTreatmentNames.filter(n => n === 'config.test.experiments.autoDefault.setting'); - assert.ok(refetchRequests.length >= 2, 'Treatment should be requested again on refetch for auto mode settings'); - }); - - test('setting with experimentMode startup does not re-apply on refetch', async () => { - configurationRegistry.registerConfiguration({ - id: 'test.experiments.startupMode', - properties: { - 'test.experiments.startupMode.setting': { - type: 'number', - default: 0, - experimentMode: 'startup', - }, - 'test.experiments.startupMode.sentinel': { - type: 'number', - default: 0, - } - } - }); - localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.startupMode', properties: { 'test.experiments.startupMode.setting': { type: 'number', default: 0, experimentMode: 'startup' }, 'test.experiments.startupMode.sentinel': { type: 'number', default: 0 } } }]) }); - - assignmentService.setTreatment('config.test.experiments.startupMode.setting', 42); - createContribution(); - - await Event.toPromise(configurationRegistry.onDidUpdateConfiguration); - - // Record how many times the startup setting was requested - const countBefore = assignmentService.requestedTreatmentNames.filter(n => n === 'config.test.experiments.startupMode.setting').length; - - // Now change the treatment and refetch — startup mode should NOT re-apply - assignmentService.setTreatment('config.test.experiments.startupMode.setting', 99); - assignmentService.fireRefetch(); - - // Wait for the auto-mode sentinel setting to be re-requested, confirming refetch completed - await assignmentService.whenNextTreatmentRequested('config.test.experiments.startupMode.sentinel'); - - const countAfter = assignmentService.requestedTreatmentNames.filter(n => n === 'config.test.experiments.startupMode.setting').length; - assert.strictEqual(countAfter, countBefore, 'Startup mode setting should not be re-requested on refetch'); - }); - - test('does not apply experiment when treatment returns undefined', async () => { - configurationRegistry.registerConfiguration({ - id: 'test.experiments.noTreatment', - properties: { - 'test.experiments.noTreatment.setting': { - type: 'string', - default: 'original', - } - } - }); - localDisposables.add({ dispose: () => configurationRegistry.deregisterConfigurations([{ id: 'test.experiments.noTreatment', properties: { 'test.experiments.noTreatment.setting': { type: 'string', default: 'original' } } }]) }); - - // No treatment set — getTreatment returns undefined - createContribution(); - - // Wait for the treatment to be requested (even though it returns undefined) - await assignmentService.whenTreatmentRequested('config.test.experiments.noTreatment.setting'); - - const overrides = configurationRegistry.getConfigurationDefaultsOverrides(); - assert.ok(!overrides.has('test.experiments.noTreatment.setting'), 'No override should be registered when treatment is undefined'); - }); -}); From 3ef1baf0977a6d6ce001ff0c7127475db8036f1c Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:02:25 -0700 Subject: [PATCH 424/448] Consolidate MockChatService We have two `MockChatService` classes. This makes updating the interface slow since you have to update both locations. The two also behave slightly differently --- .../localAgentSessionsController.test.ts | 238 ++---------------- .../common/chatService/mockChatService.ts | 236 +++++++++-------- 2 files changed, 150 insertions(+), 324 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index f5ede2843bf..fdc31ae26eb 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -6,21 +6,21 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; import { LocalAgentsSessionsController } from '../../../browser/agentSessions/localAgentSessionsController.js'; +import { IChatService, ResponseModelState } from '../../../common/chatService/chatService.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { IChatModel, IChatRequestModel, IChatResponseModel } from '../../../common/model/chatModel.js'; -import { ChatRequestQueueKind, IChatDetail, IChatQuestionAnswers, IChatService, IChatSessionStartOptions, ResponseModelState } from '../../../common/chatService/chatService.js'; -import { ChatSessionStatus, IChatSessionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; -import { ChatAgentLocation } from '../../../common/constants.js'; +import { MockChatService } from '../../common/chatService/mockChatService.js'; import { MockChatSessionsService } from '../../common/mockChatSessionsService.js'; function createTestTiming(options?: { @@ -36,204 +36,6 @@ function createTestTiming(options?: { }; } -class MockChatService implements IChatService { - private readonly _chatModels: ISettableObservable> = observableValue('chatModels', []); - readonly chatModels = this._chatModels; - requestInProgressObs = observableValue('name', false); - _serviceBrand: undefined; - editingSessions = []; - transferredSessionResource = undefined; - readonly onDidSubmitRequest = Event.None; - readonly onDidCreateModel = Event.None; - - private sessions = new Map(); - private liveSessionItems: IChatDetail[] = []; - private historySessionItems: IChatDetail[] = []; - - private readonly _onDidDisposeSession = new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>(); - readonly onDidDisposeSession = this._onDidDisposeSession.event; - - fireDidDisposeSession(sessionResource: URI[]): void { - this._onDidDisposeSession.fire({ sessionResource, reason: 'cleared' }); - } - - setSaveModelsEnabled(enabled: boolean): void { - - } - - processPendingRequests(sessionResource: URI): void { - - } - - setLiveSessionItems(items: IChatDetail[]): void { - this.liveSessionItems = items; - } - - setHistorySessionItems(items: IChatDetail[]): void { - this.historySessionItems = items; - } - - addSession(sessionResource: URI, session: IChatModel): void { - this.sessions.set(sessionResource.toString(), session); - // Update the chatModels observable - this._chatModels.set([...this.sessions.values()], undefined); - } - - removeSession(sessionResource: URI): void { - this.sessions.delete(sessionResource.toString()); - // Update the chatModels observable - this._chatModels.set([...this.sessions.values()], undefined); - } - - isEnabled(_location: ChatAgentLocation): boolean { - return true; - } - - hasSessions(): boolean { - return this.sessions.size > 0; - } - - getProviderInfos() { - return []; - } - - startNewLocalSession(_location: ChatAgentLocation, _options?: IChatSessionStartOptions): any { - throw new Error('Method not implemented.'); - } - - getSession(sessionResource: URI): IChatModel | undefined { - return this.sessions.get(sessionResource.toString()); - } - - getLatestRequest(): IChatRequestModel | undefined { - return undefined; - } - - acquireOrRestoreSession(_sessionResource: URI): Promise { - throw new Error('Method not implemented.'); - } - - getSessionTitle(_sessionResource: URI): string | undefined { - return undefined; - } - - loadSessionFromData(_data: any): any { - throw new Error('Method not implemented.'); - } - - acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - } - - acquireExistingSession(_sessionResource: URI): any { - return undefined; - } - - setSessionTitle(_sessionResource: URI, _title: string): void { } - - appendProgress(_request: IChatRequestModel, _progress: any): void { } - - sendRequest(_sessionResource: URI, _message: string): Promise { - throw new Error('Method not implemented.'); - } - - resendRequest(_request: IChatRequestModel, _options?: any): Promise { - throw new Error('Method not implemented.'); - } - - adoptRequest(_sessionResource: URI, _request: IChatRequestModel): Promise { - throw new Error('Method not implemented.'); - } - - removeRequest(_sessionResource: URI, _requestId: string): Promise { - throw new Error('Method not implemented.'); - } - - async cancelCurrentRequestForSession(_sessionResource: URI, _source?: string): Promise { } - - setYieldRequested(_sessionResource: URI): void { } - - removePendingRequest(_sessionResource: URI, _requestId: string): void { } - - setPendingRequests(_sessionResource: URI, _requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void { } - - addCompleteRequest(): void { } - - async getLocalSessionHistory(): Promise { - return this.historySessionItems; - } - - async clearAllHistoryEntries(): Promise { } - - async removeHistoryEntry(_resource: URI): Promise { } - - readonly onDidPerformUserAction = Event.None; - - notifyUserAction(_event: any): void { } - - readonly onDidReceiveQuestionCarouselAnswer = Event.None; - - notifyQuestionCarouselAnswer(_requestId: string, _resolveId: string, _answers: IChatQuestionAnswers | undefined): void { } - - async transferChatSession(): Promise { } - - setChatSessionTitle(): void { } - - isEditingLocation(_location: ChatAgentLocation): boolean { - return false; - } - - getChatStorageFolder(): URI { - return URI.file('/tmp'); - } - - logChatIndex(): void { } - - activateDefaultAgent(_location: ChatAgentLocation): Promise { - return Promise.resolve(); - } - - getChatSessionFromInternalUri(_sessionResource: URI): any { - return undefined; - } - - async getLiveSessionItems(): Promise { - return this.liveSessionItems; - } - - async getHistorySessionItems(): Promise { - return this.historySessionItems; - } - - waitForModelDisposals(): Promise { - return Promise.resolve(); - } - - getMetadataForSession(sessionResource: URI): Promise { - throw new Error('Method not implemented.'); - } - - - private onChange?: () => void; - - registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { - // Store the emitter so tests can trigger it - this.onChange = onChange; - return { - dispose: () => { - this.onChange = undefined; - } - }; - } - - // Helper method for tests to trigger progress events - triggerProgressEvent(): void { - if (this.onChange) { - this.onChange(); - } - } -} - function createMockChatModel(options: { sessionResource: URI; hasRequests?: boolean; @@ -364,7 +166,7 @@ suite('LocalAgentsSessionsController', () => { timestamp: Date.now() }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Test Session', @@ -415,7 +217,7 @@ suite('LocalAgentsSessionsController', () => { hasRequests: true }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Live Session', @@ -452,7 +254,7 @@ suite('LocalAgentsSessionsController', () => { requestInProgress: true }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'In Progress Session', @@ -483,7 +285,7 @@ suite('LocalAgentsSessionsController', () => { lastResponseHasError: false }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Completed Session', @@ -513,7 +315,7 @@ suite('LocalAgentsSessionsController', () => { lastResponseCanceled: true }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Canceled Session', @@ -543,7 +345,7 @@ suite('LocalAgentsSessionsController', () => { lastResponseHasError: true }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Error Session', @@ -588,7 +390,7 @@ suite('LocalAgentsSessionsController', () => { } }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Stats Session', @@ -634,7 +436,7 @@ suite('LocalAgentsSessionsController', () => { } }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'No Stats Session', @@ -665,7 +467,7 @@ suite('LocalAgentsSessionsController', () => { timestamp: modelTimestamp }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Timing Session', @@ -719,7 +521,7 @@ suite('LocalAgentsSessionsController', () => { lastResponseCompletedAt: completedAt }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'EndTime Session', @@ -748,7 +550,7 @@ suite('LocalAgentsSessionsController', () => { hasRequests: true }); - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); mockChatService.setLiveSessionItems([{ sessionResource, title: 'Icon Session', @@ -779,7 +581,7 @@ suite('LocalAgentsSessionsController', () => { }); // Add the session first - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); let changeEventCount = 0; disposables.add(controller.onDidChangeChatSessionItems(() => { @@ -805,7 +607,7 @@ suite('LocalAgentsSessionsController', () => { }); // Add the session first - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); let changeEventCount = 0; disposables.add(controller.onDidChangeChatSessionItems(() => { @@ -830,7 +632,7 @@ suite('LocalAgentsSessionsController', () => { }); // Add the session first - mockChatService.addSession(sessionResource, mockModel); + mockChatService.addSession(mockModel); // Now remove the session - the observable should trigger cleanup mockChatService.removeSession(sessionResource); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index ca141665f03..e268a3a137e 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -4,168 +4,192 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../../base/common/event.js'; -import { ResourceMap } from '../../../../../../base/common/map.js'; -import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../../common/model/chatModel.js'; -import { IParsedChatRequest } from '../../../common/requestParser/chatParserTypes.js'; -import { ChatRequestQueueKind, ChatSendResult, IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatQuestionAnswers, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; -import { ChatAgentLocation } from '../../../common/constants.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ChatRequestQueueKind, ChatSendResult, IChatDetail, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; +import { IChatModel, IChatRequestModel, IExportableChatData, ISerializableChatData } from '../../../common/model/chatModel.js'; export class MockChatService implements IChatService { - chatModels: IObservable> = observableValue('chatModels', []); + private readonly _chatModels: ISettableObservable> = observableValue('chatModels', []); + readonly chatModels = this._chatModels; requestInProgressObs = observableValue('name', false); _serviceBrand: undefined; editingSessions = []; - transferredSessionResource: URI | undefined; - readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }> = Event.None; - readonly onDidCreateModel: Event = Event.None; + transferredSessionResource = undefined; + readonly onDidSubmitRequest = Event.None; + readonly onDidCreateModel = Event.None; - private sessions = new ResourceMap(); + private sessions = new Map(); + private liveSessionItems: IChatDetail[] = []; + private historySessionItems: IChatDetail[] = []; + + private readonly _onDidDisposeSession = new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>(); + readonly onDidDisposeSession = this._onDidDisposeSession.event; + + fireDidDisposeSession(sessionResource: URI[]): void { + this._onDidDisposeSession.fire({ sessionResource, reason: 'cleared' }); + } setSaveModelsEnabled(enabled: boolean): void { } - isEnabled(location: ChatAgentLocation): boolean { - throw new Error('Method not implemented.'); - } - hasSessions(): boolean { - throw new Error('Method not implemented.'); - } - getProviderInfos(): IChatProviderInfo[] { - throw new Error('Method not implemented.'); - } - startNewLocalSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { - throw new Error('Method not implemented.'); - } - addSession(session: IChatModel): void { - this.sessions.set(session.sessionResource, session); - } - getSession(sessionResource: URI): IChatModel | undefined { - // eslint-disable-next-line local/code-no-dangerous-type-assertions - return this.sessions.get(sessionResource) ?? {} as IChatModel; - } - getLatestRequest(): IChatRequestModel | undefined { - return undefined; - } - async acquireOrRestoreSession(sessionResource: URI): Promise { - throw new Error('Method not implemented.'); - } - getSessionTitle(sessionResource: URI): string | undefined { - throw new Error('Method not implemented.'); - } - loadSessionFromData(data: ISerializableChatData): IChatModelReference { - throw new Error('Method not implemented.'); - } - acquireOrLoadSession(resource: URI, position: ChatAgentLocation, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - } - acquireExistingSession(sessionResource: URI): IChatModelReference | undefined { - return undefined; - } - setSessionTitle(sessionResource: URI, title: string): void { - throw new Error('Method not implemented.'); - } - appendProgress(request: IChatRequestModel, progress: IChatProgress): void { - } processPendingRequests(sessionResource: URI): void { } - /** - * Returns whether the request was accepted. - */ - sendRequest(sessionResource: URI, message: string): Promise { + + setLiveSessionItems(items: IChatDetail[]): void { + this.liveSessionItems = items; + } + + setHistorySessionItems(items: IChatDetail[]): void { + this.historySessionItems = items; + } + + addSession(session: IChatModel): void { + this.sessions.set(session.sessionResource.toString(), session); + // Update the chatModels observable + this._chatModels.set([...this.sessions.values()], undefined); + } + + removeSession(sessionResource: URI): void { + this.sessions.delete(sessionResource.toString()); + // Update the chatModels observable + this._chatModels.set([...this.sessions.values()], undefined); + } + + isEnabled(_location: ChatAgentLocation): boolean { + return true; + } + + hasSessions(): boolean { + return this.sessions.size > 0; + } + + getProviderInfos() { + return []; + } + + startNewLocalSession(_location: ChatAgentLocation, _options?: IChatSessionStartOptions): IChatModelReference { throw new Error('Method not implemented.'); } - resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions | undefined): Promise { + + getSession(sessionResource: URI): IChatModel | undefined { + return this.sessions.get(sessionResource.toString()); + } + + getLatestRequest(): IChatRequestModel | undefined { + return undefined; + } + + acquireOrRestoreSession(_sessionResource: URI): Promise { throw new Error('Method not implemented.'); } - adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise { + + getSessionTitle(_sessionResource: URI): string | undefined { + return undefined; + } + + loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference { throw new Error('Method not implemented.'); } - removeRequest(sessionResource: URI, requestId: string): Promise { + + acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - async cancelCurrentRequestForSession(sessionResource: URI, source?: string): Promise { + + acquireExistingSession(_sessionResource: URI): IChatModelReference | undefined { + return undefined; + } + + setSessionTitle(_sessionResource: URI, _title: string): void { } + + appendProgress(_request: IChatRequestModel, _progress: IChatProgress): void { } + + sendRequest(_sessionResource: URI, _message: string): Promise { throw new Error('Method not implemented.'); } - setYieldRequested(sessionResource: URI): void { + + resendRequest(_request: IChatRequestModel, _options?: IChatSendRequestOptions): Promise { throw new Error('Method not implemented.'); } - removePendingRequest(sessionResource: URI, requestId: string): void { + + adoptRequest(_sessionResource: URI, _request: IChatRequestModel): Promise { throw new Error('Method not implemented.'); } - setPendingRequests(sessionResource: URI, requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void { - throw new Error('Method not implemented.'); - } - addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void { + + removeRequest(_sessionResource: URI, _requestId: string): Promise { throw new Error('Method not implemented.'); } + + async cancelCurrentRequestForSession(_sessionResource: URI, _source?: string): Promise { } + + setYieldRequested(_sessionResource: URI): void { } + + removePendingRequest(_sessionResource: URI, _requestId: string): void { } + + setPendingRequests(_sessionResource: URI, _requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void { } + + addCompleteRequest(): void { } + async getLocalSessionHistory(): Promise { - throw new Error('Method not implemented.'); - } - async clearAllHistoryEntries() { - throw new Error('Method not implemented.'); - } - async removeHistoryEntry(resource: URI) { - throw new Error('Method not implemented.'); + return this.historySessionItems; } - readonly onDidPerformUserAction: Event = undefined!; - notifyUserAction(event: IChatUserActionEvent): void { - throw new Error('Method not implemented.'); - } - readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: IChatQuestionAnswers | undefined }> = undefined!; - notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: IChatQuestionAnswers | undefined): void { - throw new Error('Method not implemented.'); - } - readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!; + async clearAllHistoryEntries(): Promise { } - async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { - throw new Error('Method not implemented.'); - } + async removeHistoryEntry(_resource: URI): Promise { } - setChatSessionTitle(sessionResource: URI, title: string): void { - throw new Error('Method not implemented.'); - } + readonly onDidPerformUserAction = Event.None; - isEditingLocation(location: ChatAgentLocation): boolean { - throw new Error('Method not implemented.'); + notifyUserAction(_event: IChatUserActionEvent): void { } + + readonly onDidReceiveQuestionCarouselAnswer = Event.None; + + notifyQuestionCarouselAnswer(_requestId: string, _resolveId: string, _answers: Record | undefined): void { } + + async transferChatSession(): Promise { } + + setChatSessionTitle(): void { } + + isEditingLocation(_location: ChatAgentLocation): boolean { + return false; } getChatStorageFolder(): URI { - throw new Error('Method not implemented.'); + return URI.file('/tmp'); } - logChatIndex(): void { - throw new Error('Method not implemented.'); + logChatIndex(): void { } + + activateDefaultAgent(_location: ChatAgentLocation): Promise { + return Promise.resolve(); } - activateDefaultAgent(location: ChatAgentLocation): Promise { - throw new Error('Method not implemented.'); - } - - getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined { - throw new Error('Method not implemented.'); + getChatSessionFromInternalUri(_sessionResource: URI): IChatSessionContext | undefined { + return undefined; } async getLiveSessionItems(): Promise { - throw new Error('Method not implemented.'); + return this.liveSessionItems; } - getHistorySessionItems(): Promise { - throw new Error('Method not implemented.'); + + async getHistorySessionItems(): Promise { + return this.historySessionItems; } waitForModelDisposals(): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve(); } + getMetadataForSession(sessionResource: URI): Promise { throw new Error('Method not implemented.'); } + private onChange?: () => void; registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { From 71d74966ec9d026b1ccc5c8fc57b36e08f1f18ab Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Mar 2026 09:09:40 -0700 Subject: [PATCH 425/448] chat: use softAssertNever for unknown response parts (#300470) Closes https://github.com/microsoft/vscode/issues/300204 --- .../contrib/chat/common/model/chatSessionOperationLog.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index c6da97a55d2..9c69890c9a7 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertNever } from '../../../../../base/common/assert.js'; +import { softAssertNever } from '../../../../../base/common/assert.js'; import { isMarkdownString } from '../../../../../base/common/htmlContent.js'; import { equals as objectsEqual } from '../../../../../base/common/objects.js'; import { isEqual as _urisEqual } from '../../../../../base/common/resources.js'; @@ -90,7 +90,9 @@ const responsePartSchema = Adapt.v Date: Tue, 10 Mar 2026 09:17:48 -0700 Subject: [PATCH 426/448] Browser: CDP over IPC (#300292) --- src/vs/code/electron-main/app.ts | 2 - .../browserView/common/browserViewGroup.ts | 11 +- .../platform/browserView/common/cdp/proxy.ts | 40 ++- .../platform/browserView/common/cdp/types.ts | 2 +- .../browserViewCDPProxyServer.ts | 269 ------------------ .../electron-main/browserViewDebugger.ts | 2 +- .../electron-main/browserViewGroup.ts | 27 +- .../browserViewGroupMainService.ts | 9 +- .../node/browserViewGroupRemoteService.ts | 9 +- .../browserView/node/playwrightService.ts | 27 +- 10 files changed, 98 insertions(+), 300 deletions(-) delete mode 100644 src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ab9fb26c1d2..33ec9f60f2d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -41,7 +41,6 @@ import { ipcBrowserViewChannelName } from '../../platform/browserView/common/bro import { ipcBrowserViewGroupChannelName } from '../../platform/browserView/common/browserViewGroup.js'; import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js'; import { BrowserViewGroupMainService, IBrowserViewGroupMainService } from '../../platform/browserView/electron-main/browserViewGroupMainService.js'; -import { BrowserViewCDPProxyServer, IBrowserViewCDPProxyServer } from '../../platform/browserView/electron-main/browserViewCDPProxyServer.js'; import { NativeParsedArgs } from '../../platform/environment/common/argv.js'; import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js'; import { isLaunchedFromCli } from '../../platform/environment/node/argvHelper.js'; @@ -1063,7 +1062,6 @@ export class CodeApplication extends Disposable { services.set(INativeBrowserElementsMainService, new SyncDescriptor(NativeBrowserElementsMainService, undefined, false /* proxied to other processes */)); // Browser View - services.set(IBrowserViewCDPProxyServer, new SyncDescriptor(BrowserViewCDPProxyServer, undefined, true)); services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */)); services.set(IBrowserViewGroupMainService, new SyncDescriptor(BrowserViewGroupMainService, undefined, false /* proxied to other processes */)); diff --git a/src/vs/platform/browserView/common/browserViewGroup.ts b/src/vs/platform/browserView/common/browserViewGroup.ts index 0c414d9df7b..0851ac7ffe4 100644 --- a/src/vs/platform/browserView/common/browserViewGroup.ts +++ b/src/vs/platform/browserView/common/browserViewGroup.ts @@ -5,6 +5,7 @@ import { Event } from '../../../base/common/event.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; +import { CDPEvent, CDPRequest, CDPResponse } from './cdp/types.js'; export const ipcBrowserViewGroupChannelName = 'browserViewGroup'; @@ -27,10 +28,11 @@ export interface IBrowserViewGroup extends IDisposable { readonly onDidAddView: Event; readonly onDidRemoveView: Event; readonly onDidDestroy: Event; + readonly onCDPMessage: Event; addView(viewId: string): Promise; removeView(viewId: string): Promise; - getDebugWebSocketEndpoint(): Promise; + sendCDPMessage(msg: CDPRequest): Promise; } /** @@ -48,6 +50,7 @@ export interface IBrowserViewGroupService { onDynamicDidAddView(groupId: string): Event; onDynamicDidRemoveView(groupId: string): Event; onDynamicDidDestroy(groupId: string): Event; + onDynamicCDPMessage(groupId: string): Event; /** * Create a new browser view group. @@ -79,9 +82,9 @@ export interface IBrowserViewGroupService { removeViewFromGroup(groupId: string, viewId: string): Promise; /** - * Get a short-lived CDP WebSocket endpoint URL for a specific group. - * The returned URL contains a single-use token. + * Send a CDP message to a group's browser proxy. * @param groupId The group identifier. + * @param message The CDP request. */ - getDebugWebSocketEndpoint(groupId: string): Promise; + sendCDPMessage(groupId: string, message: CDPRequest): Promise; } diff --git a/src/vs/platform/browserView/common/cdp/proxy.ts b/src/vs/platform/browserView/common/cdp/proxy.ts index 85dc5f6d52d..86b3f4af1a5 100644 --- a/src/vs/platform/browserView/common/cdp/proxy.ts +++ b/src/vs/platform/browserView/common/cdp/proxy.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -import { ICDPTarget, CDPEvent, CDPError, CDPServerError, CDPMethodNotFoundError, CDPInvalidParamsError, ICDPConnection, CDPTargetInfo, ICDPBrowserTarget } from './types.js'; +import { ICDPTarget, CDPRequest, CDPResponse, CDPEvent, CDPError, CDPErrorCode, CDPServerError, CDPMethodNotFoundError, CDPInvalidParamsError, ICDPConnection, CDPTargetInfo, ICDPBrowserTarget } from './types.js'; /** * CDP protocol handler for browser-level connections. @@ -95,22 +95,29 @@ export class CDPBrowserProxy extends Disposable implements ICDPConnection { for (const target of this.browserTarget.getTargets()) { void this._targets.register(target); } + + // Mirror typed events to the onMessage channel + this._register(this._onEvent.event(event => { + this._onMessage.fire(event); + })); } // #region Public API - // Events to external client (ICDPConnection) + // Events to external clients private readonly _onEvent = this._register(new Emitter()); readonly onEvent: Event = this._onEvent.event; private readonly _onClose = this._register(new Emitter()); readonly onClose: Event = this._onClose.event; + private readonly _onMessage = this._register(new Emitter()); + readonly onMessage: Event = this._onMessage.event; /** - * Send a CDP message and await the result. + * Send a CDP command and await the result. * Browser-level handlers (Browser.*, Target.*) are checked first. * Other commands are routed to the page session identified by sessionId. */ - async sendMessage(method: string, params: unknown = {}, sessionId?: string): Promise { + async sendCommand(method: string, params: unknown = {}, sessionId?: string): Promise { try { // Browser-level command handling if ( @@ -131,7 +138,7 @@ export class CDPBrowserProxy extends Disposable implements ICDPConnection { throw new CDPServerError(`Session not found: ${sessionId}`); } - const result = await connection.sendMessage(method, params); + const result = await connection.sendCommand(method, params); return result ?? {}; } catch (error) { if (error instanceof CDPError) { @@ -141,6 +148,27 @@ export class CDPBrowserProxy extends Disposable implements ICDPConnection { } } + /** + * Accept a CDP request from a message-based transport (WebSocket, IPC, etc.), route it, + * and deliver the response or error via {@link onMessage}. + */ + async sendMessage({ id, method, params, sessionId }: CDPRequest): Promise { + return this.sendCommand(method, params, sessionId) + .then(result => { + this._onMessage.fire({ id, result, sessionId }); + }) + .catch((error: Error) => { + this._onMessage.fire({ + id, + error: { + code: error instanceof CDPError ? error.code : CDPErrorCode.ServerError, + message: error.message || 'Unknown error' + }, + sessionId + }); + }); + } + // #endregion // #region CDP Commands @@ -206,7 +234,7 @@ export class CDPBrowserProxy extends Disposable implements ICDPConnection { } private async handleTargetGetTargets() { - return { targetInfos: this._targets.getAllInfos() }; + return { targetInfos: Array.from(this._targets.getAllInfos()) }; } private async handleTargetGetTargetInfo({ targetId }: { targetId?: string } = {}) { diff --git a/src/vs/platform/browserView/common/cdp/types.ts b/src/vs/platform/browserView/common/cdp/types.ts index ca0256478c8..6fbfd30e26f 100644 --- a/src/vs/platform/browserView/common/cdp/types.ts +++ b/src/vs/platform/browserView/common/cdp/types.ts @@ -187,5 +187,5 @@ export interface ICDPConnection extends IDisposable { * @param sessionId Optional session ID for targeting a specific session * @returns Promise resolving to the result or rejecting with a CDPError */ - sendMessage(method: string, params?: unknown, sessionId?: string): Promise; + sendCommand(method: string, params?: unknown, sessionId?: string): Promise; } diff --git a/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts b/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts deleted file mode 100644 index 30ad512c042..00000000000 --- a/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts +++ /dev/null @@ -1,269 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -import { ILogService } from '../../log/common/log.js'; -import type * as http from 'http'; -import { AddressInfo, Socket } from 'net'; -import { upgradeToISocket } from '../../../base/parts/ipc/node/ipc.net.js'; -import { generateUuid } from '../../../base/common/uuid.js'; -import { VSBuffer } from '../../../base/common/buffer.js'; -import { CDPBrowserProxy } from '../common/cdp/proxy.js'; -import { CDPEvent, CDPRequest, CDPError, CDPErrorCode, ICDPBrowserTarget, ICDPConnection } from '../common/cdp/types.js'; -import { disposableTimeout } from '../../../base/common/async.js'; -import { ISocket } from '../../../base/parts/ipc/common/ipc.net.js'; -import { createDecorator } from '../../instantiation/common/instantiation.js'; - -export const IBrowserViewCDPProxyServer = createDecorator('browserViewCDPProxyServer'); - -export interface IBrowserViewCDPProxyServer { - readonly _serviceBrand: undefined; - - /** - * Returns a debug endpoint with a short-lived, single-use token for a specific browser target. - */ - getWebSocketEndpointForTarget(target: ICDPBrowserTarget): Promise; - - /** - * Unregister a previously registered browser target. - */ - removeTarget(target: ICDPBrowserTarget): Promise; -} - -/** - * WebSocket server that provides CDP debugging for browser views. - * - * Manages a registry of {@link ICDPBrowserTarget} instances, each reachable - * at its own `/devtools/browser/{id}` WebSocket endpoint. - */ -export class BrowserViewCDPProxyServer extends Disposable implements IBrowserViewCDPProxyServer { - declare readonly _serviceBrand: undefined; - - private server: http.Server | undefined; - private port: number | undefined; - - private readonly tokens = this._register(new TokenManager()); - private readonly targets = new Map(); - - constructor( - @ILogService private readonly logService: ILogService - ) { - super(); - } - - /** - * Register a browser target and return a WebSocket endpoint URL for it. - * The target is reachable at `/devtools/browser/{targetId}`. - */ - async getWebSocketEndpointForTarget(target: ICDPBrowserTarget): Promise { - await this.ensureServerStarted(); - - const targetInfo = await target.getTargetInfo(); - const targetId = targetInfo.targetId; - - // Register (or re-register) the target - this.targets.set(targetId, target); - - const token = await this.tokens.issueToken(targetId); - return `ws://localhost:${this.port}/devtools/browser/${targetId}?token=${token}`; - } - - /** - * Unregister a previously registered browser target. - */ - async removeTarget(target: ICDPBrowserTarget): Promise { - const targetInfo = await target.getTargetInfo(); - this.targets.delete(targetInfo.targetId); - } - - private async ensureServerStarted(): Promise { - if (this.server) { - return; - } - - const http = await import('http'); - this.server = http.createServer(); - - await new Promise((resolve, reject) => { - // Only listen on localhost to prevent external access - this.server!.listen(0, '127.0.0.1', () => resolve()); - this.server!.once('error', reject); - }); - - const address = this.server.address() as AddressInfo; - this.port = address.port; - - this.server.on('request', (req, res) => this.handleHttpRequest(req, res)); - this.server.on('upgrade', (req: http.IncomingMessage, socket: Socket) => this.handleWebSocketUpgrade(req, socket)); - } - - private async handleHttpRequest(_req: http.IncomingMessage, res: http.ServerResponse): Promise { - this.logService.debug(`[BrowserViewDebugProxy] HTTP request at ${_req.url}`); - // No support for HTTP endpoints for now. - res.writeHead(404); - res.end(); - } - - private handleWebSocketUpgrade(req: http.IncomingMessage, socket: Socket): void { - const [pathname, params] = (req.url || '').split('?'); - - const browserMatch = pathname.match(/^\/devtools\/browser\/([^/?]+)$/); - - this.logService.debug(`[BrowserViewDebugProxy] WebSocket upgrade requested: ${pathname}`); - - if (!browserMatch) { - this.logService.warn(`[BrowserViewDebugProxy] Rejecting WebSocket on unknown path: ${pathname}`); - socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); - socket.end(); - return; - } - - const targetId = browserMatch[1]; - - const token = new URLSearchParams(params).get('token'); - const tokenTargetId = token && this.tokens.consumeToken(token); - if (!tokenTargetId || tokenTargetId !== targetId) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); - socket.end(); - return; - } - - const target = this.targets.get(targetId); - if (!target) { - this.logService.warn(`[BrowserViewDebugProxy] Browser target not found: ${targetId}`); - socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); - socket.end(); - return; - } - - this.logService.debug(`[BrowserViewDebugProxy] WebSocket connected: ${pathname}`); - - const upgraded = upgradeToISocket(req, socket, { - debugLabel: 'browser-view-cdp-' + generateUuid(), - enableMessageSplitting: false, - }); - - if (!upgraded) { - return; - } - - const proxy = new CDPBrowserProxy(target); - const disposables = this.wireWebSocket(upgraded, proxy); - this._register(disposables); - this._register(upgraded); - } - - /** - * Wire a WebSocket (ISocket) to an ICDPConnection bidirectionally. - * Returns a DisposableStore that cleans up all subscriptions. - */ - private wireWebSocket(upgraded: ISocket, connection: ICDPConnection): DisposableStore { - const disposables = new DisposableStore(); - - // Socket -> Connection: parse JSON, call sendMessage, write response/error - disposables.add(upgraded.onData((rawData: VSBuffer) => { - try { - const message = rawData.toString(); - const { id, method, params, sessionId } = JSON.parse(message) as CDPRequest; - this.logService.debug(`[BrowserViewDebugProxy] <- ${message}`); - connection.sendMessage(method, params, sessionId) - .then((result: unknown) => { - const response = { id, result, sessionId }; - const responseStr = JSON.stringify(response); - this.logService.debug(`[BrowserViewDebugProxy] -> ${responseStr}`); - upgraded.write(VSBuffer.fromString(responseStr)); - }) - .catch((error: Error) => { - const response = { - id, - error: { - code: error instanceof CDPError ? error.code : CDPErrorCode.ServerError, - message: error.message || 'Unknown error' - }, - sessionId - }; - const responseStr = JSON.stringify(response); - this.logService.debug(`[BrowserViewDebugProxy] -> ${responseStr}`); - upgraded.write(VSBuffer.fromString(responseStr)); - }); - } catch (error) { - this.logService.error('[BrowserViewDebugProxy] Error parsing message:', error); - upgraded.end(); - } - })); - - // Connection -> Socket: serialize events and write - disposables.add(connection.onEvent((event: CDPEvent) => { - const eventStr = JSON.stringify(event); - this.logService.debug(`[BrowserViewDebugProxy] -> ${eventStr}`); - upgraded.write(VSBuffer.fromString(eventStr)); - })); - - // Connection close -> close socket - disposables.add(connection.onClose(() => { - this.logService.debug(`[BrowserViewDebugProxy] WebSocket closing`); - upgraded.end(); - })); - - // Socket closed -> cleanup - disposables.add(upgraded.onClose(() => { - this.logService.debug(`[BrowserViewDebugProxy] WebSocket closed`); - connection.dispose(); - disposables.dispose(); - })); - - return disposables; - } - - override dispose(): void { - if (this.server) { - this.server.close(); - this.server = undefined; - } - - super.dispose(); - } -} - -class TokenManager extends Disposable { - /** Map of currently valid single-use tokens to their associated details. */ - private readonly tokens = new Map(); - - /** - * Creates a short-lived, single-use token bound to a specific target. - * The token is revoked once consumed or after 30 seconds. - */ - async issueToken(details: TDetails): Promise { - const token = this.makeToken(); - this.tokens.set(token, { details: Object.freeze(details), expiresAt: Date.now() + 30_000 }); - this._register(disposableTimeout(() => this.tokens.delete(token), 30_000)); - return token; - } - - /** - * Consume a token. Returns the details it was issued with, or - * `undefined` if the token is invalid or expired. - */ - consumeToken(token: string): TDetails | undefined { - if (!token) { - return undefined; - } - const info = this.tokens.get(token); - if (!info) { - return undefined; - } - this.tokens.delete(token); - return Date.now() <= info.expiresAt ? info.details : undefined; - } - - private makeToken(): string { - const bytes = crypto.getRandomValues(new Uint8Array(32)); - const binary = Array.from(bytes).map(b => String.fromCharCode(b)).join(''); - const base64 = btoa(binary); - const urlSafeToken = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); - - return urlSafeToken; - } -} diff --git a/src/vs/platform/browserView/electron-main/browserViewDebugger.ts b/src/vs/platform/browserView/electron-main/browserViewDebugger.ts index 6e2a837d2f7..c0bdb734ef7 100644 --- a/src/vs/platform/browserView/electron-main/browserViewDebugger.ts +++ b/src/vs/platform/browserView/electron-main/browserViewDebugger.ts @@ -188,7 +188,7 @@ class DebugSession extends Disposable implements ICDPConnection { super(); } - async sendMessage(method: string, params?: unknown, _sessionId?: string): Promise { + async sendCommand(method: string, params?: unknown, _sessionId?: string): Promise { // This crashes Electron. Don't pass it through. if (method === 'Emulation.setDeviceMetricsOverride') { return Promise.resolve({}); diff --git a/src/vs/platform/browserView/electron-main/browserViewGroup.ts b/src/vs/platform/browserView/electron-main/browserViewGroup.ts index d68ba74efed..beed4f6042e 100644 --- a/src/vs/platform/browserView/electron-main/browserViewGroup.ts +++ b/src/vs/platform/browserView/electron-main/browserViewGroup.ts @@ -6,10 +6,9 @@ import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { BrowserView } from './browserView.js'; -import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget } from '../common/cdp/types.js'; +import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget, CDPRequest, CDPResponse, CDPEvent } from '../common/cdp/types.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; import { IBrowserViewGroup, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js'; -import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js'; import { IBrowserViewMainService } from './browserViewMainService.js'; /** @@ -50,8 +49,7 @@ export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, I constructor( readonly id: string, private readonly windowId: number, - @IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService, - @IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer, + @IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService ) { super(); } @@ -189,19 +187,26 @@ export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, I // #region CDP endpoint - /** - * Get a WebSocket endpoint URL for connecting to this group's CDP - * session. The URL contains a short-lived, single-use token. - */ - async getDebugWebSocketEndpoint(): Promise { - return this.cdpProxyServer.getWebSocketEndpointForTarget(this); + private _debugger: CDPBrowserProxy | undefined; + get debugger(): CDPBrowserProxy { + if (!this._debugger) { + this._debugger = this._register(new CDPBrowserProxy(this)); + } + return this._debugger; + } + + async sendCDPMessage(msg: CDPRequest): Promise { + return this.debugger.sendMessage(msg); + } + + get onCDPMessage(): Event { + return this.debugger.onMessage; } // #endregion override dispose(): void { this._onDidDestroy.fire(); - this.cdpProxyServer.removeTarget(this); super.dispose(); } } diff --git a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts index 4cc7aadfe97..c34bfa16b9d 100644 --- a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts @@ -9,6 +9,7 @@ import { createDecorator, IInstantiationService } from '../../instantiation/comm import { generateUuid } from '../../../base/common/uuid.js'; import { IBrowserViewGroupService, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js'; import { BrowserViewGroup } from './browserViewGroup.js'; +import { CDPEvent, CDPRequest, CDPResponse } from '../common/cdp/types.js'; export const IBrowserViewGroupMainService = createDecorator('browserViewGroupMainService'); @@ -58,8 +59,8 @@ export class BrowserViewGroupMainService extends Disposable implements IBrowserV return this._getGroup(groupId).removeView(viewId); } - async getDebugWebSocketEndpoint(groupId: string): Promise { - return this._getGroup(groupId).getDebugWebSocketEndpoint(); + async sendCDPMessage(groupId: string, message: CDPRequest): Promise { + return this._getGroup(groupId).debugger.sendMessage(message); } onDynamicDidAddView(groupId: string): Event { @@ -74,6 +75,10 @@ export class BrowserViewGroupMainService extends Disposable implements IBrowserV return this._getGroup(groupId).onDidDestroy; } + onDynamicCDPMessage(groupId: string): Event { + return this._getGroup(groupId).debugger.onMessage; + } + /** * Get a group or throw if not found. */ diff --git a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts index 3035a6828df..063a5b158b5 100644 --- a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts +++ b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts @@ -8,6 +8,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { IBrowserViewGroup, IBrowserViewGroupService, IBrowserViewGroupViewEvent, ipcBrowserViewGroupChannelName } from '../common/browserViewGroup.js'; +import { CDPEvent, CDPRequest, CDPResponse } from '../common/cdp/types.js'; /** * Remote-process service for managing browser view groups. @@ -62,8 +63,12 @@ class RemoteBrowserViewGroup extends Disposable implements IBrowserViewGroup { return this.groupService.removeViewFromGroup(this.id, viewId); } - async getDebugWebSocketEndpoint(): Promise { - return this.groupService.getDebugWebSocketEndpoint(this.id); + async sendCDPMessage(msg: CDPRequest): Promise { + return this.groupService.sendCDPMessage(this.id, msg); + } + + get onCDPMessage(): Event { + return this.groupService.onDynamicCDPMessage(this.id); } override dispose(fromService = false): void { diff --git a/src/vs/platform/browserView/node/playwrightService.ts b/src/vs/platform/browserView/node/playwrightService.ts index 9afb7963d66..8abf560a5c3 100644 --- a/src/vs/platform/browserView/node/playwrightService.ts +++ b/src/vs/platform/browserView/node/playwrightService.ts @@ -11,10 +11,24 @@ import { IPlaywrightService } from '../common/playwrightService.js'; import { IBrowserViewGroupRemoteService } from '../node/browserViewGroupRemoteService.js'; import { IBrowserViewGroup } from '../common/browserViewGroup.js'; import { PlaywrightTab } from './playwrightTab.js'; +import { CDPEvent, CDPRequest, CDPResponse } from '../common/cdp/types.js'; // eslint-disable-next-line local/code-import-patterns import type { Browser, BrowserContext, Page } from 'playwright-core'; +interface PlaywrightTransport { + send(s: CDPRequest): void; + close(): void; // Note: calling close is expected to issue onclose at some point. + onmessage?: (message: CDPResponse | CDPEvent) => void; + onclose?: (reason?: string) => void; +} + +declare module 'playwright-core' { + interface BrowserType { + _connectOverCDPTransport(transport: PlaywrightTransport): Promise; + } +} + /** * Shared-process implementation of {@link IPlaywrightService}. * @@ -81,8 +95,17 @@ export class PlaywrightService extends Disposable implements IPlaywrightService this.logService.debug('[PlaywrightService] Connecting to browser via CDP'); const playwright = await import('playwright-core'); - const endpoint = await group.getDebugWebSocketEndpoint(); - const browser = await playwright.chromium.connectOverCDP(endpoint); + const sub = group.onCDPMessage(msg => transport.onmessage?.(msg)); + const transport: PlaywrightTransport = { + close() { + sub.dispose(); + this.onclose?.(); + }, + send(message) { + void group.sendCDPMessage(message); + } + }; + const browser = await playwright.chromium._connectOverCDPTransport(transport); this.logService.debug('[PlaywrightService] Connected to browser'); From 9f946ce8433d53505c15f1ec9329755fff4cf6c5 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Tue, 10 Mar 2026 17:20:36 +0100 Subject: [PATCH 427/448] fix: prevent local cwd evaluation in sessions window --- src/vs/workbench/contrib/terminal/browser/terminalService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 1524e37ad3e..421ba3cb758 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1220,6 +1220,10 @@ export class TerminalService extends Disposable implements ITerminalService { } private _evaluateLocalCwd(shellLaunchConfig: IShellLaunchConfig) { + if (this._environmentService.isSessionsWindow) { + return; + } + // Add welcome message and title annotation for local terminals launched within remote or // virtual workspaces if (!isString(shellLaunchConfig.cwd) && shellLaunchConfig.cwd?.scheme === Schemas.file) { From c2da2674e13adf7ed0c0ac1a74919a474f303fb2 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:35:15 -0700 Subject: [PATCH 428/448] [MCP_Sandboxing]Updating regex for extracting paths from sandbox errors (#300354) * changes to ensure all the network requests are passed through proxy * changes to ensure all the network requests are passed through proxy * changes to quote shell arguments passed to sandbox * updates to default paths * Updating regex to extract file name --- .../contrib/mcp/common/mcpServerConnection.ts | 4 +- .../test/common/mcpServerConnection.test.ts | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts index 2b493d9cfc1..5ab7cdc20d8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts @@ -162,7 +162,7 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect }; } - if (/(?:\b(?:EACCES|EPERM|ENOENT|EROFS|fail(?:ed|ure)?)\b|\bnot accessible\b|read only)/i.test(message)) { + if (/(?:\b(?:EACCES|EPERM|ENOENT|EROFS|fail(?:ed|ure)?)\b|not accessible|read[- ]only)/i.test(message)) { return { kind: 'filesystem', message, @@ -179,7 +179,7 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect return bracketedPath[1].trim(); } - const quotedPath = line.match(/["'](\/[^"']+)["']/); + const quotedPath = line.match(/["'`](\/[^"'`]+)["'`]/); if (quotedPath?.[1]) { return quotedPath[1]; } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index 490efd09409..518481fb123 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -338,6 +338,117 @@ suite('Workbench - MCP - ServerConnection', () => { await timeout(10); }); + test('should emit a sandbox filesystem block for read-only errors with backtick paths', async () => { + const sandboxedDefinition: McpServerDefinition = { + ...serverDefinition, + sandboxEnabled: true, + }; + + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + sandboxedDefinition, + delegate, + sandboxedDefinition.launch, + new NullLogger(), + false, + store.add(new McpTaskManager()), + ); + store.add(connection); + + const message = 'error: failed to open file `/test-for-sandbox/.git`: Read-only file system (os error 30)'; + const sandboxBlock = Event.toPromise(connection.onPotentialSandboxBlock); + const startPromise = connection.start({}); + + transport.simulateLog(message); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + assert.deepStrictEqual(await sandboxBlock, { + kind: 'filesystem', + message, + path: '/test-for-sandbox/.git', + }); + + await startPromise; + + connection.dispose(); + await timeout(10); + }); + + test('should emit a sandbox filesystem block for read-only errors with double-quoted paths', async () => { + const sandboxedDefinition: McpServerDefinition = { + ...serverDefinition, + sandboxEnabled: true, + }; + + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + sandboxedDefinition, + delegate, + sandboxedDefinition.launch, + new NullLogger(), + false, + store.add(new McpTaskManager()), + ); + store.add(connection); + + const message = 'error: failed to open file `/test-for-sandbox/.testfile`: Read-only file system (os error 30)'; + const sandboxBlock = Event.toPromise(connection.onPotentialSandboxBlock); + const startPromise = connection.start({}); + + transport.simulateLog(message); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + assert.deepStrictEqual(await sandboxBlock, { + kind: 'filesystem', + message, + path: '/test-for-sandbox/.testfile', + }); + + await startPromise; + + connection.dispose(); + await timeout(10); + }); + + test('should emit a sandbox filesystem block for read-only at-path errors with double-quoted paths', async () => { + const sandboxedDefinition: McpServerDefinition = { + ...serverDefinition, + sandboxEnabled: true, + }; + + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + sandboxedDefinition, + delegate, + sandboxedDefinition.launch, + new NullLogger(), + false, + store.add(new McpTaskManager()), + ); + store.add(connection); + + const message = 'error: Read-only file system (os error 30) at path "/test-for-sandbox/.testfile"'; + const sandboxBlock = Event.toPromise(connection.onPotentialSandboxBlock); + const startPromise = connection.start({}); + + transport.simulateLog(message); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + assert.deepStrictEqual(await sandboxBlock, { + kind: 'filesystem', + message, + path: '/test-for-sandbox/.testfile', + }); + + await startPromise; + + connection.dispose(); + await timeout(10); + }); + test('should emit a sandbox network block with the denied host', async () => { const sandboxedDefinition: McpServerDefinition = { ...serverDefinition, From 7daf926d27dc1cc5fe1f3553bf98ecfcf2d7d6ed Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:16:03 -0700 Subject: [PATCH 429/448] Add telemetry for xterm imageAddon (#299017) * Add telemetry imageAddon loaded * edit descr * add more individual telmetry from xterm.js api * Make sure we have telemetry for setting AND image count --- .../contrib/terminal/browser/terminalTelemetry.ts | 5 +++++ .../contrib/terminal/browser/xterm/xtermTerminal.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts b/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts index 9351985ccb8..45914710aaa 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts @@ -80,6 +80,8 @@ export class TerminalTelemetryContribution extends Disposable implements IWorkbe shellIntegrationInjected: boolean; shellIntegrationInjectionFailureReason: ShellIntegrationInjectionFailureReason | undefined; + imageAddonLoaded: boolean; + terminalSessionId: string; }; type TerminalCreationTelemetryClassification = { @@ -101,6 +103,8 @@ export class TerminalTelemetryContribution extends Disposable implements IWorkbe shellIntegrationInjected: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the shell integration script was injected.' }; shellIntegrationInjectionFailureReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Info about shell integration injection.' }; + imageAddonLoaded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the xterm.js image addon was loaded.' }; + terminalSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The session ID of the terminal instance.' }; }; this._telemetryService.publicLog2('terminal/createInstance', { @@ -122,6 +126,7 @@ export class TerminalTelemetryContribution extends Disposable implements IWorkbe shellIntegrationQuality: commandDetection?.hasRichCommandDetection ? 2 : commandDetection ? 1 : 0, shellIntegrationInjected: instance.usedShellIntegrationInjection, shellIntegrationInjectionFailureReason: instance.shellIntegrationInjectionFailureReason, + imageAddonLoaded: instance.xterm?.isImageAddonLoaded ?? false, terminalSessionId: instance.sessionId, }); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index be33e2b306b..ea122a15c23 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -141,6 +141,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach get isStdinDisabled(): boolean { return !!this.raw.options.disableStdin; } get isGpuAccelerated(): boolean { return !!this._webglAddon; } + get isImageAddonLoaded(): boolean { return !!this._imageAddon; } private readonly _onDidRequestRunCommand = this._register(new Emitter<{ command: ITerminalCommand; noNewLine?: boolean }>()); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; @@ -916,6 +917,18 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach const AddonCtor = await this._xtermAddonLoader.importAddon('image'); this._imageAddon = new AddonCtor(); this.raw.loadAddon(this._imageAddon); + type TerminalImageAddonActivatedClassification = { + owner: 'anthonykim1'; + comment: 'Tracks when the xterm.js image addon is loaded, including dynamic enablement'; + }; + this._telemetryService.publicLog2<{}, TerminalImageAddonActivatedClassification>('terminal/imageAddonActivated'); + this._register(this._imageAddon.onImageAdded(() => { + type TerminalImageAddedClassification = { + owner: 'anthonykim1'; + comment: 'Tracks when an image is added to the terminal via the image addon'; + }; + this._telemetryService.publicLog2<{}, TerminalImageAddedClassification>('terminal/imageAdded'); + })); } } else { try { From 0cd1e5976aae3659c565ca4c7fb578380f00a5ec Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:22:07 -0700 Subject: [PATCH 430/448] Disable api-version-check for now Needed to unblock #300477 --- .github/workflows/api-proposal-version-check.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/api-proposal-version-check.yml b/.github/workflows/api-proposal-version-check.yml index 2035ceb2376..23f6e052f9f 100644 --- a/.github/workflows/api-proposal-version-check.yml +++ b/.github/workflows/api-proposal-version-check.yml @@ -23,11 +23,11 @@ jobs: check-version-changes: name: Check API Proposal Version Changes # Run on PR events, or on issue_comment if it's on a PR and contains the override command - if: | - github.event_name == 'pull_request' || - (github.event_name == 'issue_comment' && - github.event.issue.pull_request && - contains(github.event.comment.body, '/api-proposal-change-required')) + if: false # temporarily disabled + # github.event_name == 'pull_request' || + # (github.event_name == 'issue_comment' && + # github.event.issue.pull_request && + # contains(github.event.comment.body, '/api-proposal-change-required')) runs-on: ubuntu-latest steps: - name: Get PR info From 34bfd71aeac1598fe87bbd37163f345ca49dffaa Mon Sep 17 00:00:00 2001 From: Vijay Upadya <41652029+vijayupadya@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:32:58 -0700 Subject: [PATCH 431/448] Revert "Revert "Debug Panel: oTel data source support and Import/export (#299256)"" (#300477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Revert "Debug Panel: oTel data source support and Import/export (#299…" This reverts commit 11246017b66d444e6aa5e3a2535e161498182b51. --- .../common/extensionsApiProposals.ts | 2 +- .../api/browser/mainThreadChatDebug.ts | 47 ++++++++ .../workbench/api/common/extHost.protocol.ts | 2 + .../workbench/api/common/extHostChatDebug.ts | 103 ++++++++++++++++- .../actions/chatOpenAgentDebugPanelAction.ts | 105 +++++++++++++++++- .../chat/browser/chatDebug/chatDebugEditor.ts | 47 ++------ .../browser/chatDebug/chatDebugFlowGraph.ts | 53 ++++----- .../browser/chatDebug/chatDebugFlowLayout.ts | 10 +- .../browser/chatDebug/chatDebugHomeView.ts | 49 ++++---- .../browser/chatDebug/chatDebugLogsView.ts | 34 +++++- .../contrib/chat/common/chatDebugService.ts | 30 +++++ .../chat/common/chatDebugServiceImpl.ts | 55 ++++++++- src/vscode-dts/vscode.proposed.chatDebug.d.ts | 65 ++++++++++- 13 files changed, 502 insertions(+), 100 deletions(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index a9bc2d2fa10..00e09a016ac 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -45,7 +45,7 @@ const _allApiProposals = { }, chatDebug: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatDebug.d.ts', - version: 2 + version: 3 }, chatHooks: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatHooks.d.ts', diff --git a/src/vs/workbench/api/browser/mainThreadChatDebug.ts b/src/vs/workbench/api/browser/mainThreadChatDebug.ts index 82594dcb038..169324d37e7 100644 --- a/src/vs/workbench/api/browser/mainThreadChatDebug.ts +++ b/src/vs/workbench/api/browser/mainThreadChatDebug.ts @@ -5,7 +5,9 @@ import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; +import { VSBuffer } from '../../../base/common/buffer.js'; import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugService } from '../../contrib/chat/common/chatDebugService.js'; +import { IChatService } from '../../contrib/chat/common/chatService/chatService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostChatDebugShape, ExtHostContext, IChatDebugEventDto, MainContext, MainThreadChatDebugShape } from '../common/extHost.protocol.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; @@ -19,6 +21,7 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb constructor( extHostContext: IExtHostContext, @IChatDebugService private readonly _chatDebugService: IChatDebugService, + @IChatService private readonly _chatService: IChatService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatDebug); @@ -36,6 +39,26 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb }, resolveChatDebugLogEvent: async (eventId, token) => { return this._proxy.$resolveChatDebugLogEvent(handle, eventId, token); + }, + provideChatDebugLogExport: async (sessionResource, token) => { + // Gather core events and session title to pass to the extension. + const coreEventDtos = this._chatDebugService.getEvents(sessionResource) + .filter(e => this._chatDebugService.isCoreEvent(e)) + .map(e => this._serializeEvent(e)); + const sessionTitle = this._chatService.getSessionTitle(sessionResource); + const result = await this._proxy.$exportChatDebugLog(handle, sessionResource, coreEventDtos, sessionTitle, token); + return result?.buffer; + }, + resolveChatDebugLogImport: async (data, token) => { + const result = await this._proxy.$importChatDebugLog(handle, VSBuffer.wrap(data), token); + if (!result) { + return undefined; + } + const uri = URI.revive(result.uri); + if (result.sessionTitle) { + this._chatDebugService.setImportedSessionTitle(uri, result.sessionTitle); + } + return uri; } })); } @@ -58,6 +81,30 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb this._chatDebugService.addProviderEvent(revived); } + private _serializeEvent(event: IChatDebugEvent): IChatDebugEventDto { + const base = { + id: event.id, + sessionResource: event.sessionResource, + created: event.created.getTime(), + parentEventId: event.parentEventId, + }; + + switch (event.kind) { + case 'toolCall': + return { ...base, kind: 'toolCall', toolName: event.toolName, toolCallId: event.toolCallId, input: event.input, output: event.output, result: event.result, durationInMillis: event.durationInMillis }; + case 'modelTurn': + return { ...base, kind: 'modelTurn', model: event.model, requestName: event.requestName, inputTokens: event.inputTokens, outputTokens: event.outputTokens, totalTokens: event.totalTokens, durationInMillis: event.durationInMillis }; + case 'generic': + return { ...base, kind: 'generic', name: event.name, details: event.details, level: event.level, category: event.category }; + case 'subagentInvocation': + return { ...base, kind: 'subagentInvocation', agentName: event.agentName, description: event.description, status: event.status, durationInMillis: event.durationInMillis, toolCallCount: event.toolCallCount, modelTurnCount: event.modelTurnCount }; + case 'userMessage': + return { ...base, kind: 'userMessage', message: event.message, sections: event.sections.map(s => ({ name: s.name, content: s.content })) }; + case 'agentResponse': + return { ...base, kind: 'agentResponse', message: event.message, sections: event.sections.map(s => ({ name: s.name, content: s.content })) }; + } + } + private _reviveEvent(dto: IChatDebugEventDto, sessionResource: URI): IChatDebugEvent { const base = { id: dto.id, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ecd7167f23c..ef5be258b18 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1502,6 +1502,8 @@ export type IChatDebugResolvedEventContentDto = IChatDebugEventTextContentDto | export interface ExtHostChatDebugShape { $provideChatDebugLog(handle: number, sessionResource: UriComponents, token: CancellationToken): Promise; $resolveChatDebugLogEvent(handle: number, eventId: string, token: CancellationToken): Promise; + $exportChatDebugLog(handle: number, sessionResource: UriComponents, coreEvents: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise; + $importChatDebugLog(handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined>; } export interface MainThreadChatDebugShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostChatDebug.ts b/src/vs/workbench/api/common/extHostChatDebug.ts index 04125f3e551..b83bb2f3cf5 100644 --- a/src/vs/workbench/api/common/extHostChatDebug.ts +++ b/src/vs/workbench/api/common/extHostChatDebug.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; +import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtHostChatDebugShape, IChatDebugEventDto, IChatDebugResolvedEventContentDto, MainContext, MainThreadChatDebugShape } from './extHost.protocol.js'; -import { ChatDebugMessageContentType, ChatDebugSubagentStatus, ChatDebugToolCallResult } from './extHostTypes.js'; +import { ChatDebugGenericEvent, ChatDebugLogLevel, ChatDebugMessageContentType, ChatDebugMessageSection, ChatDebugModelTurnEvent, ChatDebugSubagentInvocationEvent, ChatDebugSubagentStatus, ChatDebugToolCallEvent, ChatDebugToolCallResult, ChatDebugUserMessageEvent, ChatDebugAgentResponseEvent } from './extHostTypes.js'; import { IExtHostRpcService } from './extHostRpcService.js'; export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShape { @@ -291,6 +292,106 @@ export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShap } } + private _deserializeEvent(dto: IChatDebugEventDto): vscode.ChatDebugEvent | undefined { + const created = new Date(dto.created); + const sessionResource = dto.sessionResource ? URI.revive(dto.sessionResource) : undefined; + switch (dto.kind) { + case 'toolCall': { + const evt = new ChatDebugToolCallEvent(dto.toolName, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.toolCallId = dto.toolCallId; + evt.input = dto.input; + evt.output = dto.output; + evt.result = dto.result === 'success' ? ChatDebugToolCallResult.Success + : dto.result === 'error' ? ChatDebugToolCallResult.Error + : undefined; + evt.durationInMillis = dto.durationInMillis; + return evt; + } + case 'modelTurn': { + const evt = new ChatDebugModelTurnEvent(created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.model = dto.model; + evt.inputTokens = dto.inputTokens; + evt.outputTokens = dto.outputTokens; + evt.totalTokens = dto.totalTokens; + evt.durationInMillis = dto.durationInMillis; + return evt; + } + case 'generic': { + const evt = new ChatDebugGenericEvent(dto.name, dto.level as ChatDebugLogLevel, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.details = dto.details; + evt.category = dto.category; + return evt; + } + case 'subagentInvocation': { + const evt = new ChatDebugSubagentInvocationEvent(dto.agentName, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.description = dto.description; + evt.status = dto.status === 'running' ? ChatDebugSubagentStatus.Running + : dto.status === 'completed' ? ChatDebugSubagentStatus.Completed + : dto.status === 'failed' ? ChatDebugSubagentStatus.Failed + : undefined; + evt.durationInMillis = dto.durationInMillis; + evt.toolCallCount = dto.toolCallCount; + evt.modelTurnCount = dto.modelTurnCount; + return evt; + } + case 'userMessage': { + const evt = new ChatDebugUserMessageEvent(dto.message, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content)); + return evt; + } + case 'agentResponse': { + const evt = new ChatDebugAgentResponseEvent(dto.message, created); + evt.id = dto.id; + evt.sessionResource = sessionResource; + evt.parentEventId = dto.parentEventId; + evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content)); + return evt; + } + default: + return undefined; + } + } + + async $exportChatDebugLog(_handle: number, sessionResource: UriComponents, coreEventDtos: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise { + if (!this._provider?.provideChatDebugLogExport) { + return undefined; + } + const sessionUri = URI.revive(sessionResource); + const coreEvents = coreEventDtos.map(dto => this._deserializeEvent(dto)).filter((e): e is vscode.ChatDebugEvent => e !== undefined); + const options: vscode.ChatDebugLogExportOptions = { coreEvents, sessionTitle }; + const result = await this._provider.provideChatDebugLogExport(sessionUri, options, token); + if (!result) { + return undefined; + } + return VSBuffer.wrap(result); + } + + async $importChatDebugLog(_handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined> { + if (!this._provider?.resolveChatDebugLogImport) { + return undefined; + } + const result = await this._provider.resolveChatDebugLogImport(data.buffer, token); + if (!result) { + return undefined; + } + return { uri: result.uri, sessionTitle: result.sessionTitle }; + } + override dispose(): void { for (const store of this._activeProgress.values()) { store.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts index 860559d64e3..685ebc58948 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatOpenAgentDebugPanelAction.ts @@ -3,12 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; +import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; +import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -16,6 +22,7 @@ import { IChatDebugService } from '../../common/chatDebugService.js'; import { ChatViewId, IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js'; import { ChatDebugEditorInput } from '../chatDebug/chatDebugEditorInput.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { IChatDebugEditorOptions } from '../chatDebug/chatDebugTypes.js'; /** @@ -92,4 +99,100 @@ export function registerChatOpenAgentDebugPanelAction() { await editorService.openEditor(ChatDebugEditorInput.instance, options); } }); + + const defaultDebugLogFileName = 'agent-debug-log.json'; + const debugLogFilters = [{ name: localize('chatDebugLog.file.label', "Agent Debug Log"), extensions: ['json'] }]; + + registerAction2(class ExportAgentDebugLogAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.exportAgentDebugLog', + title: localize2('chat.exportAgentDebugLog.label', "Export Agent Debug Log..."), + icon: Codicon.desktopDownload, + f1: true, + category: Categories.Developer, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID), + order: 10 + }], + }); + } + + async run(accessor: ServicesAccessor): Promise { + const chatDebugService = accessor.get(IChatDebugService); + const fileDialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + + const sessionResource = chatDebugService.activeSessionResource; + if (!sessionResource) { + notificationService.notify({ severity: Severity.Info, message: localize('chatDebugLog.noSession', "No active debug session to export. Navigate to a session first.") }); + return; + } + + const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName); + const outputPath = await fileDialogService.showSaveDialog({ defaultUri, filters: debugLogFilters }); + if (!outputPath) { + return; + } + + const data = await chatDebugService.exportLog(sessionResource); + if (!data) { + notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.exportFailed', "Export is not supported by the current provider.") }); + return; + } + + await fileService.writeFile(outputPath, VSBuffer.wrap(data)); + } + }); + + registerAction2(class ImportAgentDebugLogAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.importAgentDebugLog', + title: localize2('chat.importAgentDebugLog.label', "Import Agent Debug Log..."), + icon: Codicon.cloudUpload, + f1: true, + category: Categories.Developer, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(ChatDebugEditorInput.ID), + order: 11 + }], + }); + } + + async run(accessor: ServicesAccessor): Promise { + const chatDebugService = accessor.get(IChatDebugService); + const editorService = accessor.get(IEditorService); + const fileDialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const notificationService = accessor.get(INotificationService); + + const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultDebugLogFileName); + const result = await fileDialogService.showOpenDialog({ + defaultUri, + canSelectFiles: true, + filters: debugLogFilters + }); + if (!result) { + return; + } + + const content = await fileService.readFile(result[0]); + const sessionUri = await chatDebugService.importLog(content.value.buffer); + if (!sessionUri) { + notificationService.notify({ severity: Severity.Warning, message: localize('chatDebugLog.importFailed', "Import is not supported by the current provider.") }); + return; + } + + const options: IChatDebugEditorOptions = { pinned: true, sessionResource: sessionUri, viewHint: 'overview' }; + await editorService.openEditor(ChatDebugEditorInput.instance, options); + } + }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts index 8276d701b2a..867053d97ac 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugEditor.ts @@ -65,9 +65,6 @@ export class ChatDebugEditor extends EditorPane { private readonly sessionModelListener = this._register(new MutableDisposable()); private readonly modelChangeListeners = this._register(new DisposableMap()); - /** Saved session resource so we can restore it after the editor is re-shown. */ - private savedSessionResource: URI | undefined; - /** * Stops the streaming pipeline and clears cached events for the * active session. Called when navigating away from a session or @@ -175,7 +172,10 @@ export class ChatDebugEditor extends EditorPane { this._register(this.chatService.onDidCreateModel(model => { if (this.viewState === ViewState.Home) { - this.homeView?.render(); + // Auto-navigate to the new session when the debug panel is + // already open on the home view. This avoids the user having to + // wait for the title to resolve and manually clicking the session. + this.navigateToSession(model.sessionResource); } // Track title changes per model, disposing the previous listener @@ -307,40 +307,11 @@ export class ChatDebugEditor extends EditorPane { super.setEditorVisible(visible); if (visible) { this.telemetryService.publicLog2<{}, ChatDebugPanelOpenedClassification>('chatDebugPanelOpened'); - // Note: do NOT read this.options here. When the editor becomes - // visible via openEditor(), setEditorVisible fires before - // setOptions, so this.options still contains stale values from - // the previous openEditor() call. Navigation from new options - // is handled entirely by setOptions → _applyNavigationOptions. - // Here we only restore the previous state when the editor is - // re-shown without a new openEditor() call (e.g., tab switch). - if (this.viewState === ViewState.Home) { - const sessionResource = this.chatDebugService.activeSessionResource ?? this.savedSessionResource; - this.savedSessionResource = undefined; - if (sessionResource) { - this.navigateToSession(sessionResource, 'overview'); - } else { - this.showView(ViewState.Home); - } - } else { - // Re-activate the streaming pipeline for the current session, - // restoring the saved session resource if the editor was temporarily hidden. - const sessionResource = this.chatDebugService.activeSessionResource ?? this.savedSessionResource; - this.savedSessionResource = undefined; - if (sessionResource) { - this.chatDebugService.activeSessionResource = sessionResource; - if (!this.chatDebugService.hasInvokedProviders(sessionResource)) { - this.chatDebugService.invokeProviders(sessionResource); - } - } else { - this.showView(ViewState.Home); - } - } - } else { - // Remember the active session so we can restore when re-shown - this.savedSessionResource = this.chatDebugService.activeSessionResource; - // Stop the streaming pipeline when the editor is hidden - this.endActiveSession(); + // Re-show the current view so it reloads events from scratch, + // ensuring correct ordering and no stale duplicates. + // Navigation from new openEditor() options is handled by + // setOptions → _applyNavigationOptions (fires after this). + this.showView(this.viewState); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts index 442c56360b1..48e5ce0dced 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowGraph.ts @@ -179,13 +179,18 @@ export function buildFlowGraph(events: readonly IChatDebugEvent[]): FlowNode[] { // For subagent invocations, enrich with description from the // filtered-out completion sibling, or fall back to the event's own field. - let sublabel = getEventSublabel(event, effectiveKind); + let label = getEventLabel(event, effectiveKind); + const sublabel = getEventSublabel(event, effectiveKind); let tooltip = getEventTooltip(event); let description: string | undefined; if (effectiveKind === 'subagentInvocation') { description = getSubagentDescription(event); + // Show "Subagent: " as the label so users can identify + // these nodes and see what task they perform. + label = description + ? localize('subagentWithDesc', "Subagent: {0}", truncateLabel(description, 30)) + : localize('subagentLabel', "Subagent"); if (description) { - sublabel = truncateLabel(description, 30) + (sublabel ? ` \u00b7 ${sublabel}` : ''); // Ensure description appears in tooltip if not already present if (tooltip && !tooltip.includes(description)) { const lines = tooltip.split('\n'); @@ -199,7 +204,7 @@ export function buildFlowGraph(events: readonly IChatDebugEvent[]): FlowNode[] { id: event.id ?? `event-${events.indexOf(event)}`, kind: effectiveKind, category: event.kind === 'generic' ? event.category : undefined, - label: getEventLabel(event, effectiveKind), + label, sublabel, description, tooltip, @@ -524,29 +529,17 @@ function getEventLabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEvent[' const kind = effectiveKind ?? event.kind; switch (kind) { case 'userMessage': - return localize('userLabel', "User"); + return localize('userLabel', "User Message"); case 'modelTurn': return event.kind === 'modelTurn' ? (event.model ?? localize('modelTurnLabel', "Model Turn")) : localize('modelTurnLabel', "Model Turn"); case 'toolCall': - return event.kind === 'toolCall' ? event.toolName : event.kind === 'generic' ? event.name : ''; + return event.kind === 'toolCall' ? event.toolName : event.kind === 'generic' ? event.name : localize('toolCallLabel', "Tool Call"); case 'subagentInvocation': - return event.kind === 'subagentInvocation' ? event.agentName : ''; - case 'agentResponse': { - if (event.kind === 'agentResponse') { - return event.message || localize('responseLabel', "Response"); - } - // Remapped generic event — extract model name from parenthesized suffix - // e.g. "Agent response (claude-opus-4.5)" → "claude-opus-4.5" - if (event.kind === 'generic') { - const match = /\(([^)]+)\)\s*$/.exec(event.name); - if (match) { - return match[1]; - } - } - return localize('responseLabel', "Response"); - } + return event.kind === 'subagentInvocation' ? event.agentName : localize('subagentFallback', "Subagent"); + case 'agentResponse': + return localize('agentResponseLabel', "Agent Response"); case 'generic': - return event.kind === 'generic' ? event.name : ''; + return event.kind === 'generic' ? event.name : localize('genericLabel', "Event"); } } @@ -588,30 +581,32 @@ function getEventSublabel(event: IChatDebugEvent, effectiveKind?: IChatDebugEven } case 'userMessage': case 'agentResponse': { - // For proper typed events, prefer the first section's content - // (which has the actual message text) over the `message` field - // (which is a short summary/name). Fall back to `message` when - // no sections are available. For remapped generic events, use - // the details property. + // Use the message summary as the sublabel. For remapped generic + // events, use the details property. let text: string | undefined; if (event.kind === 'userMessage' || event.kind === 'agentResponse') { - text = event.sections[0]?.content || event.message; + text = event.message; } else if (event.kind === 'generic') { text = event.details; } if (!text) { return undefined; } - // Find the first non-empty line (content may start with newlines) + // Find the first meaningful line, skipping trivial lines like + // lone brackets/braces that appear when the message is JSON. const lines = text.split('\n'); let firstLine = ''; for (const line of lines) { const trimmed = line.trim(); - if (trimmed) { + if (trimmed && trimmed.length > 2) { firstLine = trimmed; break; } } + if (!firstLine) { + // Fall back to the full text collapsed to a single line + firstLine = text.replace(/\s+/g, ' ').trim(); + } if (!firstLine) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts index cf9dd80103e..403003e62bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugFlowLayout.ts @@ -159,7 +159,15 @@ function measureNodeWidth(label: string, sublabel?: string): number { } function subgraphHeaderLabel(node: FlowNode): string { - return node.description ? `${node.label}: ${node.description}` : node.label; + // For subagent nodes, the label already includes the description + // (e.g. "Subagent: Count markdown files"), so don't append it again. + if (node.kind === 'subagentInvocation') { + return node.label; + } + if (node.description && node.description !== node.label) { + return `${node.label}: ${node.description}`; + } + return node.label; } function measureSubgraphHeaderWidth(headerLabel: string): number { diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts index f167776e078..0492768b660 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts @@ -85,7 +85,24 @@ export class ChatDebugHomeView extends Disposable { const items: HTMLButtonElement[] = []; for (const sessionResource of sessionResources) { - const sessionTitle = this.chatService.getSessionTitle(sessionResource) || LocalChatSessionUri.parseLocalSessionId(sessionResource) || sessionResource.toString(); + const rawTitle = this.chatService.getSessionTitle(sessionResource); + let sessionTitle: string; + if (rawTitle && !isUUID(rawTitle)) { + sessionTitle = rawTitle; + } else if (LocalChatSessionUri.isLocalSession(sessionResource)) { + sessionTitle = localize('chatDebug.newSession', "New Chat"); + } else { + // For imported/external sessions, use the stored title if available + const importedTitle = this.chatDebugService.getImportedSessionTitle(sessionResource); + if (importedTitle) { + sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", importedTitle); + } else { + // Fall back to URI segment + const uriLabel = sessionResource.path || sessionResource.fragment || sessionResource.toString(); + const segment = uriLabel.replace(/^\/+/, '').split('/').pop() || uriLabel; + sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", segment); + } + } const isActive = activeSessionResource !== undefined && sessionResource.toString() === activeSessionResource.toString(); const item = DOM.append(sessionList, $('button.chat-debug-home-session-item')); @@ -98,32 +115,20 @@ export class ChatDebugHomeView extends Disposable { DOM.append(item, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`)); const titleSpan = DOM.append(item, $('span.chat-debug-home-session-item-title')); - // Show shimmer when the title is still a UUID — the session is - // either not yet loaded or hasn't produced a real title yet. - const isShimmering = isUUID(sessionTitle); - if (isShimmering) { - titleSpan.classList.add('chat-debug-home-session-item-shimmer'); - item.disabled = true; - item.setAttribute('aria-busy', 'true'); - item.setAttribute('aria-label', localize('chatDebug.loadingSession', "Loading session…")); - } else { - titleSpan.textContent = sessionTitle; - const ariaLabel = isActive - ? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle) - : sessionTitle; - item.setAttribute('aria-label', ariaLabel); - } + titleSpan.textContent = sessionTitle; + const ariaLabel = isActive + ? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle) + : sessionTitle; + item.setAttribute('aria-label', ariaLabel); if (isActive) { DOM.append(item, $('span.chat-debug-home-session-badge', undefined, localize('chatDebug.active', "Active"))); } - if (!isShimmering) { - this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => { - this._onNavigateToSession.fire(sessionResource); - })); - items.push(item); - } + this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => { + this._onNavigateToSession.fire(sessionResource); + })); + items.push(item); } // Arrow key navigation between session items diff --git a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts index 8cbf8c1a90d..550fa005b81 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugLogsView.ts @@ -12,6 +12,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; +import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -63,6 +64,7 @@ export class ChatDebugLogsView extends Disposable { private currentDimension: Dimension | undefined; private readonly eventListener = this._register(new MutableDisposable()); private readonly sessionStateDisposable = this._register(new MutableDisposable()); + private readonly refreshScheduler: RunOnceScheduler; private shimmerRow!: HTMLElement; constructor( @@ -75,6 +77,7 @@ export class ChatDebugLogsView extends Disposable { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); + this.refreshScheduler = this._register(new RunOnceScheduler(() => this.refreshList(), 50)); this.container = DOM.append(parent, $('.chat-debug-logs')); DOM.hide(this.container); @@ -383,8 +386,32 @@ export class ChatDebugLogsView extends Disposable { } addEvent(event: IChatDebugEvent): void { - this.events.push(event); - this.refreshList(); + // Binary-insert to maintain chronological order without a full sort. + // Events almost always arrive in order, so the insertion point is + // typically at the end (O(log n) comparison, O(1) splice). + const time = event.created.getTime(); + let lo = 0; + let hi = this.events.length; + while (lo < hi) { + const mid = (lo + hi) >>> 1; + if (this.events[mid].created.getTime() <= time) { + lo = mid + 1; + } else { + hi = mid; + } + } + if (lo === this.events.length) { + this.events.push(event); + } else { + this.events.splice(lo, 0, event); + } + this.scheduleRefresh(); + } + + private scheduleRefresh(): void { + if (!this.refreshScheduler.isScheduled()) { + this.refreshScheduler.schedule(); + } } private loadEvents(): void { @@ -392,8 +419,7 @@ export class ChatDebugLogsView extends Disposable { const addEventDisposable = this.chatDebugService.onDidAddEvent(e => { if (!this.currentSessionResource || e.sessionResource.toString() === this.currentSessionResource.toString()) { - this.events.push(e); - this.refreshList(); + this.addEvent(e); } }); diff --git a/src/vs/workbench/contrib/chat/common/chatDebugService.ts b/src/vs/workbench/contrib/chat/common/chatDebugService.ts index 7a6e5721ba1..d97213d3f3e 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugService.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugService.ts @@ -196,6 +196,34 @@ export interface IChatDebugService extends IDisposable { */ resolveEvent(eventId: string): Promise; + /** + /** + * Export the debug log for a session via the registered provider. + */ + exportLog(sessionResource: URI): Promise; + + /** + * Import a previously exported debug log via the registered provider. + * Returns the session URI for the imported data. + */ + importLog(data: Uint8Array): Promise; + + /** + * Returns true if the event was logged by VS Code core + * (not sourced from an external provider). + */ + isCoreEvent(event: IChatDebugEvent): boolean; + + /** + * Store a human-readable title for an imported session. + */ + setImportedSessionTitle(sessionResource: URI, title: string): void; + + /** + * Get the stored title for an imported session, if available. + */ + getImportedSessionTitle(sessionResource: URI): string | undefined; + /** * Fired when debug data is attached to a session. */ @@ -314,4 +342,6 @@ export type IChatDebugResolvedEventContent = IChatDebugEventTextContent | IChatD export interface IChatDebugLogProvider { provideChatDebugLog(sessionResource: URI, token: CancellationToken): Promise; resolveChatDebugLogEvent?(eventId: string, token: CancellationToken): Promise; + provideChatDebugLogExport?(sessionResource: URI, token: CancellationToken): Promise; + resolveChatDebugLogImport?(data: Uint8Array, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts index 9cdd711a311..c80186d968d 100644 --- a/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts @@ -39,6 +39,12 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic /** Events that were returned by providers (not internally logged). */ private readonly _providerEvents = new WeakSet(); + /** Session URIs created via import, allowed through the invokeProviders guard. */ + private readonly _importedSessions = new ResourceMap(); + + /** Human-readable titles for imported sessions. */ + private readonly _importedSessionTitles = new ResourceMap(); + activeSessionResource: URI | undefined; log(sessionResource: URI, name: string, details?: string, level: ChatDebugLogLevel = ChatDebugLogLevel.Info, options?: { id?: string; category?: string; parentEventId?: string }): void { @@ -135,10 +141,10 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic } async invokeProviders(sessionResource: URI): Promise { - if (!LocalChatSessionUri.isLocalSession(sessionResource)) { + + if (!LocalChatSessionUri.isLocalSession(sessionResource) && !this._importedSessions.has(sessionResource)) { return; } - // Cancel only the previous invocation for THIS session, not others. // Each session has its own pipeline so events from multiple sessions // can be streamed concurrently. @@ -247,6 +253,51 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic return undefined; } + isCoreEvent(event: IChatDebugEvent): boolean { + return !this._providerEvents.has(event); + } + + setImportedSessionTitle(sessionResource: URI, title: string): void { + this._importedSessionTitles.set(sessionResource, title); + } + + getImportedSessionTitle(sessionResource: URI): string | undefined { + return this._importedSessionTitles.get(sessionResource); + } + + async exportLog(sessionResource: URI): Promise { + for (const provider of this._providers) { + if (provider.provideChatDebugLogExport) { + try { + const data = await provider.provideChatDebugLogExport(sessionResource, CancellationToken.None); + if (data !== undefined) { + return data; + } + } catch (err) { + onUnexpectedError(err); + } + } + } + return undefined; + } + + async importLog(data: Uint8Array): Promise { + for (const provider of this._providers) { + if (provider.resolveChatDebugLogImport) { + try { + const sessionUri = await provider.resolveChatDebugLogImport(data, CancellationToken.None); + if (sessionUri !== undefined) { + this._importedSessions.set(sessionUri, true); + return sessionUri; + } + } catch (err) { + onUnexpectedError(err); + } + } + } + return undefined; + } + override dispose(): void { for (const cts of this._invocationCts.values()) { cts.cancel(); diff --git a/src/vscode-dts/vscode.proposed.chatDebug.d.ts b/src/vscode-dts/vscode.proposed.chatDebug.d.ts index f74f4e7ba11..3fe781d29fc 100644 --- a/src/vscode-dts/vscode.proposed.chatDebug.d.ts +++ b/src/vscode-dts/vscode.proposed.chatDebug.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 2 +// version: 3 declare module 'vscode' { /** @@ -642,6 +642,37 @@ declare module 'vscode' { eventId: string, token: CancellationToken ): ProviderResult; + + /** + * Export the debug log for a chat session as a serialized byte array. + * The extension controls the format (e.g., OTLP JSON with Copilot extensions). + * Core provides the save dialog and writes the returned bytes to disk. + * + * @param sessionResource The resource URI of the chat session to export. + * @param options Export options including core events and session metadata. + * @param token A cancellation token. + * @returns The serialized debug log data, or undefined if export is not available. + */ + provideChatDebugLogExport?( + sessionResource: Uri, + options: ChatDebugLogExportOptions, + token: CancellationToken + ): ProviderResult; + + /** + * Import a previously exported debug log from a serialized byte array. + * Core provides the open dialog and reads the file bytes. + * The extension deserializes the data and returns a session URI that can be + * opened in the debug panel via {@link provideChatDebugLog}. + * + * @param data The serialized debug log data (as returned by {@link provideChatDebugLogExport}). + * @param token A cancellation token. + * @returns The imported session info, or undefined if import failed. + */ + resolveChatDebugLogImport?( + data: Uint8Array, + token: CancellationToken + ): ProviderResult; } export namespace chat { @@ -654,4 +685,36 @@ declare module 'vscode' { */ export function registerChatDebugLogProvider(provider: ChatDebugLogProvider): Disposable; } + + /** + * Options passed to {@link ChatDebugLogProvider.provideChatDebugLogExport}. + */ + export interface ChatDebugLogExportOptions { + /** + * Core-originated debug events (prompt discovery, skill loading, etc.) + * for the session. The extension may include these in the export alongside its own data. + */ + readonly coreEvents: readonly ChatDebugEvent[]; + + /** + * Session title, if available. + * Used to provide a human-readable label in the exported file. + */ + readonly sessionTitle?: string; + } + + /** + * Result of importing a debug log via {@link ChatDebugLogProvider.resolveChatDebugLogImport}. + */ + export interface ChatDebugLogImportResult { + /** + * The session resource URI for the imported session. + */ + readonly uri: Uri; + + /** + * The session title from the imported file, if available. + */ + readonly sessionTitle?: string; + } } From 0a8edf7b2f82873d15e1ed0154502716374598bb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Mar 2026 10:57:13 -0700 Subject: [PATCH 432/448] plugins: allow updating agent plugins (#300344) * plugins: allow updating agent plugins Add update detection and update buttons to the agent plugins view and editor. This allows users to see when installed plugins have newer versions available and update them directly from the UI without manually checking for updates. - Export hasSourceChanged() from pluginMarketplaceService for reuse - Add 'outdated' and 'liveMarketplacePlugin' fields to IInstalledPluginItem - Fetch marketplace data in AgentPluginsListView.show() to cross-reference installed vs live versions and mark outdated plugins - Add UpdatePluginAction to list view with live marketplace data - Add UpdatePluginEditorAction to plugin editor with live marketplace data - Both update actions re-register the plugin with updated metadata after successful update so the UI immediately reflects the new version - Read fresh installed metadata from pluginMarketplaceService.installedPlugins store (not stale IAgentPlugin.fromMarketplace) for accurate version checks - Pass 'silent' option to runInstall() to show non-blocking notification instead of dialog when silent=true - Re-register plugins with live data after updateAllPlugins() completes so stored sourceDescriptor reflects new version/ref/sha (Commit message generated by Copilot) * pr comments and cleanup * fix test failure --- extensions/git/src/commands.ts | 27 ++- extensions/git/src/git.ts | 5 +- .../base/common/observableInternal/index.ts | 2 +- .../agentPluginEditor/agentPluginEditor.ts | 76 ++++++- .../agentPluginEditor/agentPluginItems.ts | 6 +- .../browser/agentPluginRepositoryService.ts | 58 +++-- .../contrib/chat/browser/agentPluginsView.ts | 159 +++++++++++--- .../chat/browser/pluginInstallService.ts | 181 +++++++++++++++- .../contrib/chat/browser/pluginSources.ts | 81 ++++--- .../plugins/agentPluginRepositoryService.ts | 16 +- .../common/plugins/pluginInstallService.ts | 33 ++- .../plugins/pluginMarketplaceService.ts | 201 +++++++++++++++++- .../chat/common/plugins/pluginSource.ts | 5 +- .../agentPluginRepositoryService.test.ts | 2 +- .../plugins/pluginInstallService.test.ts | 40 +++- .../browser/extensions.contribution.ts | 9 +- 16 files changed, 793 insertions(+), 108 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d3eae80b5e1..51cbef08d2e 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1064,10 +1064,33 @@ export class CommandCenter { } @command('_git.pull') - async pullRepository(repositoryPath: string): Promise { + async pullRepository(repositoryPath: string): Promise { const dotGit = await this.git.getRepositoryDotGit(repositoryPath); const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); - await repo.pull(); + return repo.pull(); + } + + @command('_git.fetchRepository') + async fetchRepository(repositoryPath: string): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + await repo.fetch(); + } + + @command('_git.revParse') + async revParse(repositoryPath: string, ref: string): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + const result = await repo.exec(['rev-parse', ref]); + return result.stdout.trim(); + } + + @command('_git.revListCount') + async revListCount(repositoryPath: string, fromRef: string, toRef: string): Promise { + const dotGit = await this.git.getRepositoryDotGit(repositoryPath); + const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger); + const result = await repo.exec(['rev-list', '--count', `${fromRef}..${toRef}`]); + return Number(result.stdout.trim()) || 0; } @command('_git.revParseAbbrevRef') diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 1bd20a42c54..90284866a51 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2424,7 +2424,7 @@ export class Repository { await this.exec(args, spawnOptions); } - async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise { + async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise { const args = ['pull']; if (options.tags) { @@ -2450,10 +2450,11 @@ export class Repository { } try { - await this.exec(args, { + const result = await this.exec(args, { cancellationToken: options.cancellationToken, env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent } }); + return !/Already up to date/i.test(result.stdout); } catch (err) { if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) { err.gitErrorCode = GitErrorCodes.Conflict; diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index af010d118c3..95347c3088b 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -7,7 +7,7 @@ export { observableValueOpts } from './observables/observableValueOpts.js'; export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta, autorunSelfDisposable } from './reactions/autorun.js'; -export { type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction } from './base.js'; +export { type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type IReaderWithStore, type ISettableObservable, type ITransaction } from './base.js'; export { disposableObservableValue } from './observables/observableValue.js'; export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './observables/derived.js'; export { type IDerivedReader } from './observables/derivedImpl.js'; diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts index 2f36d348165..216fe391589 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginEditor.ts @@ -12,7 +12,7 @@ import { Cache, CacheResult } from '../../../../../base/common/cache.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas, matchesScheme } from '../../../../../base/common/network.js'; -import { autorun } from '../../../../../base/common/observable.js'; +import { autorun, derived } from '../../../../../base/common/observable.js'; import { dirname, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; @@ -37,6 +37,7 @@ import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../../markdo import { IWebview, IWebviewService } from '../../../webview/browser/webview.js'; import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPluginService.js'; import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js'; +import { hasSourceChanged, IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plugins/pluginMarketplaceService.js'; import { AgentPluginEditorInput } from './agentPluginEditorInput.js'; import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem } from './agentPluginItems.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; @@ -97,6 +98,7 @@ export class AgentPluginEditor extends EditorPane { @IRequestService private readonly requestService: IRequestService, @IAgentPluginService private readonly agentPluginService: IAgentPluginService, @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, + @IPluginMarketplaceService private readonly pluginMarketplaceService: IPluginMarketplaceService, @ILabelService private readonly labelService: ILabelService, @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { @@ -210,12 +212,7 @@ export class AgentPluginEditor extends EditorPane { reset(template.marketplace, marketplaceLabel); } - // Set up actions reactively - const actionDisposables = this.transientDisposables.add(new DisposableStore()); - this.transientDisposables.add(autorun(reader => { - actionDisposables.clear(); - template.actionBar.clear(); - + const currentItem = derived(reader => { // Read observables to subscribe to changes const allPlugins = this.agentPluginService.plugins.read(reader); @@ -266,7 +263,33 @@ export class AgentPluginEditor extends EditorPane { } } - const actions = this.getItemActions(currentItem); + return currentItem; + }); + + const storedPlugin = currentItem.map((item, r) => { + if (!item || item.kind === AgentPluginItemKind.Marketplace) { + return undefined; + } + + return this.pluginMarketplaceService.installedPlugins.read(r) + .find(e => e.pluginUri.toString() === item.plugin.uri.toString())?.plugin + ?? item.plugin.fromMarketplace; + }); + + // Set up actions reactively + const actionDisposables = this.transientDisposables.add(new DisposableStore()); + this.transientDisposables.add(autorun(reader => { + actionDisposables.clear(); + template.actionBar.clear(); + + const current = currentItem.read(reader); + if (!current) { + return; + } + + this.pluginMarketplaceService.lastFetchedPlugins.read(reader); + + const actions = this.getItemActions(current, storedPlugin.read(reader)); if (actions.length > 0) { template.actionBar.push(actions, { icon: true, label: true }); } @@ -275,11 +298,11 @@ export class AgentPluginEditor extends EditorPane { } // Update enablement status widget - if (currentItem.kind === AgentPluginItemKind.Installed) { + if (current.kind === AgentPluginItemKind.Installed) { actionDisposables.add(this.instantiationService.createInstance( EnablementStatusWidget, template.statusContainer, - currentItem.plugin.enablement, + current.plugin.enablement, pluginEnablementLabels, )); } @@ -289,13 +312,25 @@ export class AgentPluginEditor extends EditorPane { this.activeElement = await this.openDetails(item, template, token); } - private getItemActions(item: IAgentPluginItem): Action[] { + private getItemActions(item: IAgentPluginItem, storedPlugin: IMarketplacePlugin | undefined): Action[] { if (item.kind === AgentPluginItemKind.Marketplace) { return [this.instantiationService.createInstance(InstallPluginAction, item)]; } const workspaceService = this.instantiationService.invokeFunction(a => a.get(IWorkspaceContextService)); const actions: Action[] = []; + + if (storedPlugin) { + const cachedMarketplace = this.pluginMarketplaceService.lastFetchedPlugins.get(); + const key = `${storedPlugin.marketplaceReference.canonicalId}::${storedPlugin.name}`; + const livePlugin = cachedMarketplace.find(mp => + `${mp.marketplaceReference.canonicalId}::${mp.name}` === key + ); + if (livePlugin && hasSourceChanged(storedPlugin.sourceDescriptor, livePlugin.sourceDescriptor)) { + actions.push(this.instantiationService.createInstance(UpdatePluginEditorAction, item.plugin, livePlugin)); + } + } + actions.push(createEnablePluginDropDown(item.plugin, this.agentPluginService.enablementModel, workspaceService)); actions.push(createDisablePluginDropDown(item.plugin, this.agentPluginService.enablementModel, workspaceService)); actions.push(new UninstallPluginAction(item.plugin)); @@ -535,4 +570,23 @@ export class AgentPluginEditor extends EditorPane { } } +class UpdatePluginEditorAction extends Action { + static readonly ID = 'agentPlugin.editor.update'; + + constructor( + private readonly plugin: IAgentPlugin, + private readonly liveMarketplacePlugin: IMarketplacePlugin, + @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, + @IPluginMarketplaceService private readonly pluginMarketplaceService: IPluginMarketplaceService, + ) { + super(UpdatePluginEditorAction.ID, localize('update', "Update"), 'extension-action label prominent install'); + } + + override async run(): Promise { + if (await this.pluginInstallService.updatePlugin(this.liveMarketplacePlugin)) { + this.pluginMarketplaceService.addInstalledPlugin(this.plugin.uri, this.liveMarketplacePlugin); + } + } +} + //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts index 9f1b8f8e97c..612cea9bb62 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginEditor/agentPluginItems.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../../base/common/uri.js'; +import { IObservable } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import type { IAgentPlugin } from '../../common/plugins/agentPluginService.js'; -import type { IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType } from '../../common/plugins/pluginMarketplaceService.js'; +import type { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType } from '../../common/plugins/pluginMarketplaceService.js'; export const enum AgentPluginItemKind { Installed = 'installed', @@ -18,6 +18,8 @@ export interface IInstalledPluginItem { readonly description: string; readonly marketplace?: string; readonly plugin: IAgentPlugin; + /** When set, indicates the plugin has a newer version in the marketplace. */ + readonly outdated?: IObservable; } export interface IMarketplacePluginItem { diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts index 49a0f694d65..329c531cad5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts @@ -107,38 +107,47 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi return repoDir; } - async pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise { + async pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise { const repoDir = this.getRepositoryUri(marketplace, options?.marketplaceType); const repoExists = await this._fileService.exists(repoDir); if (!repoExists) { this._logService.warn(`[AgentPluginRepositoryService] Cannot update plugin '${options?.pluginName ?? marketplace.displayLabel}': repository not cloned`); - return; + return false; } const updateLabel = options?.pluginName ?? marketplace.displayLabel; try { - await this._progressService.withProgress( + const doPull = async () => { + return !!(await this._commandService.executeCommand('_git.pull', repoDir.fsPath)); + }; + + if (options?.silent) { + return await doPull(); + } + + return await this._progressService.withProgress( { location: ProgressLocation.Notification, title: localize('updatingPlugin', "Updating plugin '{0}'...", updateLabel), cancellable: false, }, - async () => { - await this._commandService.executeCommand('_git.pull', repoDir.fsPath); - } + doPull, ); } catch (err) { this._logService.error(`[AgentPluginRepositoryService] Failed to update ${marketplace.displayLabel}:`, err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('pullFailed', "Failed to update plugin '{0}': {1}", options?.failureLabel ?? updateLabel, err?.message ?? String(err)), - actions: { - primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { - this._commandService.executeCommand('git.showOutput'); - })], - }, - }); + if (!options?.silent) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pullFailed', "Failed to update plugin '{0}': {1}", options?.failureLabel ?? updateLabel, err?.message ?? String(err)), + actions: { + primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { + this._commandService.executeCommand('git.showOutput'); + })], + }, + }); + } + throw err; } } @@ -248,7 +257,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi return repo.ensure(this._cacheRoot, plugin, options); } - async updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { + async updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { const repo = this.getPluginSource(plugin.sourceDescriptor.kind); if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { return this.pullRepository(plugin.marketplaceReference, options); @@ -256,6 +265,23 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi return repo.update(this._cacheRoot, plugin, options); } + async fetchRepository(marketplace: IMarketplaceReference): Promise { + const repoDir = this.getRepositoryUri(marketplace); + const repoExists = await this._fileService.exists(repoDir); + if (!repoExists) { + return false; + } + + try { + await this._commandService.executeCommand('_git.fetchRepository', repoDir.fsPath); + const behindCount = await this._commandService.executeCommand('_git.revListCount', repoDir.fsPath, 'HEAD', '@{u}') ?? 0; + return behindCount > 0; + } catch (err) { + this._logService.debug(`[AgentPluginRepositoryService] Silent fetch failed for ${marketplace.displayLabel}:`, err); + return false; + } + } + async cleanupPluginSource(plugin: IMarketplacePlugin): Promise { const repo = this.getPluginSource(plugin.sourceDescriptor.kind); const cleanupDir = repo.getCleanupTarget(this._cacheRoot, plugin.sourceDescriptor); diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts index 883a6cf7feb..48e0c7a57d6 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginsView.ts @@ -10,12 +10,12 @@ import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js' import { IPagedRenderer } from '../../../../base/browser/ui/list/listPaging.js'; import { Action, IAction, Separator } from '../../../../base/common/actions.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; -import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, derived, IObservable, IReaderWithStore } from '../../../../base/common/observable.js'; import { IPagedModel, PagedModel } from '../../../../base/common/paging.js'; import { dirname } from '../../../../base/common/resources.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -45,7 +45,7 @@ import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IAgentPlugin, IAgentPluginService } from '../common/plugins/agentPluginService.js'; import { isContributionEnabled } from '../common/enablement.js'; import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; -import { IMarketplacePlugin, IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js'; +import { hasSourceChanged, IMarketplacePlugin, IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js'; import { AgentPluginEditorInput } from './agentPluginEditor/agentPluginEditorInput.js'; import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from './agentPluginEditor/agentPluginItems.js'; import { getInstalledPluginContextMenuActions, InstallPluginAction, OpenPluginReadmeAction } from './agentPluginActions.js'; @@ -55,11 +55,11 @@ export const InstalledAgentPluginsViewId = 'workbench.views.agentPlugins.install //#region Item model -function installedPluginToItem(plugin: IAgentPlugin, labelService: ILabelService): IInstalledPluginItem { +function installedPluginToItem(plugin: IAgentPlugin, labelService: ILabelService, outdated?: IObservable): IInstalledPluginItem { const name = plugin.label; const description = plugin.fromMarketplace?.description ?? labelService.getUriLabel(dirname(plugin.uri), { relative: true }); const marketplace = plugin.fromMarketplace?.marketplace; - return { kind: AgentPluginItemKind.Installed, name, description, marketplace, plugin }; + return { kind: AgentPluginItemKind.Installed, name, description, marketplace, plugin, outdated }; } function marketplacePluginToItem(plugin: IMarketplacePlugin): IMarketplacePluginItem { @@ -80,6 +80,27 @@ function marketplacePluginToItem(plugin: IMarketplacePlugin): IMarketplacePlugin //#region Actions +//#region Actions + +class UpdatePluginAction extends Action { + static readonly ID = 'agentPlugin.update'; + + constructor( + private readonly plugin: IAgentPlugin, + private readonly liveMarketplacePlugin: IMarketplacePlugin, + @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, + @IPluginMarketplaceService private readonly pluginMarketplaceService: IPluginMarketplaceService, + ) { + super(UpdatePluginAction.ID, localize('update', "Update"), 'extension-action label prominent install'); + } + + override async run(): Promise { + if (await this.pluginInstallService.updatePlugin(this.liveMarketplacePlugin)) { + this.pluginMarketplaceService.addInstalledPlugin(this.plugin.uri, this.liveMarketplacePlugin); + } + } +} + class ManagePluginAction extends Action { static readonly ID = 'agentPlugin.manage'; static readonly CLASS = `extension-action icon manage ${ThemeIcon.asClassName(manageExtensionIcon)}`; @@ -194,19 +215,31 @@ class AgentPluginRenderer implements IPagedRenderer getInstalledPluginContextMenuActions(element.plugin, this.instantiationService)); - data.elementDisposables.push(manageAction); - data.actionbar.push([manageAction], { icon: true, label: false }); - } + const updateActions = (reader: IReaderWithStore) => { + data.actionbar.clear(); + if (element.kind === AgentPluginItemKind.Marketplace) { + data.detail.textContent = element.marketplace; + const installAction = this.instantiationService.createInstance(InstallPluginAction, element); + reader.store.add(installAction); + data.actionbar.push([installAction], { icon: true, label: true }); + } else { + data.detail.textContent = element.marketplace ?? ''; + const actions: Action[] = []; + const livePlugin = element.outdated?.read(reader); + if (livePlugin) { + const updateAction = this.instantiationService.createInstance(UpdatePluginAction, element.plugin, livePlugin); + reader.store.add(updateAction); + actions.push(updateAction); + } + const manageAction = this.instantiationService.createInstance(ManagePluginAction, + () => getInstalledPluginContextMenuActions(element.plugin, this.instantiationService)); + reader.store.add(manageAction); + actions.push(manageAction); + data.actionbar.push(actions, { icon: true, label: true }); + } + }; + + data.elementDisposables.push(autorun(updateActions)); } disposeElement(_element: IAgentPluginItem | undefined, _index: number, data: IAgentPluginTemplateData): void { @@ -393,7 +426,11 @@ export class AgentPluginsListView extends AbstractExtensionsListView p.name.toLowerCase().includes(lowerText) || p.description.toLowerCase().includes(lowerText)) + .map(marketplacePluginToItem); // Filter out marketplace items that are already installed const installedPaths = new Set(installed.map(i => i.plugin.uri.toString())); @@ -422,22 +459,58 @@ export class AgentPluginsListView extends AbstractExtensionsListView { + const cachedMarketplace = this.pluginMarketplaceService.lastFetchedPlugins.read(reader); + const marketplaceByKey = new Map(); + for (const mp of cachedMarketplace) { + marketplaceByKey.set(`${mp.marketplaceReference.canonicalId}::${mp.name}`, mp); + } + + + // Read fresh installed plugin metadata from the store (not from + // IAgentPlugin.fromMarketplace which may be stale after an update). + const installedByUri = new Map(); + for (const entry of this.pluginMarketplaceService.installedPlugins.read(reader)) { + installedByUri.set(entry.pluginUri.toString(), entry.plugin); + } + + return { marketplaceByKey, installedByUri }; + }); + + const plugins = this.agentPluginService.plugins.get(); - return plugins.map(p => installedPluginToItem(p, this.labelService)); + return plugins.map(p => { + const isOutdated = derived(reader => { + const { marketplaceByKey, installedByUri } = marketplaceObs.read(reader); + const storedPlugin = installedByUri.get(p.uri.toString()) ?? p.fromMarketplace; + if (storedPlugin) { + const key = `${storedPlugin.marketplaceReference.canonicalId}::${storedPlugin.name}`; + const live = marketplaceByKey.get(key); + if (live && hasSourceChanged(storedPlugin.sourceDescriptor, live.sourceDescriptor)) { + return live; + } + } + + return undefined; + }); + return installedPluginToItem(p, this.labelService, isOutdated); + }); } - private async queryMarketplace(text: string): Promise { + private async queryMarketplacePlugins(): Promise { this.queryCts.value?.cancel(); const cts = new CancellationTokenSource(); this.queryCts.value = cts; try { - const plugins = await this.pluginMarketplaceService.fetchMarketplacePlugins(cts.token); - const lowerText = text.toLowerCase(); - return plugins - .filter(p => p.name.toLowerCase().includes(lowerText) || p.description.toLowerCase().includes(lowerText)) - .map(marketplacePluginToItem); + return await this.pluginMarketplaceService.fetchMarketplacePlugins(cts.token); } catch { return []; } @@ -484,6 +557,38 @@ class AgentPluginsBrowseCommand extends Action2 { } } +class CheckForPluginUpdatesCommand extends Action2 { + constructor() { + super({ + id: 'workbench.agentPlugins.checkForUpdates', + title: localize2('agentPlugins.checkForUpdates', "Update Plugins"), + category: localize2('chat.category', "Chat"), + precondition: ChatContextKeys.enabled, + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + await accessor.get(IPluginInstallService).updateAllPlugins({}, CancellationToken.None); + } +} + +class ForceUpdatePluginsCommand extends Action2 { + constructor() { + super({ + id: 'workbench.agentPlugins.forceUpdate', + title: localize2('agentPlugins.forceUpdate', "Update Plugins (Force)"), + category: localize2('chat.category', "Chat"), + precondition: ChatContextKeys.enabled, + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + await accessor.get(IPluginInstallService).updateAllPlugins({ force: true }, CancellationToken.None); + } +} + //#endregion //#region Views contribution @@ -503,6 +608,8 @@ export class AgentPluginsViewsContribution extends Disposable implements IWorkbe })); registerAction2(AgentPluginsBrowseCommand); + registerAction2(CheckForPluginUpdatesCommand); + registerAction2(ForceUpdatePluginsCommand); Registry.as(ViewExtensions.ViewsRegistry).registerViews([ { diff --git a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts index ebe741adaad..39b0b79f015 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts @@ -3,16 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Action } from '../../../../base/common/actions.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IAgentPluginRepositoryService } from '../common/plugins/agentPluginRepositoryService.js'; -import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; -import { IMarketplacePlugin, IPluginMarketplaceService, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IPluginInstallService, IUpdateAllPluginsOptions, IUpdateAllPluginsResult } from '../common/plugins/pluginInstallService.js'; +import { IMarketplacePlugin, IPluginMarketplaceService, hasSourceChanged, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; export class PluginInstallService implements IPluginInstallService { declare readonly _serviceBrand: undefined; @@ -24,6 +28,8 @@ export class PluginInstallService implements IPluginInstallService { @INotificationService private readonly _notificationService: INotificationService, @IDialogService private readonly _dialogService: IDialogService, @ILogService private readonly _logService: ILogService, + @IProgressService private readonly _progressService: IProgressService, + @ICommandService private readonly _commandService: ICommandService, ) { } async installPlugin(plugin: IMarketplacePlugin): Promise { @@ -38,19 +44,20 @@ export class PluginInstallService implements IPluginInstallService { } if (kind === PluginSourceKind.Npm || kind === PluginSourceKind.Pip) { - return this._installPackagePlugin(plugin); + await this._installPackagePlugin(plugin); + return; } // GitHub / GitUrl return this._installGitPlugin(plugin); } - async updatePlugin(plugin: IMarketplacePlugin): Promise { + async updatePlugin(plugin: IMarketplacePlugin, silent?: boolean): Promise { const kind = plugin.sourceDescriptor.kind; if (kind === PluginSourceKind.Npm || kind === PluginSourceKind.Pip) { // Package-manager "update" re-runs install via terminal - return this._installPackagePlugin(plugin); + return this._installPackagePlugin(plugin, silent); } // For relative-path and git sources, delegate to repository service @@ -61,6 +68,161 @@ export class PluginInstallService implements IPluginInstallService { }); } + async updateAllPlugins(options: IUpdateAllPluginsOptions, token: CancellationToken): Promise { + const installed = this._pluginMarketplaceService.installedPlugins.get().filter(e => e.enabled); + if (installed.length === 0) { + return { updatedNames: [], failedNames: [] }; + } + + const updatedNames: string[] = []; + const failedNames: string[] = []; + + const doUpdate = async () => { + const gitTasks: Promise[] = []; + const packagePlugins: { installed: IMarketplacePlugin; marketplace: IMarketplacePlugin }[] = []; + + // 1. Pull each unique marketplace repository first (handles all + // relative-path plugins and ensures the marketplace index on + // disk is up-to-date before we re-read it). + const seenMarketplaces = new Set(); + for (const entry of installed) { + const ref = entry.plugin.marketplaceReference; + if (seenMarketplaces.has(ref.canonicalId)) { + continue; + } + seenMarketplaces.add(ref.canonicalId); + gitTasks.push((async () => { + if (token.isCancellationRequested) { + return; + } + + try { + const changed = await this._pluginRepositoryService.pullRepository(ref, { + pluginName: ref.displayLabel, + failureLabel: ref.displayLabel, + marketplaceType: entry.plugin.marketplaceType, + silent: options.silent, + }); + if (changed) { + updatedNames.push(ref.displayLabel); + } + } catch (err) { + this._logService.error(`[PluginInstallService] Failed to pull marketplace '${ref.displayLabel}':`, err); + failedNames.push(ref.displayLabel); + } + })()); + } + + await Promise.all(gitTasks); + + // 2. Re-fetch marketplace data *after* pulling so we see any + // updated plugin descriptors (new versions, refs, etc.). + const marketplacePlugins = await this._pluginMarketplaceService.fetchMarketplacePlugins(token); + const marketplaceByKey = new Map(); + for (const mp of marketplacePlugins) { + marketplaceByKey.set(`${mp.marketplaceReference.canonicalId}::${mp.name}`, mp); + } + + // 3. Update non-relative-path plugins individually. + const independentGitTasks: Promise[] = []; + for (const entry of installed) { + if (entry.plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { + continue; + } + + const livePlugin = marketplaceByKey.get(`${entry.plugin.marketplaceReference.canonicalId}::${entry.plugin.name}`); + if (!livePlugin || !hasSourceChanged(entry.plugin.sourceDescriptor, livePlugin.sourceDescriptor)) { + continue; + } + + const desc = livePlugin.sourceDescriptor; + if (desc.kind === PluginSourceKind.Npm || desc.kind === PluginSourceKind.Pip) { + if (!options.force && !desc.version) { + continue; + } + packagePlugins.push({ installed: entry.plugin, marketplace: livePlugin }); + continue; + } + + independentGitTasks.push((async () => { + if (token.isCancellationRequested) { + return; + } + + try { + const changed = await this._pluginRepositoryService.updatePluginSource(livePlugin, { + pluginName: livePlugin.name, + failureLabel: livePlugin.name, + marketplaceType: livePlugin.marketplaceType, + silent: options.silent, + }); + if (changed) { + updatedNames.push(livePlugin.name); + this._pluginMarketplaceService.addInstalledPlugin(entry.pluginUri, livePlugin); + } + } catch (err) { + this._logService.error(`[PluginInstallService] Failed to update plugin '${livePlugin.name}':`, err); + failedNames.push(livePlugin.name); + } + })()); + } + + await Promise.all(independentGitTasks); + + for (const { installed: _installed, marketplace } of packagePlugins) { + if (token.isCancellationRequested) { + return; + } + + try { + const changed = await this.updatePlugin(marketplace, options?.silent); + if (changed) { + updatedNames.push(marketplace.name); + const pluginUri = this._pluginRepositoryService.getPluginSourceInstallUri(marketplace.sourceDescriptor); + this._pluginMarketplaceService.addInstalledPlugin(pluginUri, marketplace); + } + } catch (err) { + this._logService.error(`[PluginInstallService] Failed to update plugin '${marketplace.name}':`, err); + failedNames.push(marketplace.name); + } + } + }; + + if (options.silent) { + await doUpdate(); + } else { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: localize('updatingAllPlugins', "Updating plugins..."), + }, + doUpdate, + ); + } + + if (failedNames.length > 0) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('updateAllFailed', "Failed to update: {0}", failedNames.join(', ')), + actions: { + primary: [new Action('showGitOutput', localize('showOutput', "Show Output"), undefined, true, () => { + this._commandService.executeCommand('git.showOutput'); + })], + }, + }); + } else if (updatedNames.length > 0) { + this._pluginMarketplaceService.clearUpdatesAvailable(); + this._notificationService.notify({ + severity: Severity.Info, + message: localize('updateAllSuccess', "Updated plugins: {0}", updatedNames.join(', ')), + }); + } else if (!token.isCancellationRequested) { + this._pluginMarketplaceService.clearUpdatesAvailable(); + } + + return { updatedNames, failedNames }; + } + getPluginInstallUri(plugin: IMarketplacePlugin): URI { if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { return this._pluginRepositoryService.getPluginInstallUri(plugin); @@ -158,11 +320,11 @@ export class PluginInstallService implements IPluginInstallService { // --- Package-manager sources (npm / pip) ---------------------------------- - private async _installPackagePlugin(plugin: IMarketplacePlugin): Promise { + private async _installPackagePlugin(plugin: IMarketplacePlugin, silent?: boolean): Promise { const repo = this._pluginRepositoryService.getPluginSource(plugin.sourceDescriptor.kind); if (!repo.runInstall) { this._logService.error(`[PluginInstallService] Expected package repository for kind '${plugin.sourceDescriptor.kind}'`); - return; + return false; } // Ensure the parent cache directory exists (returns npm/ or pip/) @@ -170,11 +332,12 @@ export class PluginInstallService implements IPluginInstallService { // The actual plugin content location (e.g. npm//node_modules/) const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(plugin.sourceDescriptor); - const result = await repo.runInstall(installDir, pluginDir, plugin); + const result = await repo.runInstall(installDir, pluginDir, plugin, { silent }); if (!result) { - return; + return false; } this._pluginMarketplaceService.addInstalledPlugin(result.pluginDir, plugin); + return true; } } diff --git a/src/vs/workbench/contrib/chat/browser/pluginSources.ts b/src/vs/workbench/contrib/chat/browser/pluginSources.ts index fef8536d1d9..8ea0f30210d 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginSources.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginSources.ts @@ -5,6 +5,7 @@ import { Action } from '../../../../base/common/actions.js'; import { CancelablePromise, timeout } from '../../../../base/common/async.js'; +import { Event } from '../../../../base/common/event.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { isWindows } from '../../../../base/common/platform.js'; import { dirname, joinPath } from '../../../../base/common/resources.js'; @@ -101,43 +102,58 @@ abstract class AbstractGitPluginSource implements IPluginSource { return repoDir; } - async update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { + async update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { const descriptor = plugin.sourceDescriptor; const repoDir = this.getInstallUri(cacheRoot, descriptor); const repoExists = await this._fileService.exists(repoDir); if (!repoExists) { this._logService.warn(`[${this.kind}] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`); - return; + return false; } const updateLabel = options?.pluginName ?? plugin.name; const failureLabel = options?.failureLabel ?? updateLabel; try { - await this._progressService.withProgress( + const doUpdate = async () => { + await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); + const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource; + let changed: boolean; + if (git.sha) { + const headBefore = await this._commandService.executeCommand('_git.revParse', repoDir.fsPath, 'HEAD').catch(() => undefined); + await this._commandService.executeCommand('git.fetch', repoDir.fsPath); + await this._checkoutRevision(repoDir, descriptor, failureLabel); + const headAfter = await this._commandService.executeCommand('_git.revParse', repoDir.fsPath, 'HEAD').catch(() => undefined); + changed = headBefore !== headAfter; + } else { + changed = !!(await this._commandService.executeCommand('_git.pull', repoDir.fsPath)); + await this._checkoutRevision(repoDir, descriptor, failureLabel); + } + return changed; + }; + + if (options?.silent) { + return await doUpdate(); + } + + return await this._progressService.withProgress( { location: ProgressLocation.Notification, title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel), cancellable: false, }, - async () => { - await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); - const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource; - if (git.sha) { - await this._commandService.executeCommand('git.fetch', repoDir.fsPath); - } else { - await this._commandService.executeCommand('_git.pull', repoDir.fsPath); - } - await this._checkoutRevision(repoDir, descriptor, failureLabel); - } + doUpdate, ); } catch (err) { this._logService.error(`[${this.kind}] Failed to update plugin source '${updateLabel}':`, err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), - actions: { primary: [showGitOutputAction(this._commandService)] }, - }); + if (!options?.silent) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + } + throw err; } } @@ -206,7 +222,7 @@ export class RelativePathPluginSource implements IPluginSource { throw new Error('Use ensureRepository() for relative-path sources'); } - async update(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { + async update(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { throw new Error('Use pullRepository() for relative-path sources'); } @@ -323,17 +339,18 @@ export abstract class AbstractPackagePluginSource implements IPluginSource { return cacheDir; } - async update(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { + async update(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { // For package-manager sources, "update" re-runs install. const installDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor); const pluginDir = this.getInstallUri(cacheRoot, plugin.sourceDescriptor); - await this.runInstall(installDir, pluginDir, plugin); + await this.runInstall(installDir, pluginDir, plugin, { silent: _options?.silent }); + return true; } - async runInstall(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin): Promise<{ pluginDir: URI } | undefined> { + async runInstall(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin, options?: { silent?: boolean }): Promise<{ pluginDir: URI } | undefined> { const args = this._buildInstallArgs(installDir, plugin); const command = formatShellCommand(args); - const confirmed = await this._confirmTerminalCommand(plugin.name, command); + const confirmed = await this._confirmTerminalCommand(plugin.name, command, options?.silent); if (!confirmed) { return undefined; } @@ -359,7 +376,23 @@ export abstract class AbstractPackagePluginSource implements IPluginSource { // -- terminal helpers (moved from PluginInstallService) --- - private async _confirmTerminalCommand(pluginName: string, command: string): Promise { + private async _confirmTerminalCommand(pluginName: string, command: string, silent?: boolean): Promise { + if (silent) { + return new Promise(resolve => { + const n = this._notificationService.notify({ + severity: Severity.Info, + message: localize('confirmPluginInstallNotification', "Plugin '{0}' wants to run: {1}", pluginName, command), + actions: { + primary: [ + new Action('installPlugin', localize('install', "Install"), undefined, true, async () => resolve(true)), + ], + }, + }); + + Event.once(n.onDidClose)(() => resolve(false)); + }); + } + const { confirmed } = await this._dialogService.confirm({ type: 'question', message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName), diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts index c0708bb503d..ea90fb6f832 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts @@ -32,6 +32,8 @@ export interface IPullRepositoryOptions { readonly failureLabel?: string; /** Marketplace type metadata for repository index updates. */ readonly marketplaceType?: MarketplaceType; + /** When `true`, suppresses progress notifications. */ + readonly silent?: boolean; } /** @@ -60,8 +62,9 @@ export interface IAgentPluginRepositoryService { /** * Pulls latest changes for a cloned marketplace repository. + * Returns `true` if the pull brought in new changes. */ - pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise; + pullRepository(marketplace: IMarketplaceReference, options?: IPullRepositoryOptions): Promise; /** * Returns the local install URI for a plugin based on its @@ -82,8 +85,9 @@ export interface IAgentPluginRepositoryService { * Updates a plugin source that is stored outside the marketplace repository. * For github/url sources this pulls latest changes and reapplies pinned * ref/sha checkout. For npm/pip sources this is a no-op. + * Returns `true` if the update brought in new changes. */ - updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; + updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; /** * Returns the {@link IPluginSource} strategy for the given @@ -101,4 +105,12 @@ export interface IAgentPluginRepositoryService { * This is best-effort: failures are logged but do not throw. */ cleanupPluginSource(plugin: IMarketplacePlugin): Promise; + + /** + * Silently fetches remote refs for a cloned marketplace repository and + * returns whether the local branch is behind the remote (i.e. updates + * are available). Returns `false` if the repo is not cloned or on + * network failure. + */ + fetchRepository(marketplace: IMarketplaceReference): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginInstallService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginInstallService.ts index 0f66af02f91..4748070ce18 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginInstallService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginInstallService.ts @@ -3,12 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { URI } from '../../../../../base/common/uri.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMarketplacePlugin } from './pluginMarketplaceService.js'; export const IPluginInstallService = createDecorator('pluginInstallService'); +export interface IUpdateAllPluginsOptions { + /** + * When `true`, also re-installs npm/pip packages that have no pinned + * version. Defaults to `false` to avoid interactive terminal prompts + * during background updates. + */ + readonly force?: boolean; + + /** + * When `true`, suppresses the progress notification. An info + * notification is still shown listing any plugins that were + * updated, and error notifications are shown on failure. + */ + readonly silent?: boolean; +} + +export interface IUpdateAllPluginsResult { + /** Names of plugins/marketplaces that were updated successfully. */ + readonly updatedNames: readonly string[]; + /** Names of plugins/marketplaces that failed to update. */ + readonly failedNames: readonly string[]; +} + export interface IPluginInstallService { readonly _serviceBrand: undefined; @@ -22,7 +46,14 @@ export interface IPluginInstallService { /** * Pulls the latest changes for an already-cloned marketplace repository. */ - updatePlugin(plugin: IMarketplacePlugin): Promise; + updatePlugin(plugin: IMarketplacePlugin): Promise; + + /** + * Updates all installed plugins. First pulls each unique marketplace + * repository, then updates non-relative-path plugins individually + * (git pull, npm install, pip install, etc.). + */ + updateAllPlugins(options: IUpdateAllPluginsOptions, token: CancellationToken): Promise; /** * Returns the URI where a marketplace plugin would be installed on disk. diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index 8fdfc8a3ba6..d67472945cb 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { runWhenGlobalIdle } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Lazy } from '../../../../../base/common/lazy.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { revive } from '../../../../../base/common/marshalling.js'; -import { IObservable } from '../../../../../base/common/observable.js'; +import { IObservable, observableValue } from '../../../../../base/common/observable.js'; import { isEqual, isEqualOrParent, joinPath, normalizePath, relativePath } from '../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -20,6 +21,7 @@ import { ObservableMemento, observableMemento } from '../../../../../platform/ob import { asJson, IRequestService } from '../../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import type { Dto } from '../../../../services/extensions/common/proxyIdentifier.js'; +import { AutoUpdateConfigurationKey, AutoUpdateConfigurationValue } from '../../../extensions/common/extensions.js'; import { ChatConfiguration } from '../constants.js'; import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js'; @@ -149,6 +151,20 @@ export interface IPluginMarketplaceService { readonly onDidChangeMarketplaces: Event; /** Installed marketplace plugins, backed by storage. */ readonly installedPlugins: IObservable; + /** + * Observable that is `true` when at least one cloned marketplace + * repository has upstream changes available. Checked periodically + * (approximately once per day) when `extensions.autoUpdate` is enabled. + */ + readonly hasUpdatesAvailable: IObservable; + /** + * Observable snapshot of the last {@link fetchMarketplacePlugins} result. + * Empty until the first fetch completes. Views should use this for + * synchronous outdated-detection instead of calling fetchMarketplacePlugins. + */ + readonly lastFetchedPlugins: IObservable; + /** Resets {@link hasUpdatesAvailable} to `false`. */ + clearUpdatesAvailable(): void; fetchMarketplacePlugins(token: CancellationToken): Promise; getMarketplacePluginMetadata(pluginUri: URI): IMarketplacePlugin | undefined; addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void; @@ -172,6 +188,13 @@ const MARKETPLACE_DEFINITIONS: { type: MarketplaceType; path: string }[] = [ const GITHUB_MARKETPLACE_CACHE_TTL_MS = 8 * 60 * 60 * 1000; const GITHUB_MARKETPLACE_CACHE_STORAGE_KEY = 'chat.plugins.marketplaces.githubCache.v1'; +/** Interval between periodic plugin update checks (24 hours). */ +const PLUGIN_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; +const PLUGIN_UPDATE_LAST_CHECK_STORAGE_KEY = 'chat.plugins.lastUpdateCheck.v1'; + +/** TTL for the lastFetchedPlugins cache (5 minutes). */ +const LAST_FETCHED_PLUGINS_TTL_MS = 5 * 60 * 1000; + interface IGitHubMarketplaceCacheEntry { readonly plugins: readonly IMarketplacePlugin[]; readonly expiresAt: number; @@ -223,15 +246,39 @@ const trustedMarketplacesMemento = observableMemento({ }, }); +interface IStoredLastFetchedPlugins { + readonly plugins: readonly IMarketplacePlugin[]; + readonly fetchedAt: number; + readonly configFingerprint: string; +} + +const lastFetchedPluginsMemento = observableMemento({ + defaultValue: { plugins: [], fetchedAt: 0, configFingerprint: '' }, + key: 'chat.plugins.lastFetchedPlugins.v2', + toStorage: value => JSON.stringify(value), + fromStorage: value => { + const parsed = JSON.parse(value); + if (parsed && Array.isArray(parsed.plugins)) { + return parsed; + } + return { plugins: [], fetchedAt: 0, configFingerprint: '' }; + }, +}); + export class PluginMarketplaceService extends Disposable implements IPluginMarketplaceService { declare readonly _serviceBrand: undefined; private readonly _gitHubMarketplaceCache = new Lazy>(() => this._loadPersistedGitHubMarketplaceCache()); private readonly _installedPluginsStore: ObservableMemento; private readonly _trustedMarketplacesStore: ObservableMemento; + private readonly _lastFetchedPluginsStore: ObservableMemento; + private readonly _hasUpdatesAvailable = observableValue('hasUpdatesAvailable', false); + private _updateCheckTimer: ReturnType | undefined; readonly onDidChangeMarketplaces: Event; readonly installedPlugins: IObservable; + readonly hasUpdatesAvailable: IObservable = this._hasUpdatesAvailable; + readonly lastFetchedPlugins: IObservable; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -251,6 +298,15 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke trustedMarketplacesMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, _storageService) ); + this._lastFetchedPluginsStore = this._register( + lastFetchedPluginsMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, _storageService) + ); + + this.lastFetchedPlugins = this._lastFetchedPluginsStore.map(s => { + const revived = revive(s) as IStoredLastFetchedPlugins; + return revived.plugins.map(ensureSourceDescriptor); + }); + this.installedPlugins = this._installedPluginsStore.map(s => (revive(s) as readonly IMarketplaceInstalledPlugin[]).map(e => ({ ...e, @@ -262,6 +318,27 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke _configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.PluginsEnabled) || e.affectsConfiguration(ChatConfiguration.PluginMarketplaces), ) as Event as Event; + + this._register(runWhenGlobalIdle(() => { + // Schedule periodic update checks when auto-update is enabled. + this._scheduleUpdateCheck(); + this._register(Event.filter( + _configurationService.onDidChangeConfiguration, + e => e.affectsConfiguration(AutoUpdateConfigurationKey), + )(() => this._scheduleUpdateCheck())); + })); + } + + override dispose(): void { + if (this._updateCheckTimer !== undefined) { + clearTimeout(this._updateCheckTimer); + this._updateCheckTimer = undefined; + } + super.dispose(); + } + + clearUpdatesAvailable(): void { + this._hasUpdatesAvailable.set(false, undefined); } async fetchMarketplacePlugins(token: CancellationToken): Promise { @@ -272,6 +349,16 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke const configuredRefs = this._configurationService.getValue(ChatConfiguration.PluginMarketplaces) ?? []; const refs = parseMarketplaceReferences(configuredRefs); + // Return cached results if recent and the marketplace config is unchanged. + const configFingerprint = refs.map(r => r.canonicalId).sort().join('\n'); + const stored = this._lastFetchedPluginsStore.get(); + if (stored.configFingerprint === configFingerprint && Date.now() - stored.fetchedAt < LAST_FETCHED_PLUGINS_TTL_MS) { + const cached = this.lastFetchedPlugins.get(); + if (cached.length > 0) { + return [...cached]; + } + } + for (const value of configuredRefs) { if (typeof value !== 'string' || !parseMarketplaceReference(value)) { this._logService.debug(`[PluginMarketplaceService] Ignoring invalid marketplace entry: ${String(value)}`); @@ -286,7 +373,9 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke return this._fetchFromClonedRepo(ref, token); }) ); - return results.flat(); + const plugins = results.flat(); + this._lastFetchedPluginsStore.set({ plugins, fetchedAt: Date.now(), configFingerprint }, undefined); + return plugins; } private async _fetchFromGitHubRepo(reference: IMarketplaceReference, repo: string, token: CancellationToken): Promise { @@ -454,9 +543,10 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void { const current = this.installedPlugins.get(); - if (current.some(e => isEqual(e.pluginUri, pluginUri))) { + const existing = current.find(e => isEqual(e.pluginUri, pluginUri)); + if (existing) { // Still update to trigger watchers to re-check, something might have happened that we want to know about - this._installedPluginsStore.set([...current], undefined); + this._installedPluginsStore.set(current.map(c => c === existing ? { pluginUri, plugin, enabled: existing.enabled } : c), undefined); } else { this._installedPluginsStore.set([...current, { pluginUri, plugin, enabled: true }], undefined); } @@ -486,6 +576,84 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke } } + // --- Periodic update check ------------------------------------------------ + + private _isAutoUpdateEnabled(): AutoUpdateConfigurationValue { + return this._configurationService.getValue(AutoUpdateConfigurationKey); + } + + /** + * (Re-)schedules the next periodic update check. Called on + * construction and whenever the auto-update config changes. + */ + private _scheduleUpdateCheck(): void { + if (this._updateCheckTimer !== undefined) { + clearTimeout(this._updateCheckTimer); + this._updateCheckTimer = undefined; + } + + if (!this._isAutoUpdateEnabled()) { + return; + } + + const lastCheck = this._storageService.getNumber( + PLUGIN_UPDATE_LAST_CHECK_STORAGE_KEY, + StorageScope.APPLICATION, + 0, + ); + const elapsed = Date.now() - lastCheck; + const delay = Math.max(0, PLUGIN_UPDATE_CHECK_INTERVAL_MS - elapsed); + + this._updateCheckTimer = setTimeout(() => this._runUpdateCheck(), delay); + } + + private async _runUpdateCheck(): Promise { + this._updateCheckTimer = undefined; + + try { + const installed = this.installedPlugins.get().filter(e => e.enabled); + if (installed.length === 0) { + return; + } + + const seenMarketplaces = new Set(); + let hasUpdates = false; + + for (const entry of installed) { + const ref = entry.plugin.marketplaceReference; + if (seenMarketplaces.has(ref.canonicalId)) { + continue; + } + seenMarketplaces.add(ref.canonicalId); + + try { + const behind = await this._pluginRepositoryService.fetchRepository(ref); + if (behind) { + hasUpdates = true; + break; + } + } catch (err) { + this._logService.debug(`[PluginMarketplaceService] Update check failed for ${ref.displayLabel}:`, err); + } + } + + this._hasUpdatesAvailable.set(hasUpdates, undefined); + this._storageService.store( + PLUGIN_UPDATE_LAST_CHECK_STORAGE_KEY, + Date.now(), + StorageScope.APPLICATION, + StorageTarget.MACHINE, + ); + } catch (err) { + this._logService.debug('[PluginMarketplaceService] Periodic update check failed:', err); + } finally { + // Reschedule for the next check + if (this._isAutoUpdateEnabled()) { + this._updateCheckTimer = setTimeout(() => this._runUpdateCheck(), PLUGIN_UPDATE_CHECK_INTERVAL_MS); + } + } + } + private async _fetchFromClonedRepo(reference: IMarketplaceReference, token: CancellationToken): Promise { let repoDir: URI; try { @@ -891,6 +1059,31 @@ export function getPluginSourceLabel(descriptor: IPluginSourceDescriptor): strin } } +/** + * Returns `true` when the marketplace source descriptor differs from the + * installed one — meaning an update should be performed. + */ +export function hasSourceChanged(installed: IPluginSourceDescriptor, marketplace: IPluginSourceDescriptor): boolean { + if (installed.kind !== marketplace.kind) { + return true; + } + + switch (installed.kind) { + case PluginSourceKind.GitHub: + return installed.ref !== (marketplace as typeof installed).ref + || installed.sha !== (marketplace as typeof installed).sha; + case PluginSourceKind.GitUrl: + return installed.ref !== (marketplace as typeof installed).ref + || installed.sha !== (marketplace as typeof installed).sha; + case PluginSourceKind.Npm: + return installed.version !== (marketplace as typeof installed).version; + case PluginSourceKind.Pip: + return installed.version !== (marketplace as typeof installed).version; + default: + return false; + } +} + function getMarketplaceReadmeUri(repo: string, source: string): URI { const normalizedSource = source.trim().replace(/^\.?\/+|\/+$/g, ''); const readmePath = normalizedSource ? `${normalizedSource}/README.md` : 'README.md'; diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts index 704d4334b0c..dd5cd838580 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts @@ -33,8 +33,9 @@ export interface IPluginSource { /** * Update an already-installed plugin source (git pull, npm update, etc.). + * Returns `true` if the update brought in new changes. */ - update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; + update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; /** * Returns the on-disk directory to delete when this plugin is @@ -59,5 +60,5 @@ export interface IPluginSource { * * Not implemented by non-package-manager sources. */ - runInstall?(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin): Promise<{ pluginDir: URI } | undefined>; + runInstall?(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin, options?: { silent?: boolean }): Promise<{ pluginDir: URI } | undefined>; } diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts index f2f3b310a13..6fe9944953e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts @@ -208,7 +208,7 @@ suite('AgentPluginRepositoryService', () => { marketplaceType: MarketplaceType.Copilot, }); - assert.deepStrictEqual(commands, ['git.openRepository', 'git.fetch', '_git.checkout']); + assert.deepStrictEqual(commands, ['git.openRepository', '_git.revParse', 'git.fetch', '_git.checkout', '_git.revParse']); }); // ========================================================================= diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts index 1d8b6862fa3..2804da7b73a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts @@ -158,7 +158,7 @@ suite('PluginInstallService', () => { getCleanupTarget: () => URI.file('/mock-cleanup'), getInstallUri: () => URI.file('/mock'), ensure: async () => state.ensurePluginSourceResult, - update: async () => { }, + update: async () => true, getLabel: (d) => kind === PluginSourceKind.Npm ? (d as { package: string }).package : (d as { package: string }).package, runInstall: async (_installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin) => { // Simulate confirmation dialog @@ -209,8 +209,8 @@ suite('PluginInstallService', () => { const mockSourceRepos = new Map([ [PluginSourceKind.RelativePath, { kind: PluginSourceKind.RelativePath, getCleanupTarget: () => undefined, getInstallUri: () => { throw new Error(); }, ensure: async () => { throw new Error(); }, update: async () => { throw new Error(); }, getLabel: (d) => (d as { path: string }).path || '.' }], - [PluginSourceKind.GitHub, { kind: PluginSourceKind.GitHub, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => { }, getLabel: (d) => (d as { repo: string }).repo }], - [PluginSourceKind.GitUrl, { kind: PluginSourceKind.GitUrl, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => { }, getLabel: (d) => (d as { url: string }).url }], + [PluginSourceKind.GitHub, { kind: PluginSourceKind.GitHub, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => true, getLabel: (d) => (d as { repo: string }).repo }], + [PluginSourceKind.GitUrl, { kind: PluginSourceKind.GitUrl, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => true, getLabel: (d) => (d as { url: string }).url }], [PluginSourceKind.Npm, makeMockPackageRepo(PluginSourceKind.Npm)], [PluginSourceKind.Pip, makeMockPackageRepo(PluginSourceKind.Pip)], ]); @@ -668,6 +668,23 @@ suite('PluginInstallService', () => { assert.ok(state.terminalCommands[0].includes('npm')); }); + test('does not report npm plugin as updated when install is declined', async () => { + const { service, state } = createService({ + dialogConfirmResult: false, + ensurePluginSourceResult: URI.file('/cache/agentPlugins/npm/my-pkg'), + pluginSourceInstallUris: new Map([['npm', URI.file('/cache/agentPlugins/npm/my-pkg/node_modules/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Npm, package: 'my-pkg' }, + }); + + const updated = await service.updatePlugin(plugin); + + assert.strictEqual(updated, false); + assert.strictEqual(state.terminalCommands.length, 0); + assert.strictEqual(state.addedPlugins.length, 0); + }); + test('re-installs for pip plugin updates', async () => { const { service, state } = createService({ ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), @@ -682,6 +699,23 @@ suite('PluginInstallService', () => { assert.strictEqual(state.terminalCommands.length, 1); assert.ok(state.terminalCommands[0].includes('pip')); }); + + test('does not report pip plugin as updated when install is declined', async () => { + const { service, state } = createService({ + dialogConfirmResult: false, + ensurePluginSourceResult: URI.file('/cache/agentPlugins/pip/my-pkg'), + pluginSourceInstallUris: new Map([['pip', URI.file('/cache/agentPlugins/pip/my-pkg')]]), + }); + const plugin = createPlugin({ + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pkg' }, + }); + + const updated = await service.updatePlugin(plugin); + + assert.strictEqual(updated, false); + assert.strictEqual(state.terminalCommands.length, 0); + assert.strictEqual(state.addedPlugins.length, 0); + }); }); // ========================================================================= diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 37e6e916e16..6e2b340903c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -62,6 +62,7 @@ import { IPreferencesService } from '../../../services/preferences/common/prefer import { CONTEXT_SYNC_ENABLEMENT } from '../../../services/userDataSync/common/userDataSync.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from '../../../services/workspaces/common/workspaceTrust.js'; +import { IPluginInstallService } from '../../chat/common/plugins/pluginInstallService.js'; import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js'; import { CONTEXT_KEYBINDINGS_EDITOR } from '../../preferences/common/preferences.js'; import { IWebview } from '../../webview/browser/webview.js'; @@ -557,6 +558,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, @IProductService private readonly productService: IProductService, + @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, ) { super(); const hasLocalServerContext = CONTEXT_HAS_LOCAL_SERVER.bindTo(contextKeyService); @@ -698,11 +700,14 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi order: 1 }], run: async () => { - await this.extensionsWorkbenchService.checkForUpdates(); + const [, pluginResult] = await Promise.all([ + this.extensionsWorkbenchService.checkForUpdates(), + this.pluginInstallService.updateAllPlugins({ silent: true }, CancellationToken.None), + ]); const outdated = this.extensionsWorkbenchService.outdated; if (outdated.length) { return this.extensionsWorkbenchService.openSearch('@outdated '); - } else { + } else if (pluginResult.updatedNames.length === 0 && pluginResult.failedNames.length === 0) { return this.dialogService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); } } From cb61e0d5bd0d53a1a57ebf4693ebea81549c52d3 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 10 Mar 2026 18:57:33 +0100 Subject: [PATCH 433/448] Fix source path duplication in NLS plugin source maps and add corresponding test (#300487) --- build/next/nls-plugin.ts | 6 +++++- build/next/test/nls-sourcemap.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 9f3bfa01e35..e2b19f7d7f1 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -427,7 +427,11 @@ export function nlsPlugin(options: NLSPluginOptions): esbuild.Plugin { // back to the original. Embed it inline so esbuild composes it // with its own bundle source map, making the final map point to // the original TS source. - const sourceName = relativePath.replace(/\\/g, '/'); + // This inline source map is resolved relative to esbuild's sourcefile + // for args.path. Using the full repo-relative path here makes esbuild + // resolve it against the file's own directory, which duplicates the + // directory segments in the final bundled source map. + const sourceName = path.basename(args.path); const sourcemap = generateNLSSourceMap(source, sourceName, edits); const encodedMap = Buffer.from(sourcemap).toString('base64'); const contentsWithMap = code + `\n//# sourceMappingURL=data:application/json;base64,${encodedMap}\n`; diff --git a/build/next/test/nls-sourcemap.test.ts b/build/next/test/nls-sourcemap.test.ts index 09bad2f5c27..c3aad2c80dc 100644 --- a/build/next/test/nls-sourcemap.test.ts +++ b/build/next/test/nls-sourcemap.test.ts @@ -220,6 +220,28 @@ suite('NLS plugin source maps', () => { } }); + test('NLS-affected nested file keeps a non-duplicated source path', async () => { + const source = [ + 'import { localize } from "../../vs/nls";', + 'export const msg = localize("myKey", "Hello World");', + ].join('\n'); + + const { mapJson, cleanup } = await bundleWithNLS( + { 'nested/deep/file.ts': source }, + 'nested/deep/file.ts', + ); + + try { + const sources: string[] = mapJson.sources ?? []; + const nestedSource = sources.find((s: string) => s.endsWith('/nested/deep/file.ts')); + assert.ok(nestedSource, 'Should find nested/deep/file.ts in sources'); + assert.ok(!nestedSource.includes('/nested/deep/nested/deep/file.ts'), + `Source path should not duplicate directory segments. Actual: ${nestedSource}`); + } finally { + cleanup(); + } + }); + test('line mapping correct for code after localize calls', async () => { const source = [ 'import { localize } from "../vs/nls";', // 1 From 01eb53f0f975ea9b316210e921f0144fcf99fe25 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:03:43 -0700 Subject: [PATCH 434/448] Bump xterm to take IME overflow fix (#300322) Bump xterm to take IME fix --- package-lock.json | 96 ++++++++++++++++++------------------ package.json | 20 ++++---- remote/package-lock.json | 96 ++++++++++++++++++------------------ remote/package.json | 20 ++++---- remote/web/package-lock.json | 88 ++++++++++++++++----------------- remote/web/package.json | 18 +++---- 6 files changed, 169 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2a666fcfa7..f72b18718f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,16 +30,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.169", - "@xterm/addon-image": "^0.10.0-beta.169", - "@xterm/addon-ligatures": "^0.11.0-beta.169", - "@xterm/addon-progress": "^0.3.0-beta.169", - "@xterm/addon-search": "^0.17.0-beta.169", - "@xterm/addon-serialize": "^0.15.0-beta.169", - "@xterm/addon-unicode11": "^0.10.0-beta.169", - "@xterm/addon-webgl": "^0.20.0-beta.168", - "@xterm/headless": "^6.1.0-beta.169", - "@xterm/xterm": "^6.1.0-beta.169", + "@xterm/addon-clipboard": "^0.3.0-beta.180", + "@xterm/addon-image": "^0.10.0-beta.180", + "@xterm/addon-ligatures": "^0.11.0-beta.180", + "@xterm/addon-progress": "^0.3.0-beta.180", + "@xterm/addon-search": "^0.17.0-beta.180", + "@xterm/addon-serialize": "^0.15.0-beta.180", + "@xterm/addon-unicode11": "^0.10.0-beta.180", + "@xterm/addon-webgl": "^0.20.0-beta.179", + "@xterm/headless": "^6.1.0-beta.180", + "@xterm/xterm": "^6.1.0-beta.180", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -4343,30 +4343,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.169.tgz", - "integrity": "sha512-EJtjBTEAmgPCLRCwR0sF3tkOlZqswKX33Su9oeuAxjuFKsW5WvzOrGOAkMsfrc1i9zMUyzEcAKRknZyGYgr6ag==", + "version": "0.3.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.180.tgz", + "integrity": "sha512-8vJ2BN8tot7Qqgl1p0ALXIy/SOvF7nQmh3DEZph6ZARNuV3JUrpF8xdK+lmd25/fm7NFgnGdntLe9k/g2qbAgw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.169.tgz", - "integrity": "sha512-uNkn/31WeQ3qei98p3Z4TC/LNajsI8T/D0vXKWj45VOS5gCnvogTsagNNbdbV9ifEQsPg/bP9oPjWIpW2V0GGQ==", + "version": "0.10.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.180.tgz", + "integrity": "sha512-CGZxW47ZllCqS6n4vVD0lVCz2sq9FqP6czQoCJBStFJho05ioVLy0wQr8XCDJO87wuYvPjJaU3BlsyCDL2xVzQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.169.tgz", - "integrity": "sha512-CdInbfQsP1fFfXHiqsOl6TnpN75LV/BinLXrWfDe3u4yDFKB+1F8uFVqxSGKGoKdjZXn119CQZ+Op2WlsjIEPA==", + "version": "0.11.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.180.tgz", + "integrity": "sha512-YiCxw7P1rj5TVs25BJT9OiZ9QghNtboBhpYB7KZUOB2aqlCPsyIknf9GMbCrZegDfMMa22FWPyXF4oeZDBmpFg==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -4376,7 +4376,7 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { @@ -4398,63 +4398,63 @@ "license": "ISC" }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.169.tgz", - "integrity": "sha512-dPAEWBZ80Mwro4S98xzhyeW9o3hxJow5XkKnUxJBi28BwzmWijLS1eDdnI0jQDQkZ9T8bmvtHPCdypebG6LlgA==", + "version": "0.3.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.180.tgz", + "integrity": "sha512-73vv8C6Fg8CBhZZUNyX286kaNNd3iwjimSqctPZhN1wPUO4wE9mOp52hP6vCi2Vq8aZvbREmVcAvAIzNx0iNNg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.169.tgz", - "integrity": "sha512-omAD9odPBHT7zkM2W8mr6nIY63QNn8mu3iNlo53j+JBf1PBrwXxgOCrXkGYy4CcCX66oTtWnhqaHOQS9Eal8Dw==", + "version": "0.17.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.180.tgz", + "integrity": "sha512-w6dwnHeA9xxmTon2JF6SK1NwfNcmq8LzGPjyzPTwRKleFayHOH88MyIz/HCfgey+AbZRh8gsiA+lvS89/YMcQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.169.tgz", - "integrity": "sha512-PJeXwXORBsdo6VpPgdpMbiwZIhigaV9b9E/vKjUa75dy3qKGrHl/QmptDvOF1JCVjea9loaEvQ0pKHUoYJxzxA==", + "version": "0.15.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.180.tgz", + "integrity": "sha512-+lmgiIXKWMdQvxBM5MfEX4QQ6f4F1jz4VhxnIcfuxUodq5gE+aQHC7gA7zT/LE1R7UJKT0t2byNqFb96lVu0QQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.169.tgz", - "integrity": "sha512-Nf3uCCibOGl1yN+cgzYwEf5iV1hVbBOT/qa2HUl3vJNulyOkBEa+GTQI4fi2zlPrt+/VCtbc3eW2qSxOD5I3sg==", + "version": "0.10.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.180.tgz", + "integrity": "sha512-lStuJCp27ftt7BC7V1poSbPM4J2rluNUybeMn9/D9Kj0bNQYEqMX+cxjYPgVCbgnQIluQAsay8fS2pGiRM4PZA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.168.tgz", - "integrity": "sha512-5+WenEODiuatzM1/qKkDi9YQ6zM+N+iFM2P/jX4KjXwxsewHdzkExm/TZkbhkyhqlnZxxSeg3cKNxTnPevZhqQ==", + "version": "0.20.0-beta.179", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.179.tgz", + "integrity": "sha512-mYWo305LkZjK+wUt+27+GHXs8NTV5YCzxRY6l9vWaqd+/5V/JdzyZTTR4gA3Vc4CEQETUrhwAVex5arzjWE/IA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.169.tgz", - "integrity": "sha512-HQAy6ILa+ZdND7gG9hpII1aTpD6+KdlSL/k1DEmR6oKql3iPGRRGNIESSXymVp0G8F/XL+ZPCMHMBjrBHD7jAQ==", + "version": "6.1.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.180.tgz", + "integrity": "sha512-ls1zAXWBWmBGclvhduQoNO66ZNFqaWZY+SqlKl76p/02EKoPMVdPQ0VSc+w7G+Il3OZdL+HMyq6of7b/V2nZ8Q==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.169.tgz", - "integrity": "sha512-8/wbTnEDRpyMh3bTC8dhb6g4FXobyAPA/1EAnuPWe+NLkiA8Fo1fjFVbEqe9TXa8KMHXEivI7Hqj1vPDt87A4w==", + "version": "6.1.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.180.tgz", + "integrity": "sha512-EBF41C3OWwmy5XyWbxiWvgjnb1mXvzwLfFlOvRZPnHA/esXjvwF30aJRvpfI8tPxkNlT05zsx908hLv4XwDuRg==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/package.json b/package.json index 2080bf15658..8576372167d 100644 --- a/package.json +++ b/package.json @@ -100,16 +100,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.169", - "@xterm/addon-image": "^0.10.0-beta.169", - "@xterm/addon-ligatures": "^0.11.0-beta.169", - "@xterm/addon-progress": "^0.3.0-beta.169", - "@xterm/addon-search": "^0.17.0-beta.169", - "@xterm/addon-serialize": "^0.15.0-beta.169", - "@xterm/addon-unicode11": "^0.10.0-beta.169", - "@xterm/addon-webgl": "^0.20.0-beta.168", - "@xterm/headless": "^6.1.0-beta.169", - "@xterm/xterm": "^6.1.0-beta.169", + "@xterm/addon-clipboard": "^0.3.0-beta.180", + "@xterm/addon-image": "^0.10.0-beta.180", + "@xterm/addon-ligatures": "^0.11.0-beta.180", + "@xterm/addon-progress": "^0.3.0-beta.180", + "@xterm/addon-search": "^0.17.0-beta.180", + "@xterm/addon-serialize": "^0.15.0-beta.180", + "@xterm/addon-unicode11": "^0.10.0-beta.180", + "@xterm/addon-webgl": "^0.20.0-beta.179", + "@xterm/headless": "^6.1.0-beta.180", + "@xterm/xterm": "^6.1.0-beta.180", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index 3f11da9088f..cb95c26bc12 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -22,16 +22,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.169", - "@xterm/addon-image": "^0.10.0-beta.169", - "@xterm/addon-ligatures": "^0.11.0-beta.169", - "@xterm/addon-progress": "^0.3.0-beta.169", - "@xterm/addon-search": "^0.17.0-beta.169", - "@xterm/addon-serialize": "^0.15.0-beta.169", - "@xterm/addon-unicode11": "^0.10.0-beta.169", - "@xterm/addon-webgl": "^0.20.0-beta.168", - "@xterm/headless": "^6.1.0-beta.169", - "@xterm/xterm": "^6.1.0-beta.169", + "@xterm/addon-clipboard": "^0.3.0-beta.180", + "@xterm/addon-image": "^0.10.0-beta.180", + "@xterm/addon-ligatures": "^0.11.0-beta.180", + "@xterm/addon-progress": "^0.3.0-beta.180", + "@xterm/addon-search": "^0.17.0-beta.180", + "@xterm/addon-serialize": "^0.15.0-beta.180", + "@xterm/addon-unicode11": "^0.10.0-beta.180", + "@xterm/addon-webgl": "^0.20.0-beta.179", + "@xterm/headless": "^6.1.0-beta.180", + "@xterm/xterm": "^6.1.0-beta.180", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -579,30 +579,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.169.tgz", - "integrity": "sha512-EJtjBTEAmgPCLRCwR0sF3tkOlZqswKX33Su9oeuAxjuFKsW5WvzOrGOAkMsfrc1i9zMUyzEcAKRknZyGYgr6ag==", + "version": "0.3.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.180.tgz", + "integrity": "sha512-8vJ2BN8tot7Qqgl1p0ALXIy/SOvF7nQmh3DEZph6ZARNuV3JUrpF8xdK+lmd25/fm7NFgnGdntLe9k/g2qbAgw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.169.tgz", - "integrity": "sha512-uNkn/31WeQ3qei98p3Z4TC/LNajsI8T/D0vXKWj45VOS5gCnvogTsagNNbdbV9ifEQsPg/bP9oPjWIpW2V0GGQ==", + "version": "0.10.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.180.tgz", + "integrity": "sha512-CGZxW47ZllCqS6n4vVD0lVCz2sq9FqP6czQoCJBStFJho05ioVLy0wQr8XCDJO87wuYvPjJaU3BlsyCDL2xVzQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.169.tgz", - "integrity": "sha512-CdInbfQsP1fFfXHiqsOl6TnpN75LV/BinLXrWfDe3u4yDFKB+1F8uFVqxSGKGoKdjZXn119CQZ+Op2WlsjIEPA==", + "version": "0.11.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.180.tgz", + "integrity": "sha512-YiCxw7P1rj5TVs25BJT9OiZ9QghNtboBhpYB7KZUOB2aqlCPsyIknf9GMbCrZegDfMMa22FWPyXF4oeZDBmpFg==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -612,67 +612,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.169.tgz", - "integrity": "sha512-dPAEWBZ80Mwro4S98xzhyeW9o3hxJow5XkKnUxJBi28BwzmWijLS1eDdnI0jQDQkZ9T8bmvtHPCdypebG6LlgA==", + "version": "0.3.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.180.tgz", + "integrity": "sha512-73vv8C6Fg8CBhZZUNyX286kaNNd3iwjimSqctPZhN1wPUO4wE9mOp52hP6vCi2Vq8aZvbREmVcAvAIzNx0iNNg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.169.tgz", - "integrity": "sha512-omAD9odPBHT7zkM2W8mr6nIY63QNn8mu3iNlo53j+JBf1PBrwXxgOCrXkGYy4CcCX66oTtWnhqaHOQS9Eal8Dw==", + "version": "0.17.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.180.tgz", + "integrity": "sha512-w6dwnHeA9xxmTon2JF6SK1NwfNcmq8LzGPjyzPTwRKleFayHOH88MyIz/HCfgey+AbZRh8gsiA+lvS89/YMcQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.169.tgz", - "integrity": "sha512-PJeXwXORBsdo6VpPgdpMbiwZIhigaV9b9E/vKjUa75dy3qKGrHl/QmptDvOF1JCVjea9loaEvQ0pKHUoYJxzxA==", + "version": "0.15.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.180.tgz", + "integrity": "sha512-+lmgiIXKWMdQvxBM5MfEX4QQ6f4F1jz4VhxnIcfuxUodq5gE+aQHC7gA7zT/LE1R7UJKT0t2byNqFb96lVu0QQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.169.tgz", - "integrity": "sha512-Nf3uCCibOGl1yN+cgzYwEf5iV1hVbBOT/qa2HUl3vJNulyOkBEa+GTQI4fi2zlPrt+/VCtbc3eW2qSxOD5I3sg==", + "version": "0.10.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.180.tgz", + "integrity": "sha512-lStuJCp27ftt7BC7V1poSbPM4J2rluNUybeMn9/D9Kj0bNQYEqMX+cxjYPgVCbgnQIluQAsay8fS2pGiRM4PZA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.168.tgz", - "integrity": "sha512-5+WenEODiuatzM1/qKkDi9YQ6zM+N+iFM2P/jX4KjXwxsewHdzkExm/TZkbhkyhqlnZxxSeg3cKNxTnPevZhqQ==", + "version": "0.20.0-beta.179", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.179.tgz", + "integrity": "sha512-mYWo305LkZjK+wUt+27+GHXs8NTV5YCzxRY6l9vWaqd+/5V/JdzyZTTR4gA3Vc4CEQETUrhwAVex5arzjWE/IA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.169.tgz", - "integrity": "sha512-HQAy6ILa+ZdND7gG9hpII1aTpD6+KdlSL/k1DEmR6oKql3iPGRRGNIESSXymVp0G8F/XL+ZPCMHMBjrBHD7jAQ==", + "version": "6.1.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.180.tgz", + "integrity": "sha512-ls1zAXWBWmBGclvhduQoNO66ZNFqaWZY+SqlKl76p/02EKoPMVdPQ0VSc+w7G+Il3OZdL+HMyq6of7b/V2nZ8Q==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.169.tgz", - "integrity": "sha512-8/wbTnEDRpyMh3bTC8dhb6g4FXobyAPA/1EAnuPWe+NLkiA8Fo1fjFVbEqe9TXa8KMHXEivI7Hqj1vPDt87A4w==", + "version": "6.1.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.180.tgz", + "integrity": "sha512-EBF41C3OWwmy5XyWbxiWvgjnb1mXvzwLfFlOvRZPnHA/esXjvwF30aJRvpfI8tPxkNlT05zsx908hLv4XwDuRg==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index bcacae3a1ab..373bbc43d44 100644 --- a/remote/package.json +++ b/remote/package.json @@ -17,16 +17,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.169", - "@xterm/addon-image": "^0.10.0-beta.169", - "@xterm/addon-ligatures": "^0.11.0-beta.169", - "@xterm/addon-progress": "^0.3.0-beta.169", - "@xterm/addon-search": "^0.17.0-beta.169", - "@xterm/addon-serialize": "^0.15.0-beta.169", - "@xterm/addon-unicode11": "^0.10.0-beta.169", - "@xterm/addon-webgl": "^0.20.0-beta.168", - "@xterm/headless": "^6.1.0-beta.169", - "@xterm/xterm": "^6.1.0-beta.169", + "@xterm/addon-clipboard": "^0.3.0-beta.180", + "@xterm/addon-image": "^0.10.0-beta.180", + "@xterm/addon-ligatures": "^0.11.0-beta.180", + "@xterm/addon-progress": "^0.3.0-beta.180", + "@xterm/addon-search": "^0.17.0-beta.180", + "@xterm/addon-serialize": "^0.15.0-beta.180", + "@xterm/addon-unicode11": "^0.10.0-beta.180", + "@xterm/addon-webgl": "^0.20.0-beta.179", + "@xterm/headless": "^6.1.0-beta.180", + "@xterm/xterm": "^6.1.0-beta.180", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 284a4c27dcd..33bd3f8e13b 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -14,15 +14,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.169", - "@xterm/addon-image": "^0.10.0-beta.169", - "@xterm/addon-ligatures": "^0.11.0-beta.169", - "@xterm/addon-progress": "^0.3.0-beta.169", - "@xterm/addon-search": "^0.17.0-beta.169", - "@xterm/addon-serialize": "^0.15.0-beta.169", - "@xterm/addon-unicode11": "^0.10.0-beta.169", - "@xterm/addon-webgl": "^0.20.0-beta.168", - "@xterm/xterm": "^6.1.0-beta.169", + "@xterm/addon-clipboard": "^0.3.0-beta.180", + "@xterm/addon-image": "^0.10.0-beta.180", + "@xterm/addon-ligatures": "^0.11.0-beta.180", + "@xterm/addon-progress": "^0.3.0-beta.180", + "@xterm/addon-search": "^0.17.0-beta.180", + "@xterm/addon-serialize": "^0.15.0-beta.180", + "@xterm/addon-unicode11": "^0.10.0-beta.180", + "@xterm/addon-webgl": "^0.20.0-beta.179", + "@xterm/xterm": "^6.1.0-beta.180", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", @@ -100,30 +100,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.169.tgz", - "integrity": "sha512-EJtjBTEAmgPCLRCwR0sF3tkOlZqswKX33Su9oeuAxjuFKsW5WvzOrGOAkMsfrc1i9zMUyzEcAKRknZyGYgr6ag==", + "version": "0.3.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.180.tgz", + "integrity": "sha512-8vJ2BN8tot7Qqgl1p0ALXIy/SOvF7nQmh3DEZph6ZARNuV3JUrpF8xdK+lmd25/fm7NFgnGdntLe9k/g2qbAgw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.169.tgz", - "integrity": "sha512-uNkn/31WeQ3qei98p3Z4TC/LNajsI8T/D0vXKWj45VOS5gCnvogTsagNNbdbV9ifEQsPg/bP9oPjWIpW2V0GGQ==", + "version": "0.10.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.180.tgz", + "integrity": "sha512-CGZxW47ZllCqS6n4vVD0lVCz2sq9FqP6czQoCJBStFJho05ioVLy0wQr8XCDJO87wuYvPjJaU3BlsyCDL2xVzQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.169.tgz", - "integrity": "sha512-CdInbfQsP1fFfXHiqsOl6TnpN75LV/BinLXrWfDe3u4yDFKB+1F8uFVqxSGKGoKdjZXn119CQZ+Op2WlsjIEPA==", + "version": "0.11.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.180.tgz", + "integrity": "sha512-YiCxw7P1rj5TVs25BJT9OiZ9QghNtboBhpYB7KZUOB2aqlCPsyIknf9GMbCrZegDfMMa22FWPyXF4oeZDBmpFg==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -133,58 +133,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.169.tgz", - "integrity": "sha512-dPAEWBZ80Mwro4S98xzhyeW9o3hxJow5XkKnUxJBi28BwzmWijLS1eDdnI0jQDQkZ9T8bmvtHPCdypebG6LlgA==", + "version": "0.3.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.180.tgz", + "integrity": "sha512-73vv8C6Fg8CBhZZUNyX286kaNNd3iwjimSqctPZhN1wPUO4wE9mOp52hP6vCi2Vq8aZvbREmVcAvAIzNx0iNNg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.169.tgz", - "integrity": "sha512-omAD9odPBHT7zkM2W8mr6nIY63QNn8mu3iNlo53j+JBf1PBrwXxgOCrXkGYy4CcCX66oTtWnhqaHOQS9Eal8Dw==", + "version": "0.17.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.180.tgz", + "integrity": "sha512-w6dwnHeA9xxmTon2JF6SK1NwfNcmq8LzGPjyzPTwRKleFayHOH88MyIz/HCfgey+AbZRh8gsiA+lvS89/YMcQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.169.tgz", - "integrity": "sha512-PJeXwXORBsdo6VpPgdpMbiwZIhigaV9b9E/vKjUa75dy3qKGrHl/QmptDvOF1JCVjea9loaEvQ0pKHUoYJxzxA==", + "version": "0.15.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.180.tgz", + "integrity": "sha512-+lmgiIXKWMdQvxBM5MfEX4QQ6f4F1jz4VhxnIcfuxUodq5gE+aQHC7gA7zT/LE1R7UJKT0t2byNqFb96lVu0QQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.169.tgz", - "integrity": "sha512-Nf3uCCibOGl1yN+cgzYwEf5iV1hVbBOT/qa2HUl3vJNulyOkBEa+GTQI4fi2zlPrt+/VCtbc3eW2qSxOD5I3sg==", + "version": "0.10.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.180.tgz", + "integrity": "sha512-lStuJCp27ftt7BC7V1poSbPM4J2rluNUybeMn9/D9Kj0bNQYEqMX+cxjYPgVCbgnQIluQAsay8fS2pGiRM4PZA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.168", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.168.tgz", - "integrity": "sha512-5+WenEODiuatzM1/qKkDi9YQ6zM+N+iFM2P/jX4KjXwxsewHdzkExm/TZkbhkyhqlnZxxSeg3cKNxTnPevZhqQ==", + "version": "0.20.0-beta.179", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.179.tgz", + "integrity": "sha512-mYWo305LkZjK+wUt+27+GHXs8NTV5YCzxRY6l9vWaqd+/5V/JdzyZTTR4gA3Vc4CEQETUrhwAVex5arzjWE/IA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.169" + "@xterm/xterm": "^6.1.0-beta.180" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.169", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.169.tgz", - "integrity": "sha512-8/wbTnEDRpyMh3bTC8dhb6g4FXobyAPA/1EAnuPWe+NLkiA8Fo1fjFVbEqe9TXa8KMHXEivI7Hqj1vPDt87A4w==", + "version": "6.1.0-beta.180", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.180.tgz", + "integrity": "sha512-EBF41C3OWwmy5XyWbxiWvgjnb1mXvzwLfFlOvRZPnHA/esXjvwF30aJRvpfI8tPxkNlT05zsx908hLv4XwDuRg==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index 1f0bfdd6760..b71c4b2a597 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -9,15 +9,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.169", - "@xterm/addon-image": "^0.10.0-beta.169", - "@xterm/addon-ligatures": "^0.11.0-beta.169", - "@xterm/addon-progress": "^0.3.0-beta.169", - "@xterm/addon-search": "^0.17.0-beta.169", - "@xterm/addon-serialize": "^0.15.0-beta.169", - "@xterm/addon-unicode11": "^0.10.0-beta.169", - "@xterm/addon-webgl": "^0.20.0-beta.168", - "@xterm/xterm": "^6.1.0-beta.169", + "@xterm/addon-clipboard": "^0.3.0-beta.180", + "@xterm/addon-image": "^0.10.0-beta.180", + "@xterm/addon-ligatures": "^0.11.0-beta.180", + "@xterm/addon-progress": "^0.3.0-beta.180", + "@xterm/addon-search": "^0.17.0-beta.180", + "@xterm/addon-serialize": "^0.15.0-beta.180", + "@xterm/addon-unicode11": "^0.10.0-beta.180", + "@xterm/addon-webgl": "^0.20.0-beta.179", + "@xterm/xterm": "^6.1.0-beta.180", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", From 82ee6ebf02fc3bf5ccdc1dd7ac467ba1913172cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:52:14 -0700 Subject: [PATCH 435/448] Bump the component-explorer group with 2 updates (#300168) Bumps the component-explorer group with 2 updates: [@vscode/component-explorer](https://github.com/microsoft/vscode-packages/tree/HEAD/js-component-explorer/packages/explorer) and [@vscode/component-explorer-cli](https://github.com/microsoft/vscode-packages/tree/HEAD/js-component-explorer/packages/cli). Updates `@vscode/component-explorer` from 0.1.1-19 to 0.1.1-22 - [Commits](https://github.com/microsoft/vscode-packages/commits/HEAD/js-component-explorer/packages/explorer) Updates `@vscode/component-explorer-cli` from 0.1.1-15 to 0.1.1-18 - [Commits](https://github.com/microsoft/vscode-packages/commits/HEAD/js-component-explorer/packages/cli) --- updated-dependencies: - dependency-name: "@vscode/component-explorer" dependency-version: 0.1.1-22 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: component-explorer - dependency-name: "@vscode/component-explorer-cli" dependency-version: 0.1.1-18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: component-explorer ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 1441 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 2 files changed, 1435 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f72b18718f9..46b1b98b935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,8 +84,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260306", - "@vscode/component-explorer": "^0.1.1-19", - "@vscode/component-explorer-cli": "^0.1.1-15", + "@vscode/component-explorer": "^0.1.1-22", + "@vscode/component-explorer-cli": "^0.1.1-18", "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", @@ -170,6 +170,245 @@ "windows-foreground-love": "0.6.1" } }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.2.0.tgz", + "integrity": "sha512-9UAZqn8ywdR70n3GwVle4N8ALosQs4z50N7XMXrSTUVOmVpaBC5kE3TRTT7qQdi3OaQV24mjGuJZsHUmhD+ZXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^1.0.3", + "@octokit/graphql": "^4.3.1", + "@octokit/rest": "^16.43.1" + } + }, + "node_modules/@actions/github/node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@actions/github/node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", + "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.1" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", + "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.1", + "deprecation": "^2.3.1" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@actions/github/node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/rest": { + "version": "16.43.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", + "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^2.4.0", + "@octokit/plugin-paginate-rest": "^1.1.1", + "@octokit/plugin-request-log": "^1.0.0", + "@octokit/plugin-rest-endpoint-methods": "2.4.0", + "@octokit/request": "^5.2.0", + "@octokit/request-error": "^1.0.2", + "atob-lite": "^2.0.0", + "before-after-hook": "^2.0.0", + "btoa-lite": "^1.0.0", + "deprecation": "^2.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", + "octokit-pagination-methods": "^1.1.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@actions/github/node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "os-name": "^3.1.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@actions/github/node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@actions/github/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -774,6 +1013,100 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", @@ -1178,6 +1511,449 @@ "xtend": "~4.0.1" } }, + "node_modules/@hediet/semver": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@hediet/semver/-/semver-0.2.2.tgz", + "integrity": "sha512-sdH+TwXwaYOgnKij3QQbJERl2HkJ+l8idWINwHBI+8nXl1yuTCMerDLDPC48t1wbr849qBTpJTV1EJXlh7OGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.0.4", + "@actions/github": "^2.2.0", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "eslint": "^7.1.0" + } + }, + "node_modules/@hediet/semver/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@hediet/semver/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@hediet/semver/node_modules/@typescript-eslint/eslint-plugin": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^3.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@hediet/semver/node_modules/@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@hediet/semver/node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@hediet/semver/node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@hediet/semver/node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@hediet/semver/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@hediet/semver/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@hediet/semver/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@hediet/semver/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@hediet/semver/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/@hediet/semver/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@hediet/semver/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@hediet/semver/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@hediet/semver/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@hediet/semver/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@hediet/semver/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@hediet/semver/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hediet/semver/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@hediet/semver/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@hediet/semver/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@hediet/semver/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@hediet/semver/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hediet/semver/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -1215,6 +1991,22 @@ "node": ">=18.18.0" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1228,6 +2020,14 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", @@ -2464,6 +3264,13 @@ "@types/estree": "*" } }, + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2802,6 +3609,146 @@ "node": ">= 4" } }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/parser": { "version": "8.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", @@ -3157,23 +4104,25 @@ "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-19", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-19.tgz", - "integrity": "sha512-wvcjw1A8wSH/oR5q+lZrBSyOQZfvXtLPYkQJBj11FBKu35iHko0FTIPMG25Ee+TpT2/BWLd29dWwiJODDQbC8w==", + "version": "0.1.1-22", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-22.tgz", + "integrity": "sha512-T3L8WMHIP7BRNq5E8SHbQ1FINiVpgoWW6tAk95G5vc3yPnAHFoOEHC5kM9eUFJPzbuWO+nYl7gf+7787UBFT4w==", "dev": true, "license": "MIT", "dependencies": { + "@hediet/semver": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@vscode/component-explorer-cli": { - "version": "0.1.1-15", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-15.tgz", - "integrity": "sha512-5unK3ehSezNAGJqN4Nn1CjIjavLY9Rc17buUOC/4SfqyXSFStWN/0JG7S/ESgwqW1I2WruadZis0X0sS33dlFQ==", + "version": "0.1.1-18", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-18.tgz", + "integrity": "sha512-kInOiRaXOVaoI3bxa5yFiT9iYKVkxsufIQR4V7ll2D472nZYssbSWIfMrZtdTlG0SdXRHeufyFVVRTa7jLIHIw==", "dev": true, "license": "MIT", "dependencies": { + "@hediet/semver": "^0.2.2", "@modelcontextprotocol/sdk": "^1.26.0", "clipanion": "^4.0.0-rc.4", "express": "^5.0.0", @@ -5311,6 +6260,16 @@ "node": ">=4" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -5384,6 +6343,13 @@ "node": ">= 4.5.0" } }, + "node_modules/atob-lite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", + "dev": true, + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -5752,6 +6718,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -7539,6 +8512,13 @@ "node": ">= 0.8" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -7615,6 +8595,19 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -7978,6 +8971,30 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -8324,6 +9341,32 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -8547,6 +9590,129 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/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/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -9679,6 +10845,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, "node_modules/geckodriver": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz", @@ -13621,12 +14794,33 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.some": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -13712,6 +14906,19 @@ "es5-ext": "~0.10.2" } }, + "node_modules/macos-release": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -14692,6 +15899,13 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nise": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", @@ -15095,6 +16309,29 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -15317,6 +16554,13 @@ "node": ">=0.10.0" } }, + "node_modules/octokit-pagination-methods": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -15551,6 +16795,20 @@ "node": ">=0.10.0" } }, + "node_modules/os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -15592,6 +16850,16 @@ "node": ">=8" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -16897,6 +18165,19 @@ "node": ">=0.10.0" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -18005,6 +19286,24 @@ "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", "dev": true }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -18752,6 +20051,16 @@ "node": ">=0.10.0" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -18965,6 +20274,69 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -19203,6 +20575,13 @@ "b4a": "^1.6.4" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/textextensions": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", @@ -19583,6 +20962,29 @@ "node": ">=0.6.x" } }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -20055,6 +21457,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-inspect-profiler": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/v8-inspect-profiler/-/v8-inspect-profiler-0.1.1.tgz", @@ -20814,6 +22223,22 @@ "license": "MIT", "optional": true }, + "node_modules/windows-release": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 8576372167d..f934c399373 100644 --- a/package.json +++ b/package.json @@ -154,8 +154,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260306", - "@vscode/component-explorer": "^0.1.1-19", - "@vscode/component-explorer-cli": "^0.1.1-15", + "@vscode/component-explorer": "^0.1.1-22", + "@vscode/component-explorer-cli": "^0.1.1-18", "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", From 13262e38d8ea96ebb2b0fbf370acd876e06cf07e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Mar 2026 14:58:55 -0400 Subject: [PATCH 436/448] fix `cannot read properties of undefined (reading 'getCell')` error that causes terminal benchmarks to fail (#300517) fix error --- .../contrib/terminal/browser/xterm/xtermTerminal.ts | 12 +++++++++--- .../test/browser/chatTerminalCommandMirror.test.ts | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index ea122a15c23..b043728acf3 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -47,6 +47,7 @@ import type { IProgressState } from '@xterm/addon-progress'; import type { CommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js'; import { URI } from '../../../../../base/common/uri.js'; import { isNumber } from '../../../../../base/common/types.js'; +import { clamp } from '../../../../../base/common/numbers.js'; const enum RenderConstants { SmoothScrollDuration = 125 @@ -964,16 +965,21 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.loadAddon(this._serializeAddon); } + const lastLine = this.raw.buffer.active.length - 1; + if (lastLine < 0) { + return ''; + } + const hasValidEndMarker = isNumber(endMarker?.line); - const start = isNumber(startMarker?.line) && startMarker?.line > -1 ? startMarker.line : 0; + const start = clamp(isNumber(startMarker?.line) && startMarker.line > -1 ? startMarker.line : 0, 0, lastLine); let end = hasValidEndMarker ? endMarker.line : this.raw.buffer.active.length - 1; if (skipLastLine && hasValidEndMarker) { end = end - 1; } - end = Math.max(end, start); + end = clamp(Math.max(end, start), start, lastLine); return this._serializeAddon.serialize({ range: { - start: startMarker?.line ?? 0, + start, end } }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts index 3d91b41d178..9a30e722fe2 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts @@ -202,6 +202,17 @@ suite('Workbench - ChatTerminalCommandMirror', () => { strictEqual(mirrorText.includes('before'), false); }); + test('disposed start marker does not throw in VT serialization', async () => { + const source = await createXterm(); + await write(source, 'line 1\r\nline 2'); + + const startMarker = source.raw.registerMarker(0)!; + startMarker.dispose(); + + const vt = await source.getRangeAsVT(startMarker, undefined, true); + strictEqual(typeof vt, 'string'); + }); + test('incremental mirroring appends correctly', async () => { const source = await createXterm(); const marker = source.raw.registerMarker(0)!; From 97c94c2a79ef74288bfdadd7e1d26070c047ecc0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:04:01 +0100 Subject: [PATCH 437/448] Sessions - update context key calculation (#300514) --- src/vs/sessions/contrib/changes/browser/changesView.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 5d49d35ab00..ffb41b25c95 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -750,10 +750,11 @@ export class ChangesViewPane extends ViewPane { })); // Set context key for merge base branch protection - const isMergeBaseBranchProtectedContextKey = new RawContextKey('sessions.isMergeBaseBranchProtected', false); - this.renderDisposables.add(bindContextKey(isMergeBaseBranchProtectedContextKey, this.scopedContextKeyService, r => { - const repository = this.activeSessionRepositoryObs.read(r)?.read(r).value; - return repository?.state.read(r).HEAD?.base?.isProtected === true; + const isMergeBaseBranchProtectedContextKey = scopedContextKeyService.createKey('sessions.isMergeBaseBranchProtected', false); + this.renderDisposables.add(autorun(reader => { + const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader).value; + const state = repository?.state.read(reader); + isMergeBaseBranchProtectedContextKey.set(state?.HEAD?.base?.isProtected === true); })); // Set context key for PR state from session metadata From db2c7eebf049afc5f49451af5692d0977eaba209 Mon Sep 17 00:00:00 2001 From: JeffreyCA Date: Tue, 10 Mar 2026 12:11:37 -0700 Subject: [PATCH 438/448] Update Fig spec for Azure Developer CLI (azd) (#299892) Add azd terminal completions for new extension and core commands --- .../terminal-suggest/src/completions/azd.ts | 645 +++++++++++++++++- 1 file changed, 632 insertions(+), 13 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/azd.ts b/extensions/terminal-suggest/src/completions/azd.ts index 6a4cf535ee7..97c9fc66191 100644 --- a/extensions/terminal-suggest/src/completions/azd.ts +++ b/extensions/terminal-suggest/src/completions/azd.ts @@ -278,6 +278,121 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['monitor'], + description: 'Monitor logs from a hosted agent container.', + options: [ + { + name: ['--account-name', '-a'], + description: 'Cognitive Services account name', + args: [ + { + name: 'account-name', + }, + ], + }, + { + name: ['--follow', '-f'], + description: 'Stream logs in real-time', + }, + { + name: ['--name', '-n'], + description: 'Name of the hosted agent (required)', + args: [ + { + name: 'name', + }, + ], + }, + { + name: ['--project-name', '-p'], + description: 'AI Foundry project name', + args: [ + { + name: 'project-name', + }, + ], + }, + { + name: ['--tail', '-l'], + description: 'Number of trailing log lines to fetch (1-300)', + args: [ + { + name: 'tail', + }, + ], + }, + { + name: ['--type', '-t'], + description: 'Type of logs: \'console\' (stdout/stderr) or \'system\' (container events)', + args: [ + { + name: 'type', + }, + ], + }, + { + name: ['--version', '-v'], + description: 'Version of the hosted agent (required)', + args: [ + { + name: 'version', + }, + ], + }, + ], + }, + { + name: ['show'], + description: 'Show the status of a hosted agent deployment.', + options: [ + { + name: ['--account-name', '-a'], + description: 'Cognitive Services account name', + args: [ + { + name: 'account-name', + }, + ], + }, + { + name: ['--name', '-n'], + description: 'Name of the hosted agent (required)', + args: [ + { + name: 'name', + }, + ], + }, + { + name: ['--output', '-o'], + description: 'Output format (json or table)', + args: [ + { + name: 'output', + }, + ], + }, + { + name: ['--project-name', '-p'], + description: 'AI Foundry project name', + args: [ + { + name: 'project-name', + }, + ], + }, + { + name: ['--version', '-v'], + description: 'Version of the hosted agent (required)', + args: [ + { + name: 'version', + }, + ], + }, + ], + }, { name: ['version'], description: 'Prints the version of the application', @@ -750,6 +865,363 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['models'], + description: 'Extension for managing custom models in Azure AI Foundry. (Preview)', + subcommands: [ + { + name: ['custom'], + description: 'Manage custom models in Azure AI Foundry', + subcommands: [ + { + name: ['create'], + description: 'Upload and register a custom model', + options: [ + { + name: ['--azcopy-path'], + description: 'Path to azcopy binary (auto-detected if not provided)', + args: [ + { + name: 'azcopy-path', + }, + ], + }, + { + name: ['--base-model'], + description: 'Base model identifier (e.g., FW-GPT-OSS-120B or full azureml:// URI)', + args: [ + { + name: 'base-model', + }, + ], + }, + { + name: ['--blob-uri'], + description: 'Already-uploaded blob URI (skips upload, registers directly)', + args: [ + { + name: 'blob-uri', + }, + ], + }, + { + name: ['--description'], + description: 'Model description', + args: [ + { + name: 'description', + }, + ], + }, + { + name: ['--name', '-n'], + description: 'Model name (required)', + args: [ + { + name: 'name', + }, + ], + }, + { + name: ['--project-endpoint', '-e'], + description: 'Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)', + args: [ + { + name: 'project-endpoint', + }, + ], + }, + { + name: ['--publisher'], + description: 'Model publisher ID for catalog info', + args: [ + { + name: 'publisher', + }, + ], + }, + { + name: ['--source'], + description: 'Local path or remote URL to model files', + args: [ + { + name: 'source', + }, + ], + }, + { + name: ['--source-file'], + description: 'Path to a file containing the source URL (useful for URLs with special characters)', + args: [ + { + name: 'source-file', + }, + ], + }, + { + name: ['--subscription', '-s'], + description: 'Azure subscription ID', + args: [ + { + name: 'subscription', + }, + ], + }, + { + name: ['--version'], + description: 'Model version', + args: [ + { + name: 'version', + }, + ], + }, + ], + }, + { + name: ['delete'], + description: 'Delete a custom model', + options: [ + { + name: ['--force', '-f'], + description: 'Skip confirmation prompt', + isDangerous: true, + }, + { + name: ['--name', '-n'], + description: 'Model name (required)', + args: [ + { + name: 'name', + }, + ], + }, + { + name: ['--project-endpoint', '-e'], + description: 'Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)', + args: [ + { + name: 'project-endpoint', + }, + ], + }, + { + name: ['--subscription', '-s'], + description: 'Azure subscription ID', + args: [ + { + name: 'subscription', + }, + ], + }, + { + name: ['--version'], + description: 'Model version', + args: [ + { + name: 'version', + }, + ], + }, + ], + }, + { + name: ['list'], + description: 'List all custom models', + options: [ + { + name: ['--output', '-o'], + description: 'Output format (table, json)', + args: [ + { + name: 'output', + }, + ], + }, + { + name: ['--project-endpoint', '-e'], + description: 'Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)', + args: [ + { + name: 'project-endpoint', + }, + ], + }, + { + name: ['--subscription', '-s'], + description: 'Azure subscription ID', + args: [ + { + name: 'subscription', + }, + ], + }, + ], + }, + { + name: ['show'], + description: 'Show details of a custom model', + options: [ + { + name: ['--name', '-n'], + description: 'Model name (required)', + args: [ + { + name: 'name', + }, + ], + }, + { + name: ['--output', '-o'], + description: 'Output format (table, json)', + args: [ + { + name: 'output', + }, + ], + }, + { + name: ['--project-endpoint', '-e'], + description: 'Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)', + args: [ + { + name: 'project-endpoint', + }, + ], + }, + { + name: ['--subscription', '-s'], + description: 'Azure subscription ID', + args: [ + { + name: 'subscription', + }, + ], + }, + { + name: ['--version'], + description: 'Model version', + args: [ + { + name: 'version', + }, + ], + }, + ], + }, + ], + options: [ + { + name: ['--project-endpoint', '-e'], + description: 'Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)', + args: [ + { + name: 'project-endpoint', + }, + ], + }, + { + name: ['--subscription', '-s'], + description: 'Azure subscription ID', + args: [ + { + name: 'subscription', + }, + ], + }, + ], + }, + { + name: ['init'], + description: 'Initialize a new AI models project. (Preview)', + options: [ + { + name: ['--environment', '-n'], + description: 'The name of the azd environment to use', + args: [ + { + name: 'environment', + }, + ], + }, + { + name: ['--project-endpoint', '-e'], + description: 'Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)', + args: [ + { + name: 'project-endpoint', + }, + ], + }, + { + name: ['--project-resource-id', '-p'], + description: 'ARM resource ID of the Foundry project', + args: [ + { + name: 'project-resource-id', + }, + ], + }, + { + name: ['--subscription', '-s'], + description: 'Azure subscription ID', + args: [ + { + name: 'subscription', + }, + ], + }, + ], + }, + { + name: ['version'], + description: 'Prints the version of the application', + }, + ], + }, + ], + }, + { + name: ['appservice'], + description: 'Extension for managing Azure App Service resources.', + subcommands: [ + { + name: ['swap'], + description: 'Swap deployment slots for an App Service.', + options: [ + { + name: ['--dst'], + description: 'The destination slot name. Use @main for production.', + args: [ + { + name: 'dst', + }, + ], + }, + { + name: ['--service'], + description: 'The name of the service to swap slots for.', + args: [ + { + name: 'service', + }, + ], + }, + { + name: ['--src'], + description: 'The source slot name. Use @main for production.', + args: [ + { + name: 'src', + }, + ], + }, + ], + }, + { + name: ['version'], + description: 'Display the version of the extension.', + }, ], }, { @@ -1001,8 +1473,26 @@ const completionSpec: Fig.Spec = { }, { name: ['demo'], - description: 'This extension provides examples of the AZD extension framework.', + description: 'This extension provides examples of the azd extension framework.', subcommands: [ + { + name: ['ai'], + description: 'Interactive AI model discovery, deployment, and quota demos.', + subcommands: [ + { + name: ['deployment'], + description: 'Select model/version/SKU/capacity and resolve a valid deployment configuration.', + }, + { + name: ['models'], + description: 'Browse available AI models interactively.', + }, + { + name: ['quota'], + description: 'View usage meters and limits for a selected location.', + }, + ], + }, { name: ['colors', 'colours'], description: 'Displays all ASCII colors with their standard and high-intensity variants.', @@ -1013,7 +1503,7 @@ const completionSpec: Fig.Spec = { }, { name: ['context'], - description: 'Get the context of the AZD project & environment.', + description: 'Get the context of the azd project & environment.', }, { name: ['gh-url-parse'], @@ -1226,7 +1716,7 @@ const completionSpec: Fig.Spec = { }, { name: ['--subscription'], - description: 'Name or ID of an Azure subscription to use for the new environment', + description: 'ID of an Azure subscription to use for the new environment', args: [ { name: 'subscription', @@ -1493,6 +1983,19 @@ const completionSpec: Fig.Spec = { name: 'name', }, }, + { + name: ['validate'], + description: 'Validate an extension source\'s registry.json file.', + options: [ + { + name: ['--strict'], + description: 'Enable strict validation (require checksums)', + }, + ], + args: { + name: 'name-or-path-or-url', + }, + }, ], }, { @@ -1683,7 +2186,7 @@ const completionSpec: Fig.Spec = { }, { name: ['--subscription', '-s'], - description: 'Name or ID of an Azure subscription to use for the new environment', + description: 'ID of an Azure subscription to use for the new environment', args: [ { name: 'subscription', @@ -1692,7 +2195,7 @@ const completionSpec: Fig.Spec = { }, { name: ['--template', '-t'], - description: 'Initializes a new application from a template. You can use Full URI, /, or if it\'s part of the azure-samples organization.', + description: 'Initializes a new application from a template. You can use a Full URI, /, if it\'s part of the azure-samples organization, or a local directory path (./dir, ../dir, or absolute path).', args: [ { name: 'template', @@ -2059,6 +2562,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--location', '-l'], + description: 'Azure location for the new environment', + args: [ + { + name: 'location', + }, + ], + }, { name: ['--no-state'], description: '(Bicep only) Forces a fresh deployment based on current Bicep template files, ignoring any stored deployment state.', @@ -2067,6 +2579,15 @@ const completionSpec: Fig.Spec = { name: ['--preview'], description: 'Preview changes to Azure resources.', }, + { + name: ['--subscription'], + description: 'ID of an Azure subscription to use for the new environment', + args: [ + { + name: 'subscription', + }, + ], + }, ], args: { name: 'layer', @@ -2267,6 +2788,24 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--location', '-l'], + description: 'Azure location for the new environment', + args: [ + { + name: 'location', + }, + ], + }, + { + name: ['--subscription'], + description: 'ID of an Azure subscription to use for the new environment', + args: [ + { + name: 'subscription', + }, + ], + }, ], }, { @@ -2275,7 +2814,7 @@ const completionSpec: Fig.Spec = { }, { name: ['x'], - description: 'This extension provides a set of tools for AZD extension developers to test and debug their extensions.', + description: 'This extension provides a set of tools for azd extension developers to test and debug their extensions.', subcommands: [ { name: ['build'], @@ -2302,7 +2841,7 @@ const completionSpec: Fig.Spec = { }, { name: ['init'], - description: 'Initialize a new AZD extension project', + description: 'Initialize a new azd extension project', options: [ { name: ['--capabilities'], @@ -2506,7 +3045,7 @@ const completionSpec: Fig.Spec = { }, { name: ['watch'], - description: 'Watches the AZD extension project for file changes and rebuilds it.', + description: 'Watches the azd extension project for file changes and rebuilds it.', }, ], }, @@ -2530,6 +3069,14 @@ const completionSpec: Fig.Spec = { name: ['init'], description: 'Initialize a new AI agent project. (Preview)', }, + { + name: ['monitor'], + description: 'Monitor logs from a hosted agent container.', + }, + { + name: ['show'], + description: 'Show the status of a hosted agent deployment.', + }, { name: ['version'], description: 'Prints the version of the application', @@ -2584,6 +3131,56 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['models'], + description: 'Extension for managing custom models in Azure AI Foundry. (Preview)', + subcommands: [ + { + name: ['custom'], + description: 'Manage custom models in Azure AI Foundry', + subcommands: [ + { + name: ['create'], + description: 'Upload and register a custom model', + }, + { + name: ['delete'], + description: 'Delete a custom model', + }, + { + name: ['list'], + description: 'List all custom models', + }, + { + name: ['show'], + description: 'Show details of a custom model', + }, + ], + }, + { + name: ['init'], + description: 'Initialize a new AI models project. (Preview)', + }, + { + name: ['version'], + description: 'Prints the version of the application', + }, + ], + }, + ], + }, + { + name: ['appservice'], + description: 'Extension for managing Azure App Service resources.', + subcommands: [ + { + name: ['swap'], + description: 'Swap deployment slots for an App Service.', + }, + { + name: ['version'], + description: 'Display the version of the extension.', + }, ], }, { @@ -2694,8 +3291,26 @@ const completionSpec: Fig.Spec = { }, { name: ['demo'], - description: 'This extension provides examples of the AZD extension framework.', + description: 'This extension provides examples of the azd extension framework.', subcommands: [ + { + name: ['ai'], + description: 'Interactive AI model discovery, deployment, and quota demos.', + subcommands: [ + { + name: ['deployment'], + description: 'Select model/version/SKU/capacity and resolve a valid deployment configuration.', + }, + { + name: ['models'], + description: 'Browse available AI models interactively.', + }, + { + name: ['quota'], + description: 'View usage meters and limits for a selected location.', + }, + ], + }, { name: ['colors', 'colours'], description: 'Displays all ASCII colors with their standard and high-intensity variants.', @@ -2706,7 +3321,7 @@ const completionSpec: Fig.Spec = { }, { name: ['context'], - description: 'Get the context of the AZD project & environment.', + description: 'Get the context of the azd project & environment.', }, { name: ['gh-url-parse'], @@ -2836,6 +3451,10 @@ const completionSpec: Fig.Spec = { name: ['remove'], description: 'Remove an extension source with the specified name', }, + { + name: ['validate'], + description: 'Validate an extension source\'s registry.json file.', + }, ], }, { @@ -2976,7 +3595,7 @@ const completionSpec: Fig.Spec = { }, { name: ['x'], - description: 'This extension provides a set of tools for AZD extension developers to test and debug their extensions.', + description: 'This extension provides a set of tools for azd extension developers to test and debug their extensions.', subcommands: [ { name: ['build'], @@ -2984,7 +3603,7 @@ const completionSpec: Fig.Spec = { }, { name: ['init'], - description: 'Initialize a new AZD extension project', + description: 'Initialize a new azd extension project', }, { name: ['pack'], @@ -3004,7 +3623,7 @@ const completionSpec: Fig.Spec = { }, { name: ['watch'], - description: 'Watches the AZD extension project for file changes and rebuilds it.', + description: 'Watches the azd extension project for file changes and rebuilds it.', }, ], }, From b50d56f99a2d165e2b5b86ea125c6d9be1774022 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Mar 2026 15:19:26 -0400 Subject: [PATCH 439/448] do not send freeform input for autoreply, just return (#300515) --- .../browser/tools/monitoring/outputMonitor.ts | 55 +++-------------- .../test/browser/outputMonitor.test.ts | 61 ++++++++++++++++++- 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index d0af9560728..612a59eb9bc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -243,11 +243,11 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { // Check for generic "press any key" prompts from scripts. if ((!isTask || !isTaskInactive) && detectsGenericPressAnyKeyPattern(output)) { this._logService.trace('OutputMonitor: Idle -> generic "press any key" detected'); - // In autopilot mode, auto-reply to "press any key" prompts - if (this._isAutopilotMode()) { - this._logService.trace('OutputMonitor: Autopilot mode -> auto-replying to "press any key"'); - await this._execution.instance.sendText('', true); - return { shouldContinuePollling: true }; + const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts) || this._isAutopilotMode(); + if (autoReply) { + this._logService.trace('OutputMonitor: Auto-reply enabled -> not showing free-form prompt for "press any key", stopping'); + this._cleanupIdleInputListener(); + return { shouldContinuePollling: false, output }; } this._logService.trace('OutputMonitor: Requesting free-form input for "press any key"'); // Register a marker to track this prompt position so we don't re-detect it @@ -299,16 +299,10 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { return { shouldContinuePollling: true }; } const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts) || this._isAutopilotMode(); - if (autoReply && !this._isSensitivePrompt(confirmationPrompt.prompt)) { - const explicitInput = confirmationPrompt.suggestedInput ?? this._extractExplicitInputFromPrompt(confirmationPrompt.prompt); - const normalizedInput = this._normalizeAutoReplyInput(explicitInput); - if (normalizedInput !== undefined) { - this._logService.trace('OutputMonitor: Auto-replying to free-form prompt'); - await this._execution.instance.sendText(normalizedInput, true); - this._outputMonitorTelemetryCounters.inputToolAutoAcceptCount++; - this._outputMonitorTelemetryCounters.inputToolAutoChars += normalizedInput.length; - return { shouldContinuePollling: true }; - } + if (autoReply) { + this._logService.trace('OutputMonitor: Auto-reply enabled -> not propagating free-form prompt, stopping'); + this._cleanupIdleInputListener(); + return { shouldContinuePollling: false, output }; } // Clean up the input listener now - the prompt will set up its own this._cleanupIdleInputListener(); @@ -621,37 +615,6 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { return request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot; } - private _normalizeAutoReplyInput(input: string | undefined): string | undefined { - if (!input) { - return undefined; - } - const trimmed = input.trim(); - if (!trimmed) { - return undefined; - } - const lowered = trimmed.toLowerCase(); - if (lowered === '\\r' || lowered === '\\n' || lowered === 'enter' || lowered === 'return') { - return ''; - } - return trimmed; - } - - private _extractExplicitInputFromPrompt(prompt: string): string | undefined { - const normalizedPrompt = prompt.trim(); - if (!normalizedPrompt) { - return undefined; - } - const directCommandMatch = normalizedPrompt.match(/\b(?:type|enter|input)\s+["'`]([^"'`]+)["'`]/i); - if (directCommandMatch?.[1]) { - return directCommandMatch[1]; - } - const bareCommandMatch = normalizedPrompt.match(/\b(?:type|enter|input)\s+([\w.-]+)\b/i); - if (bareCommandMatch?.[1]) { - return bareCommandMatch[1]; - } - return undefined; - } - private async _selectAndHandleOption( confirmationPrompt: IConfirmationPrompt | undefined, token: CancellationToken, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts index 5e37377e3df..f4f34ecd4e9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts @@ -31,17 +31,22 @@ suite('OutputMonitor', () => { let cts: CancellationTokenSource; let instantiationService: TestInstantiationService; let sendTextCalled: boolean; + let sentText: string | undefined; let dataEmitter: Emitter; setup(() => { sendTextCalled = false; + sentText = undefined; dataEmitter = new Emitter(); execution = { getOutput: () => 'test output', isActive: async () => false, instance: { instanceId: 1, - sendText: async () => { sendTextCalled = true; }, + sendText: async (text?: string) => { + sendTextCalled = true; + sentText = text; + }, onDidInputData: dataEmitter.event, onDisposed: Event.None, onData: dataEmitter.event, @@ -285,6 +290,60 @@ suite('OutputMonitor', () => { assert.strictEqual(optionResult?.suggestedOption, 'n', 'suggested option should be derived from fallback model response'); }); + test('auto reply stops on generic press any key prompts', async () => { + instantiationService.stub(IConfigurationService, new TestConfigurationService({ + [TerminalChatAgentToolsSettingId.AutoReplyToPrompts]: true + })); + + execution.getOutput = () => 'Press any key to continue...'; + const monitorCts = new CancellationTokenSource(); + monitorCts.cancel(); + monitor = store.add(instantiationService.createInstance(OutputMonitor, execution, undefined, createTestContext('1'), monitorCts.token, 'test command')); + + const outputMonitorWithPrivateMethod = monitor as unknown as { + [key: string]: ((token: CancellationToken) => Promise<{ shouldContinuePollling: boolean }>) | undefined; + }; + const idleResult = await outputMonitorWithPrivateMethod['_handleIdleState']!(CancellationToken.None); + await Event.toPromise(monitor.onDidFinishCommand); + monitorCts.dispose(); + + assert.strictEqual(sendTextCalled, false, 'sendText should not be called when auto reply is enabled for free-form prompts'); + assert.strictEqual(sentText, undefined, 'no terminal input should be sent'); + assert.strictEqual(idleResult.shouldContinuePollling, false, 'monitor should stop polling for free-form prompts in auto reply mode'); + }); + + test('auto reply does not propagate free-form input requests without explicit input', async () => { + instantiationService.stub(IConfigurationService, new TestConfigurationService({ + [TerminalChatAgentToolsSettingId.AutoReplyToPrompts]: true + })); + + const monitorCts = new CancellationTokenSource(); + monitorCts.cancel(); + monitor = store.add(instantiationService.createInstance(OutputMonitor, execution, undefined, createTestContext('1'), monitorCts.token, 'test command')); + + const outputMonitorWithPrivateMethod = monitor as unknown as { + [key: string]: unknown; + }; + let freeFormRequestShown = false; + outputMonitorWithPrivateMethod['_determineUserInputOptions'] = async () => ({ + prompt: 'Password:', + options: [], + detectedRequestForFreeFormInput: true + }); + outputMonitorWithPrivateMethod['_requestFreeFormTerminalInput'] = async () => { + freeFormRequestShown = true; + return true; + }; + + const idleResult = await (outputMonitorWithPrivateMethod['_handleIdleState'] as (token: CancellationToken) => Promise<{ shouldContinuePollling: boolean }>)(CancellationToken.None); + await Event.toPromise(monitor.onDidFinishCommand); + monitorCts.dispose(); + + assert.strictEqual(freeFormRequestShown, false, 'free-form elicitation should not be shown when auto reply is enabled'); + assert.strictEqual(sendTextCalled, false, 'sensitive free-form prompt should not be auto-replied'); + assert.strictEqual(idleResult.shouldContinuePollling, false, 'monitor should stop instead of propagating free-form prompt'); + }); + suite('detectsInputRequiredPattern', () => { test('detects yes/no confirmation prompts (pairs and variants)', () => { assert.strictEqual(detectsInputRequiredPattern('Continue? (y/N) '), true); From cfe3b3286e45f2ddafedea30b9ce840c2f143aa6 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 10 Mar 2026 12:20:43 -0700 Subject: [PATCH 440/448] Update action for the title bar (#300453) --- .../lib/stylelint/vscode-known-variables.json | 1 + .../common/update.config.contribution.ts | 14 + src/vs/platform/update/common/update.ts | 5 +- .../electron-main/abstractUpdateService.ts | 11 +- .../electron-main/updateService.darwin.ts | 6 +- .../electron-main/updateService.linux.ts | 2 +- .../electron-main/updateService.snap.ts | 2 +- .../electron-main/updateService.win32.ts | 2 +- .../browser/media/updateStatusBarEntry.css | 133 ---- .../browser/media/updateTitleBarEntry.css | 90 +++ .../update/browser/media/updateTooltip.css | 115 ++++ .../update/browser/update.contribution.ts | 6 +- .../contrib/update/browser/update.ts | 101 +-- .../update/browser/updateStatusBarEntry.ts | 587 +++--------------- .../update/browser/updateTitleBarEntry.ts | 267 ++++++++ .../contrib/update/browser/updateTooltip.ts | 378 +++++++++++ .../contrib/update/common/updateUtils.ts | 218 +++++++ .../updateUtils.test.ts} | 163 +++-- 18 files changed, 1359 insertions(+), 742 deletions(-) create mode 100644 src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css create mode 100644 src/vs/workbench/contrib/update/browser/media/updateTooltip.css create mode 100644 src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts create mode 100644 src/vs/workbench/contrib/update/browser/updateTooltip.ts create mode 100644 src/vs/workbench/contrib/update/common/updateUtils.ts rename src/vs/workbench/contrib/update/test/{browser/updateStatusBarEntry.test.ts => common/updateUtils.test.ts} (56%) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 29a3d47d232..5b0fc9b6ad5 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -949,6 +949,7 @@ "--testMessageDecorationFontSize", "--title-border-bottom-color", "--title-wco-width", + "--update-progress", "--reveal-button-size", "--part-background", "--part-border-color", diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index 93dd62df97e..76483ca546e 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -89,6 +89,20 @@ configurationRegistry.registerConfiguration({ localize('actionable', "The status bar entry is shown when an action is required (e.g., download, install, or restart)."), localize('detailed', "The status bar entry is shown for all update states including progress.") ] + }, + 'update.titleBar': { + type: 'string', + enum: ['none', 'actionable', 'detailed'], + default: 'none', + scope: ConfigurationScope.APPLICATION, + tags: ['experimental'], + experiment: { mode: 'startup' }, + description: localize('titleBar', "Controls the experimental update title bar entry."), + enumDescriptions: [ + localize('titleBarNone', "The title bar entry is never shown."), + localize('titleBarActionable', "The title bar entry is shown when an action is required (e.g., download, install, or restart)."), + localize('titleBarDetailed', "The title bar entry is shown for all update states including progress.") + ] } } }); diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index b5c2b121c64..bc90a03ad8c 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -59,6 +59,7 @@ export const enum DisablementReason { NotBuilt, DisabledByEnvironment, ManuallyDisabled, + Policy, MissingConfiguration, InvalidConfiguration, RunningAsAdmin, @@ -66,7 +67,7 @@ export const enum DisablementReason { export type Uninitialized = { type: StateType.Uninitialized }; export type Disabled = { type: StateType.Disabled; reason: DisablementReason }; -export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string }; +export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string; notAvailable?: boolean }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean }; export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate; canInstall?: boolean }; export type Downloading = { type: StateType.Downloading; update?: IUpdate; explicit: boolean; overwrite: boolean; downloadedBytes?: number; totalBytes?: number; startTime?: number }; @@ -80,7 +81,7 @@ export type State = Uninitialized | Disabled | Idle | CheckingForUpdates | Avail export const State = { Uninitialized: upcast({ type: StateType.Uninitialized }), Disabled: (reason: DisablementReason): Disabled => ({ type: StateType.Disabled, reason }), - Idle: (updateType: UpdateType, error?: string): Idle => ({ type: StateType.Idle, updateType, error }), + Idle: (updateType: UpdateType, error?: string, notAvailable?: boolean): Idle => ({ type: StateType.Idle, updateType, error, notAvailable }), CheckingForUpdates: (explicit: boolean): CheckingForUpdates => ({ type: StateType.CheckingForUpdates, explicit }), AvailableForDownload: (update: IUpdate, canInstall?: boolean): AvailableForDownload => ({ type: StateType.AvailableForDownload, update, canInstall }), Downloading: (update: IUpdate | undefined, explicit: boolean, overwrite: boolean, downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading => ({ type: StateType.Downloading, update, explicit, overwrite, downloadedBytes, totalBytes, startTime }), diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 06a42fe3485..c207a036712 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -142,11 +142,18 @@ export abstract class AbstractUpdateService implements IUpdateService { } const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode'); + const updateModeInspection = this.configurationService.inspect<'none' | 'manual' | 'start' | 'default'>('update.mode'); + const policyDisablesUpdates = updateModeInspection.policyValue !== undefined && !this.getProductQuality(updateModeInspection.policyValue); const quality = this.getProductQuality(updateMode); if (!quality) { - this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); - this.logService.info('update#ctor - updates are disabled by user preference'); + if (policyDisablesUpdates) { + this.setState(State.Disabled(DisablementReason.Policy)); + this.logService.info('update#ctor - updates are disabled by policy'); + } else { + this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); + this.logService.info('update#ctor - updates are disabled by user preference'); + } return; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 317ae6408bf..a9e46ff937e 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -172,7 +172,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau const update = await asJson(context); if (!update || !update.url || !update.version || !update.productVersion) { this.logService.trace('update#checkForUpdateNoDownload - no update available'); - this.setState(State.Idle(UpdateType.Archive)); + const notAvailable = this.state.type === StateType.CheckingForUpdates && this.state.explicit; + this.setState(State.Idle(UpdateType.Archive, undefined, notAvailable || undefined)); } else { this.logService.trace('update#checkForUpdateNoDownload - update available', { version: update.version, productVersion: update.productVersion }); this.setState(State.AvailableForDownload(update, canInstall)); @@ -211,7 +212,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } - this.setState(State.Idle(UpdateType.Archive)); + const notAvailable = this.state.explicit; + this.setState(State.Idle(UpdateType.Archive, undefined, notAvailable || undefined)); } protected override async doDownloadUpdate(state: AvailableForDownload): Promise { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 3ace29fed5a..4c6c0b76191 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -48,7 +48,7 @@ export class LinuxUpdateService extends AbstractUpdateService { .then(asJson) .then(update => { if (!update || !update.url || !update.version || !update.productVersion) { - this.setState(State.Idle(UpdateType.Archive)); + this.setState(State.Idle(UpdateType.Archive, undefined, explicit || undefined)); } else { this.setState(State.AvailableForDownload(update)); } diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index a68a25e577b..ae2df6ac89d 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -176,7 +176,7 @@ export class SnapUpdateService extends AbstractUpdateService { if (result) { this.setState(State.Ready({ version: 'something' }, false, false)); } else { - this.setState(State.Idle(UpdateType.Snap)); + this.setState(State.Idle(UpdateType.Snap, undefined, undefined)); } }, err => { this.logService.error(err); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 7933d7f675b..905928b8d2e 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -219,7 +219,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this._overwrite = false; this.setState(State.Ready(this.state.update, this.state.explicit, false)); } else { - this.setState(State.Idle(updateType)); + this.setState(State.Idle(updateType, undefined, explicit || undefined)); } return Promise.resolve(null); } diff --git a/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css b/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css index ee233981af0..c6bf5c79915 100644 --- a/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css +++ b/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css @@ -7,136 +7,3 @@ color: var(--vscode-button-background); font-size: 16px; } - -.update-status-tooltip { - display: flex; - flex-direction: column; - padding: 4px 0; - min-width: 310px; - max-width: 410px; -} - -/* Header with title and gear icon */ -.update-status-tooltip .header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.update-status-tooltip .header .title { - font-weight: 600; - font-size: var(--vscode-bodyFontSize); - color: var(--vscode-foreground); - margin-bottom: 0; -} - -.update-status-tooltip .header .monaco-action-bar { - margin-left: auto; -} - -/* Product info section with logo */ -.update-status-tooltip .product-info { - display: flex; - gap: 12px; - margin-bottom: 16px; -} - -.update-status-tooltip .product-logo { - width: 48px; - height: 48px; - border-radius: var(--vscode-cornerRadius-large); - padding: 5px; - flex-shrink: 0; - background-image: url('../../../../browser/media/code-icon.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; -} - -.update-status-tooltip .product-details { - display: flex; - flex-direction: column; - justify-content: center; -} - -.update-status-tooltip .product-name { - font-weight: 600; - color: var(--vscode-foreground); - margin-bottom: 4px; -} - -.update-status-tooltip .product-version, -.update-status-tooltip .product-release-date { - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-bodyFontSize-small); -} - -.update-status-tooltip .release-notes-link { - color: var(--vscode-textLink-foreground); - text-decoration: none; - font-size: var(--vscode-bodyFontSize-small); - cursor: pointer; -} - -.update-status-tooltip .release-notes-link:hover { - color: var(--vscode-textLink-activeForeground); - text-decoration: underline; -} - -/* What's Included section */ -.update-status-tooltip .whats-included .section-title { - font-weight: 600; - color: var(--vscode-foreground); - margin-bottom: 8px; -} - -.update-status-tooltip .whats-included ul { - margin: 0; - padding-left: 16px; - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-bodyFontSize-small); -} - -.update-status-tooltip .whats-included li { - margin-bottom: 2px; -} - -/* Progress bar */ -.update-status-tooltip .progress-container { - margin-bottom: 8px; -} - -.update-status-tooltip .progress-bar { - width: 100%; - height: 4px; - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 30%, transparent); - border-radius: var(--vscode-cornerRadius-small); - overflow: hidden; -} - -.update-status-tooltip .progress-bar .progress-fill { - height: 100%; - background-color: var(--vscode-progressBar-background); - border-radius: var(--vscode-cornerRadius-small); - transition: width 0.3s ease; -} - -.update-status-tooltip .progress-text { - display: flex; - justify-content: space-between; - margin-top: 4px; - font-size: var(--vscode-bodyFontSize-small); - color: var(--vscode-descriptionForeground); -} - -.update-status-tooltip .progress-details { - color: var(--vscode-descriptionForeground); - margin-bottom: 4px; -} - -.update-status-tooltip .speed-info, -.update-status-tooltip .time-remaining { - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-bodyFontSize-small); -} diff --git a/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css b/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css new file mode 100644 index 00000000000..eb3ac37b111 --- /dev/null +++ b/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-action-bar .update-indicator { + display: flex; + align-items: center; + border-radius: var(--vscode-cornerRadius-medium); + white-space: nowrap; + padding: 0px 12px; + height: 24px; + background-color: transparent; + border: 1px solid transparent; +} + +.monaco-action-bar .update-indicator:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.monaco-action-bar .update-indicator .indicator-label { + font-size: var(--vscode-bodyFontSize-small); + position: relative; +} + +/* Prominent state (action required) — primary button style */ +.monaco-action-bar .update-indicator.prominent { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-color: var(--vscode-button-background); +} + +.monaco-action-bar .update-indicator.prominent:hover { + background-color: var(--vscode-button-hoverBackground); + border-color: var(--vscode-button-hoverBackground); +} + +/* Disabled state */ +.monaco-action-bar .update-indicator.update-disabled .indicator-label { + color: var(--vscode-disabledForeground); +} + +/* Progress underline bar (shared base) */ +.monaco-action-bar .update-indicator.progress-indefinite .indicator-label::after, +.monaco-action-bar .update-indicator.progress-percent .indicator-label::after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + height: 2px; + border-radius: 1px; +} + +/* Progress: indefinite — animated shimmer underline */ +.monaco-action-bar .update-indicator.progress-indefinite .indicator-label::after { + width: 100%; + background: linear-gradient( + 90deg, + transparent 0%, + var(--vscode-progressBar-background) 80%, + transparent 100% + ); + background-size: 200% 100%; + animation: update-indicator-shimmer 1.5s ease-in-out infinite; +} + +@keyframes update-indicator-shimmer { + 0% { background-position: 100% 0; } + 100% { background-position: -100% 0; } +} + +/* Progress: percentage — left-to-right fill underline */ +.monaco-action-bar .update-indicator.progress-percent .indicator-label::after { + width: 100%; + background: linear-gradient( + 90deg, + var(--vscode-progressBar-background) var(--update-progress, 0%), + color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent) var(--update-progress, 0%) + ); + transition: background 0.3s ease; +} + +/* Reduced motion */ +.monaco-workbench.monaco-reduce-motion .update-indicator.progress-indefinite .indicator-label::after { + animation: none; +} + +.monaco-workbench.monaco-reduce-motion .update-indicator.progress-percent .indicator-label::after { + transition: none; +} diff --git a/src/vs/workbench/contrib/update/browser/media/updateTooltip.css b/src/vs/workbench/contrib/update/browser/media/updateTooltip.css new file mode 100644 index 00000000000..ab714ea2e0f --- /dev/null +++ b/src/vs/workbench/contrib/update/browser/media/updateTooltip.css @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.update-tooltip { + display: flex; + flex-direction: column; + gap: 12px; + padding: 6px 6px; + min-width: 310px; + max-width: 410px; + color: var(--vscode-descriptionForeground); + font-size: var(--vscode-bodyFontSize-small); +} + +/* Header */ +.update-tooltip .header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.update-tooltip .header .title { + font-weight: 600; + font-size: var(--vscode-bodyFontSize); + color: var(--vscode-foreground); +} + +/* Product info */ +.update-tooltip .product-info { + display: flex; + gap: 12px; +} + +.update-tooltip .product-logo { + width: 48px; + height: 48px; + border-radius: var(--vscode-cornerRadius-large); + padding: 5px; + flex-shrink: 0; + background: url('../../../../browser/media/code-icon.svg') center / contain no-repeat; +} + +.update-tooltip .product-details { + display: flex; + flex-direction: column; + justify-content: center; +} + +.update-tooltip .product-name { + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 4px; +} + +.update-tooltip .release-notes-link { + color: var(--vscode-textLink-foreground); + text-decoration: none; +} + +.update-tooltip .release-notes-link:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; +} + +/* Progress bar */ +.update-tooltip .progress-bar { + height: 4px; + background-color: color-mix(in srgb, var(--vscode-progressBar-background) 30%, transparent); + border-radius: var(--vscode-cornerRadius-small); + overflow: hidden; +} + +.update-tooltip .progress-fill { + height: 100%; + background-color: var(--vscode-progressBar-background); + border-radius: var(--vscode-cornerRadius-small); + transition: width 0.3s ease; +} + +.monaco-workbench.monaco-reduce-motion .update-tooltip .progress-fill { + transition: none; +} + +.update-tooltip .progress-text, +.update-tooltip .download-stats { + display: flex; + justify-content: space-between; +} + +.update-tooltip .progress-text { + margin-top: 4px; +} + +.update-tooltip .state-message { + display: flex; + align-items: flex-start; + font-size: var(--vscode-bodyFontSize); + gap: 4px; +} + +.update-tooltip .state-message-icon.codicon[class*='codicon-'] { + font-size: 16px; + flex-shrink: 0; + margin-top: 2px; +} + +.update-tooltip .state-message-icon.codicon.codicon-warning { + color: var(--vscode-editorWarning-foreground); +} + +.update-tooltip .state-message-icon.codicon.codicon-error { + color: var(--vscode-editorError-foreground); +} diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index 35a6855e8f9..4f19831828b 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -10,7 +10,8 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { MenuId, registerAction2, Action2 } from '../../../../platform/actions/common/actions.js'; import { ProductContribution, UpdateContribution, CONTEXT_UPDATE_STATE, SwitchProductQualityContribution, showReleaseNotesInEditor, DefaultAccountUpdateContribution } from './update.js'; -import { UpdateStatusBarEntryContribution } from './updateStatusBarEntry.js'; +import { UpdateStatusBarContribution } from './updateStatusBarEntry.js'; +import { UpdateTitleBarContribution } from './updateTitleBarEntry.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import product from '../../../../platform/product/common/product.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; @@ -30,7 +31,8 @@ workbench.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Rest workbench.registerWorkbenchContribution(UpdateContribution, LifecyclePhase.Restored); workbench.registerWorkbenchContribution(SwitchProductQualityContribution, LifecyclePhase.Restored); workbench.registerWorkbenchContribution(DefaultAccountUpdateContribution, LifecyclePhase.Eventually); -workbench.registerWorkbenchContribution(UpdateStatusBarEntryContribution, LifecyclePhase.Restored); +workbench.registerWorkbenchContribution(UpdateStatusBarContribution, LifecyclePhase.Restored); +workbench.registerWorkbenchContribution(UpdateTitleBarContribution, LifecyclePhase.Restored); // Release notes diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 68b982cf452..aca3bb3ce27 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -32,6 +32,7 @@ import { Event } from '../../../../base/common/event.js'; import { toAction } from '../../../../base/common/actions.js'; import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; import { getInternalOrg } from '../../../../platform/assignment/common/assignment.js'; +import { IVersion, preprocessError, tryParseVersion } from '../common/updateUtils.js'; export const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Uninitialized); export const MAJOR_MINOR_UPDATE_AVAILABLE = new RawContextKey('majorMinorUpdateAvailable', false); @@ -146,26 +147,6 @@ export function appendUpdateMenuItems(menuId: MenuId, group: string): void { }); } -interface IVersion { - major: number; - minor: number; - patch: number; -} - -function parseVersion(version: string): IVersion | undefined { - const match = /([0-9]+)\.([0-9]+)\.([0-9]+)/.exec(version); - - if (!match) { - return undefined; - } - - return { - major: parseInt(match[1]), - minor: parseInt(match[2]), - patch: parseInt(match[3]) - }; -} - function isMajorMinorUpdate(before: IVersion, after: IVersion): boolean { return before.major < after.major || before.minor < after.minor; } @@ -193,8 +174,12 @@ export class ProductContribution implements IWorkbenchContribution { return; } - const lastVersion = parseVersion(storageService.get(ProductContribution.KEY, StorageScope.APPLICATION, '')); - const currentVersion = parseVersion(productService.version); + if (configurationService.getValue('update.titleBar') !== 'none') { + return; + } + + const lastVersion = tryParseVersion(storageService.get(ProductContribution.KEY, StorageScope.APPLICATION, '')); + const currentVersion = tryParseVersion(productService.version); const shouldShowReleaseNotes = configurationService.getValue('update.showReleaseNotes'); const releaseNotesUrl = productService.releaseNotesUrl; @@ -229,6 +214,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu private overwriteNotificationHandle: INotificationHandle | undefined; private updateStateContextKey: IContextKey; private majorMinorUpdateAvailableContextKey: IContextKey; + private titleBarEnabled: boolean; constructor( @IStorageService private readonly storageService: IStorageService, @@ -268,6 +254,14 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu this.storageService.remove('update/updateNotificationTime', StorageScope.APPLICATION); } + this.titleBarEnabled = this.configurationService.getValue('update.titleBar') !== 'none'; + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('update.titleBar')) { + this.titleBarEnabled = this.configurationService.getValue('update.titleBar') !== 'none'; + this.onUpdateStateChange(this.updateService.state); + } + })); + this.registerGlobalActivityActions(); } @@ -276,7 +270,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu switch (state.type) { case StateType.Disabled: - if (state.reason === DisablementReason.RunningAsAdmin) { + if (!this.titleBarEnabled && state.reason === DisablementReason.RunningAsAdmin) { this.notificationService.notify({ severity: Severity.Info, message: nls.localize('update service disabled', "Updates are disabled because you are running the user-scope installation of {0} as Administrator.", this.productService.nameLong), @@ -317,8 +311,8 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu case StateType.Ready: { const productVersion = state.update.productVersion; if (productVersion) { - const currentVersion = parseVersion(this.productService.version); - const nextVersion = parseVersion(productVersion); + const currentVersion = tryParseVersion(this.productService.version); + const nextVersion = tryParseVersion(productVersion); this.majorMinorUpdateAvailableContextKey.set(Boolean(currentVersion && nextVersion && isMajorMinorUpdate(currentVersion, nextVersion))); } this.onUpdateReady(state); @@ -328,14 +322,16 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu let badge: IBadge | undefined = undefined; - if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { - badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); - } else if (state.type === StateType.CheckingForUpdates) { - badge = new ProgressBadge(() => nls.localize('checkingForUpdates', "Checking for {0} updates...", this.productService.nameShort)); - } else if (state.type === StateType.Downloading || state.type === StateType.Overwriting) { - badge = new ProgressBadge(() => nls.localize('downloading', "Downloading {0} update...", this.productService.nameShort)); - } else if (state.type === StateType.Updating) { - badge = new ProgressBadge(() => nls.localize('updating', "Updating {0}...", this.productService.nameShort)); + if (!this.titleBarEnabled) { + if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { + badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); + } else if (state.type === StateType.CheckingForUpdates) { + badge = new ProgressBadge(() => nls.localize('checkingForUpdates', "Checking for {0} updates...", this.productService.nameShort)); + } else if (state.type === StateType.Downloading || state.type === StateType.Overwriting) { + badge = new ProgressBadge(() => nls.localize('downloading', "Downloading {0} update...", this.productService.nameShort)); + } else if (state.type === StateType.Updating) { + badge = new ProgressBadge(() => nls.localize('updating', "Updating {0}...", this.productService.nameShort)); + } } this.badgeDisposable.clear(); @@ -348,25 +344,34 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu } private onError(error: string): void { - if (/The request timed out|The network connection was lost/i.test(error)) { + if (this.titleBarEnabled) { return; } - error = error.replace(/See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information'); - - this.notificationService.notify({ - severity: Severity.Error, - message: error, - source: nls.localize('update service', "Update Service"), - }); + const processedError = preprocessError(error); + if (processedError) { + this.notificationService.notify({ + severity: Severity.Error, + message: processedError, + source: nls.localize('update service', "Update Service"), + }); + } } private onUpdateNotAvailable(): void { + if (this.titleBarEnabled) { + return; + } + this.dialogService.info(nls.localize('noUpdatesAvailable', "There are currently no updates available.")); } // linux private onUpdateAvailable(update: IUpdate): void { + if (this.titleBarEnabled) { + return; + } + if (!this.shouldShowNotification()) { return; } @@ -397,6 +402,10 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu // windows fast updates private onUpdateDownloaded(update: IUpdate): void { + if (this.titleBarEnabled) { + return; + } + if (isMacintosh) { return; } @@ -434,6 +443,12 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu // windows and mac private onUpdateReady(state: Ready): void { + if (this.titleBarEnabled) { + this.overwriteNotificationHandle?.progress.done(); + this.overwriteNotificationHandle = undefined; + return; + } + if (state.overwrite && this.overwriteNotificationHandle) { const handle = this.overwriteNotificationHandle; this.overwriteNotificationHandle = undefined; @@ -485,6 +500,10 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu // macOS overwrite update - overwriting private onUpdateOverwriting(state: Overwriting): void { + if (this.titleBarEnabled) { + return; + } + if (!state.explicit) { return; } diff --git a/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts b/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts index baf84977f90..4ed3e130eea 100644 --- a/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts +++ b/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts @@ -3,39 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from '../../../../base/browser/dom.js'; -import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { toAction } from '../../../../base/common/actions.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { isWeb } from '../../../../base/common/platform.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import * as nls from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { Command } from '../../../../editor/common/languages.js'; +import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; -import { Downloading, IUpdate, IUpdateService, Overwriting, StateType, State as UpdateState, Updating } from '../../../../platform/update/common/update.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { Downloading, IUpdateService, StateType, State as UpdateState, Updating } from '../../../../platform/update/common/update.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, TooltipContent } from '../../../services/statusbar/browser/statusbar.js'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; +import { computeProgressPercent, formatBytes } from '../common/updateUtils.js'; import './media/updateStatusBarEntry.css'; +import { UpdateTooltip } from './updateTooltip.js'; /** * Displays update status and actions in the status bar. */ -export class UpdateStatusBarEntryContribution extends Disposable implements IWorkbenchContribution { - private static readonly NAME = nls.localize('updateStatus', "Update Status"); - private readonly statusBarEntryAccessor = this._register(new MutableDisposable()); +export class UpdateStatusBarContribution extends Disposable implements IWorkbenchContribution { + private static readonly actionableStates = [StateType.AvailableForDownload, StateType.Downloaded, StateType.Ready]; + private readonly accessor = this._register(new MutableDisposable()); + private readonly tooltip!: UpdateTooltip; private lastStateType: StateType | undefined; constructor( - @IUpdateService private readonly updateService: IUpdateService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, @IStatusbarService private readonly statusbarService: IStatusbarService, - @IProductService private readonly productService: IProductService, - @ICommandService private readonly commandService: ICommandService, - @IHoverService private readonly hoverService: IHoverService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IUpdateService updateService: IUpdateService, ) { super(); @@ -43,126 +37,112 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor return; // Electron only } - this._register(this.updateService.onStateChange(state => this.onUpdateStateChange(state))); + this.tooltip = this._register(instantiationService.createInstance(UpdateTooltip)); + + this._register(updateService.onStateChange(this.onStateChange.bind(this))); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('update.statusBar')) { - this.onUpdateStateChange(this.updateService.state); + if (e.affectsConfiguration('update.statusBar') || e.affectsConfiguration('update.titleBar')) { + this.onStateChange(updateService.state); } })); - this.onUpdateStateChange(this.updateService.state); + + this.onStateChange(updateService.state); } - private onUpdateStateChange(state: UpdateState) { + private onStateChange(state: UpdateState) { + const titleBarMode = this.configurationService.getValue('update.titleBar'); + if (titleBarMode !== 'none') { + this.accessor.clear(); + return; + } + + const mode = this.configurationService.getValue('update.statusBar'); + if (mode === 'hidden' || mode === 'actionable' && !UpdateStatusBarContribution.actionableStates.includes(state.type)) { + this.accessor.clear(); + return; + } + if (this.lastStateType !== state.type) { - this.statusBarEntryAccessor.clear(); + this.accessor.clear(); this.lastStateType = state.type; } - const statusBarMode = this.configurationService.getValue('update.statusBar'); - - if (statusBarMode === 'hidden') { - this.statusBarEntryAccessor.clear(); - return; - } - - const actionRequiredStates = [ - StateType.AvailableForDownload, - StateType.Downloaded, - StateType.Ready - ]; - - // In 'actionable' mode, only show for states that require user action - if (statusBarMode === 'actionable' && !actionRequiredStates.includes(state.type)) { - this.statusBarEntryAccessor.clear(); - return; - } - switch (state.type) { - case StateType.Uninitialized: - case StateType.Idle: - case StateType.Disabled: - this.statusBarEntryAccessor.clear(); - break; - case StateType.CheckingForUpdates: - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.checkingForUpdates', "$(sync~spin) Checking for updates..."), - ariaLabel: nls.localize('updateStatus.checkingForUpdatesAria', "Checking for updates"), - tooltip: this.getCheckingTooltip(), - command: ShowTooltipCommand, - }); + this.updateEntry( + localize('updateStatus.checkingForUpdates', "$(loading~spin) Checking for updates..."), + localize('updateStatus.checkingForUpdatesAria', "Checking for updates"), + ShowTooltipCommand, + ); break; case StateType.AvailableForDownload: - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.updateAvailableStatus', "$(circle-filled) Update available, click to download."), - ariaLabel: nls.localize('updateStatus.updateAvailableAria', "Update available, click to download."), - tooltip: this.getAvailableTooltip(state.update), - command: 'update.downloadNow' - }); + this.updateEntry( + localize('updateStatus.updateAvailableStatus', "$(circle-filled) Update available, click to download."), + localize('updateStatus.updateAvailableAria', "Update available, click to download."), + 'update.downloadNow' + ); break; case StateType.Downloading: - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: this.getDownloadingText(state), - ariaLabel: nls.localize('updateStatus.downloadingUpdateAria', "Downloading update"), - tooltip: this.getDownloadingTooltip(state), - command: ShowTooltipCommand - }); + this.updateEntry( + this.getDownloadingText(state), + localize('updateStatus.downloadingUpdateAria', "Downloading update"), + ShowTooltipCommand + ); break; case StateType.Downloaded: - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.updateReadyStatus', "$(circle-filled) Update downloaded, click to install."), - ariaLabel: nls.localize('updateStatus.updateReadyAria', "Update downloaded, click to install."), - tooltip: this.getReadyToInstallTooltip(state.update), - command: 'update.install' - }); + this.updateEntry( + localize('updateStatus.updateReadyStatus', "$(circle-filled) Update downloaded, click to install."), + localize('updateStatus.updateReadyAria', "Update downloaded, click to install."), + 'update.install' + ); break; case StateType.Updating: - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: this.getUpdatingText(state), - ariaLabel: this.getUpdatingText(state), - tooltip: this.getUpdatingTooltip(state), - command: ShowTooltipCommand - }); + this.updateEntry( + this.getUpdatingText(state), + undefined, + ShowTooltipCommand + ); break; - case StateType.Ready: { - - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.restartToUpdateStatus', "$(circle-filled) Update is ready, click to restart."), - ariaLabel: nls.localize('updateStatus.restartToUpdateAria', "Update is ready, click to restart."), - tooltip: this.getRestartToUpdateTooltip(state.update), - command: 'update.restart' - }); + case StateType.Ready: + this.updateEntry( + localize('updateStatus.restartToUpdateStatus', "$(circle-filled) Update is ready, click to restart."), + localize('updateStatus.restartToUpdateAria', "Update is ready, click to restart."), + 'update.restart' + ); break; - } case StateType.Overwriting: - this.updateStatusBarEntry({ - name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.downloadingNewerUpdateStatus', "$(sync~spin) Downloading update..."), - ariaLabel: nls.localize('updateStatus.downloadingNewerUpdateAria', "Downloading a newer update"), - tooltip: this.getOverwritingTooltip(state), - command: ShowTooltipCommand - }); + this.updateEntry( + localize('updateStatus.downloadingNewerUpdateStatus', "$(loading~spin) Downloading update..."), + localize('updateStatus.downloadingNewerUpdateAria', "Downloading a newer update"), + ShowTooltipCommand + ); + break; + + default: + this.accessor.clear(); break; } } - private updateStatusBarEntry(entry: IStatusbarEntry) { - if (this.statusBarEntryAccessor.value) { - this.statusBarEntryAccessor.value.update(entry); + private updateEntry(text: string, ariaLabel: string | undefined, command: string | Command) { + const entry: IStatusbarEntry = { + text, + ariaLabel: ariaLabel ?? text, + name: localize('updateStatus', "Update Status"), + tooltip: this.tooltip?.domNode, + command + }; + + if (this.accessor.value) { + this.accessor.value.update(entry); } else { - this.statusBarEntryAccessor.value = this.statusbarService.addEntry( + this.accessor.value = this.statusbarService.addEntry( entry, 'status.update', StatusbarAlignment.LEFT, @@ -171,401 +151,24 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor } } - private getCheckingTooltip(): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.checkingForUpdatesTitle', "Checking for Updates"), store); - this.appendProductInfo(container); - - const message = dom.append(container, dom.$('.progress-details')); - message.textContent = nls.localize('updateStatus.checkingPleaseWait', "Checking for updates, please wait..."); - - return container; - } - }; - } - - private getAvailableTooltip(update: IUpdate): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.updateAvailableTitle', "Update Available"), store); - this.appendProductInfo(container, update); - this.appendWhatsIncluded(container); - - return container; - } - }; - } - private getDownloadingText({ downloadedBytes, totalBytes }: Downloading): string { if (downloadedBytes !== undefined && totalBytes !== undefined && totalBytes > 0) { - return nls.localize('updateStatus.downloadUpdateProgressStatus', "$(sync~spin) Downloading update: {0} / {1} • {2}%", + const percent = computeProgressPercent(downloadedBytes, totalBytes) ?? 0; + return localize('updateStatus.downloadUpdateProgressStatus', "$(loading~spin) Downloading update: {0} / {1} • {2}%", formatBytes(downloadedBytes), formatBytes(totalBytes), - getProgressPercent(downloadedBytes, totalBytes) ?? 0); + percent); } else { - return nls.localize('updateStatus.downloadUpdateStatus', "$(sync~spin) Downloading update..."); + return localize('updateStatus.downloadUpdateStatus', "$(loading~spin) Downloading update..."); } } - private getDownloadingTooltip(state: Downloading): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.downloadingUpdateTitle', "Downloading Update"), store); - this.appendProductInfo(container, state.update); - - const { downloadedBytes, totalBytes } = state; - if (downloadedBytes !== undefined && totalBytes !== undefined && totalBytes > 0) { - const percentage = getProgressPercent(downloadedBytes, totalBytes) ?? 0; - - const progressContainer = dom.append(container, dom.$('.progress-container')); - const progressBar = dom.append(progressContainer, dom.$('.progress-bar')); - const progressFill = dom.append(progressBar, dom.$('.progress-fill')); - progressFill.style.width = `${percentage}%`; - - const progressText = dom.append(progressContainer, dom.$('.progress-text')); - const percentageSpan = dom.append(progressText, dom.$('span')); - percentageSpan.textContent = `${percentage}%`; - - const sizeSpan = dom.append(progressText, dom.$('span')); - sizeSpan.textContent = `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}`; - - const speed = computeDownloadSpeed(state); - if (speed !== undefined && speed > 0) { - const speedInfo = dom.append(container, dom.$('.speed-info')); - speedInfo.textContent = nls.localize('updateStatus.downloadSpeed', '{0}/s', formatBytes(speed)); - } - - const timeRemaining = computeDownloadTimeRemaining(state); - if (timeRemaining !== undefined && timeRemaining > 0) { - const timeRemainingNode = dom.append(container, dom.$('.time-remaining')); - timeRemainingNode.textContent = `~${formatTimeRemaining(timeRemaining)} ${nls.localize('updateStatus.timeRemaining', "remaining")}`; - } - } else { - const message = dom.append(container, dom.$('.progress-details')); - message.textContent = nls.localize('updateStatus.downloadingPleaseWait', "Downloading, please wait..."); - } - - return container; - } - }; - } - - private getReadyToInstallTooltip(update: IUpdate): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.updateReadyTitle', "Update is Ready to Install"), store); - this.appendProductInfo(container, update); - this.appendWhatsIncluded(container); - - return container; - } - }; - } - - private getRestartToUpdateTooltip(update: IUpdate): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.updateInstalledTitle', "Update Installed"), store); - this.appendProductInfo(container, update); - this.appendWhatsIncluded(container); - - return container; - } - }; - } - private getUpdatingText({ currentProgress, maxProgress }: Updating): string { - const percentage = getProgressPercent(currentProgress, maxProgress); + const percentage = computeProgressPercent(currentProgress, maxProgress); if (percentage !== undefined) { - return nls.localize('updateStatus.installingUpdateProgressStatus', "$(sync~spin) Installing update: {0}%", percentage); + return localize('updateStatus.installingUpdateProgressStatus', "$(loading~spin) Installing update: {0}%", percentage); } else { - return nls.localize('updateStatus.installingUpdateStatus', "$(sync~spin) Installing update..."); + return localize('updateStatus.installingUpdateStatus', "$(loading~spin) Installing update..."); } } - - private getUpdatingTooltip(state: Updating): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.installingUpdateTitle', "Installing Update"), store); - this.appendProductInfo(container, state.update); - - const { currentProgress, maxProgress } = state; - const percentage = getProgressPercent(currentProgress, maxProgress); - if (percentage !== undefined) { - const progressContainer = dom.append(container, dom.$('.progress-container')); - const progressBar = dom.append(progressContainer, dom.$('.progress-bar')); - const progressFill = dom.append(progressBar, dom.$('.progress-fill')); - progressFill.style.width = `${percentage}%`; - - const progressText = dom.append(progressContainer, dom.$('.progress-text')); - const percentageSpan = dom.append(progressText, dom.$('span')); - percentageSpan.textContent = `${percentage}%`; - } else { - const message = dom.append(container, dom.$('.progress-details')); - message.textContent = nls.localize('updateStatus.installingPleaseWait', "Installing update, please wait..."); - } - - return container; - } - }; - } - - private getOverwritingTooltip(state: Overwriting): TooltipContent { - return { - element: (token: CancellationToken) => { - const store = this.createTooltipDisposableStore(token); - const container = dom.$('.update-status-tooltip'); - - this.appendHeader(container, nls.localize('updateStatus.downloadingNewerUpdateTitle', "Downloading Newer Update"), store); - this.appendProductInfo(container, state.update); - - const message = dom.append(container, dom.$('.progress-details')); - message.textContent = nls.localize('updateStatus.downloadingNewerPleaseWait', "A newer update was released. Downloading, please wait..."); - - return container; - } - }; - } - - private createTooltipDisposableStore(token: CancellationToken): DisposableStore { - const store = new DisposableStore(); - store.add(token.onCancellationRequested(() => store.dispose())); - return store; - } - - private runCommandAndClose(command: string, ...args: unknown[]): void { - this.commandService.executeCommand(command, ...args); - this.hoverService.hideHover(true); - } - - private appendHeader(container: HTMLElement, title: string, store: DisposableStore) { - const header = dom.append(container, dom.$('.header')); - const text = dom.append(header, dom.$('.title')); - text.textContent = title; - - const actionBar = store.add(new ActionBar(header, { hoverDelegate: nativeHoverDelegate })); - actionBar.push([toAction({ - id: 'update.openSettings', - label: nls.localize('updateStatus.settingsTooltip', "Update Settings"), - class: ThemeIcon.asClassName(Codicon.gear), - run: () => this.runCommandAndClose('workbench.action.openSettings', '@id:update*'), - })], { icon: true, label: false }); - } - - private appendProductInfo(container: HTMLElement, update?: IUpdate) { - const productInfo = dom.append(container, dom.$('.product-info')); - - const logoContainer = dom.append(productInfo, dom.$('.product-logo')); - logoContainer.setAttribute('role', 'img'); - logoContainer.setAttribute('aria-label', this.productService.nameLong); - - const details = dom.append(productInfo, dom.$('.product-details')); - - const productName = dom.append(details, dom.$('.product-name')); - productName.textContent = this.productService.nameLong; - - const productVersion = this.productService.version; - if (productVersion) { - const currentVersion = dom.append(details, dom.$('.product-version')); - const currentCommitId = this.productService.commit?.substring(0, 7); - currentVersion.textContent = currentCommitId - ? nls.localize('updateStatus.currentVersionLabelWithCommit', "Current Version: {0} ({1})", productVersion, currentCommitId) - : nls.localize('updateStatus.currentVersionLabel', "Current Version: {0}", productVersion); - } - - const version = update?.productVersion; - if (version) { - const latestVersion = dom.append(details, dom.$('.product-version')); - const updateCommitId = update.version?.substring(0, 7); - latestVersion.textContent = updateCommitId - ? nls.localize('updateStatus.latestVersionLabelWithCommit', "Latest Version: {0} ({1})", version, updateCommitId) - : nls.localize('updateStatus.latestVersionLabel', "Latest Version: {0}", version); - } - - const releaseDate = update?.timestamp ?? tryParseDate(this.productService.date); - if (typeof releaseDate === 'number' && releaseDate > 0) { - const releaseDateNode = dom.append(details, dom.$('.product-release-date')); - releaseDateNode.textContent = nls.localize('updateStatus.releasedLabel', "Released {0}", formatDate(releaseDate)); - } - - const releaseNotesVersion = version ?? productVersion; - if (releaseNotesVersion) { - const link = dom.append(details, dom.$('a.release-notes-link')) as HTMLAnchorElement; - link.textContent = nls.localize('updateStatus.releaseNotesLink', "Release Notes"); - link.href = '#'; - link.addEventListener('click', (e) => { - e.preventDefault(); - this.runCommandAndClose('update.showCurrentReleaseNotes', releaseNotesVersion); - }); - } - } - - private appendWhatsIncluded(container: HTMLElement) { - /* - const whatsIncluded = dom.append(container, dom.$('.whats-included')); - - const sectionTitle = dom.append(whatsIncluded, dom.$('.section-title')); - sectionTitle.textContent = nls.localize('updateStatus.whatsIncludedTitle', "What's Included"); - - const list = dom.append(whatsIncluded, dom.$('ul')); - - const items = [ - nls.localize('updateStatus.featureItem', "New features and functionality"), - nls.localize('updateStatus.bugFixesItem', "Bug fixes and improvements"), - nls.localize('updateStatus.securityItem', "Security fixes and enhancements") - ]; - - for (const item of items) { - const li = dom.append(list, dom.$('li')); - li.textContent = item; - } - */ - } -} - -/** - * Returns the progress percentage based on the current and maximum progress values. - */ -export function getProgressPercent(current: number | undefined, max: number | undefined): number | undefined { - if (current === undefined || max === undefined || max <= 0) { - return undefined; - } else { - return Math.max(Math.min(Math.round((current / max) * 100), 100), 0); - } -} - -/** - * Tries to parse a date string and returns the timestamp or undefined if parsing fails. - */ -export function tryParseDate(date: string | undefined): number | undefined { - if (date === undefined) { - return undefined; - } - const parsed = Date.parse(date); - return isNaN(parsed) ? undefined : parsed; -} - -/** - * Formats a timestamp as a localized date string. - */ -export function formatDate(timestamp: number): string { - return new Date(timestamp).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric' - }); -} - -/** - * Computes an estimate of remaining download time in seconds. - */ -export function computeDownloadTimeRemaining(state: Downloading): number | undefined { - const { downloadedBytes, totalBytes, startTime } = state; - if (downloadedBytes === undefined || totalBytes === undefined || startTime === undefined) { - return undefined; - } - - const elapsedMs = Date.now() - startTime; - if (downloadedBytes <= 0 || totalBytes <= 0 || elapsedMs <= 0) { - return undefined; - } - - const remainingBytes = totalBytes - downloadedBytes; - if (remainingBytes <= 0) { - return 0; - } - - const bytesPerMs = downloadedBytes / elapsedMs; - if (bytesPerMs <= 0) { - return undefined; - } - - const remainingMs = remainingBytes / bytesPerMs; - return Math.ceil(remainingMs / 1000); -} - -/** - * Formats the time remaining as a human-readable string. - */ -export function formatTimeRemaining(seconds: number): string { - const hours = seconds / 3600; - if (hours >= 1) { - const formattedHours = formatDecimal(hours); - return formattedHours === '1' - ? nls.localize('timeRemainingHour', "{0} hour", formattedHours) - : nls.localize('timeRemainingHours', "{0} hours", formattedHours); - } - - const minutes = Math.floor(seconds / 60); - if (minutes >= 1) { - return nls.localize('timeRemainingMinutes', "{0} min", minutes); - } - - return nls.localize('timeRemainingSeconds', "{0}s", seconds); -} - -/** - * Formats a byte count as a human-readable string. - */ -export function formatBytes(bytes: number): string { - if (bytes < 1024) { - return nls.localize('bytes', "{0} B", bytes); - } - - const kb = bytes / 1024; - if (kb < 1024) { - return nls.localize('kilobytes', "{0} KB", formatDecimal(kb)); - } - - const mb = kb / 1024; - if (mb < 1024) { - return nls.localize('megabytes', "{0} MB", formatDecimal(mb)); - } - - const gb = mb / 1024; - return nls.localize('gigabytes', "{0} GB", formatDecimal(gb)); -} - -/** - * Formats a number to 1 decimal place, omitting ".0" for whole numbers. - */ -function formatDecimal(value: number): string { - const rounded = Math.round(value * 10) / 10; - return rounded % 1 === 0 ? rounded.toString() : rounded.toFixed(1); -} - -/** - * Computes the current download speed in bytes per second. - */ -export function computeDownloadSpeed(state: Downloading): number | undefined { - const { downloadedBytes, startTime } = state; - if (downloadedBytes === undefined || startTime === undefined) { - return undefined; - } - - const elapsedMs = Date.now() - startTime; - if (elapsedMs <= 0 || downloadedBytes <= 0) { - return undefined; - } - - return (downloadedBytes / elapsedMs) * 1000; } diff --git a/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts b/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts new file mode 100644 index 00000000000..93be7e36170 --- /dev/null +++ b/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { IManagedHoverContent } from '../../../../base/browser/ui/hover/hover.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { isWeb } from '../../../../base/common/platform.js'; +import { localize } from '../../../../nls.js'; +import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { DisablementReason, IUpdateService, State, StateType } from '../../../../platform/update/common/update.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { computeProgressPercent, tryParseVersion } from '../common/updateUtils.js'; +import './media/updateTitleBarEntry.css'; +import { UpdateTooltip } from './updateTooltip.js'; + +const UPDATE_TITLE_BAR_ACTION_ID = 'workbench.actions.updateIndicator'; +const UPDATE_TITLE_BAR_CONTEXT = new RawContextKey('updateTitleBar', false); +const LAST_KNOWN_VERSION_KEY = 'updateTitleBar/lastKnownVersion'; +const ACTIONABLE_STATES: readonly StateType[] = [StateType.AvailableForDownload, StateType.Downloaded, StateType.Ready]; + +registerAction2(class UpdateIndicatorTitleBarAction extends Action2 { + constructor() { + super({ + id: UPDATE_TITLE_BAR_ACTION_ID, + title: localize('updateIndicatorTitleBarAction', 'Update'), + f1: false, + menu: [{ + id: MenuId.CommandCenter, + order: 10003, + when: UPDATE_TITLE_BAR_CONTEXT, + }] + }); + } + + override async run() { } +}); + +/** + * Displays update status and actions in the title bar. + */ +export class UpdateTitleBarContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @IProductService private readonly productService: IProductService, + @IStorageService private readonly storageService: IStorageService, + @IUpdateService updateService: IUpdateService, + ) { + super(); + + if (isWeb) { + return; // Electron only + } + + const context = UPDATE_TITLE_BAR_CONTEXT.bindTo(contextKeyService); + + const updateContext = () => { + const mode = configurationService.getValue('update.titleBar'); + const state = updateService.state.type; + context.set(mode === 'detailed' || mode === 'actionable' && ACTIONABLE_STATES.includes(state)); + }; + + let entry: UpdateTitleBarEntry | undefined; + let showTooltipOnRender = false; + + this._register(actionViewItemService.register( + MenuId.CommandCenter, + UPDATE_TITLE_BAR_ACTION_ID, + (action, options) => { + entry = instantiationService.createInstance(UpdateTitleBarEntry, action, options, updateContext, showTooltipOnRender); + showTooltipOnRender = false; + return entry; + } + )); + + const onStateChange = () => { + if (this.shouldShowTooltip(updateService.state)) { + if (context.get()) { + entry?.showTooltip(); + } else { + context.set(true); + showTooltipOnRender = true; + } + } else { + updateContext(); + } + }; + + this._register(updateService.onStateChange(onStateChange)); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('update.titleBar')) { + updateContext(); + } + })); + + onStateChange(); + } + + private shouldShowTooltip(state: State): boolean { + switch (state.type) { + case StateType.Disabled: + return state.reason === DisablementReason.InvalidConfiguration || state.reason === DisablementReason.RunningAsAdmin; + case StateType.Idle: + return !!state.error || state.notAvailable || this.isMajorMinorVersionChange(); + case StateType.AvailableForDownload: + case StateType.Downloaded: + case StateType.Ready: + return true; + default: + return false; + } + } + + private isMajorMinorVersionChange(): boolean { + const currentVersion = this.productService.version; + const lastKnownVersion = this.storageService.get(LAST_KNOWN_VERSION_KEY, StorageScope.APPLICATION); + this.storageService.store(LAST_KNOWN_VERSION_KEY, currentVersion, StorageScope.APPLICATION, StorageTarget.MACHINE); + if (!lastKnownVersion) { + return false; + } + + const current = tryParseVersion(currentVersion); + const last = tryParseVersion(lastKnownVersion); + if (!current || !last) { + return false; + } + + return current.major !== last.major || current.minor !== last.minor; + } +} + +/** + * Custom action view item for the update indicator in the title bar. + */ +export class UpdateTitleBarEntry extends BaseActionViewItem { + private content: HTMLElement | undefined; + private readonly tooltip: UpdateTooltip; + + constructor( + action: IAction, + options: IBaseActionViewItemOptions, + private readonly onDisposeTooltip: () => void, + private showTooltipOnRender: boolean, + @ICommandService private readonly commandService: ICommandService, + @IHoverService private readonly hoverService: IHoverService, + @IInstantiationService instantiationService: IInstantiationService, + @IUpdateService private readonly updateService: IUpdateService, + ) { + super(undefined, action, options); + + this.action.run = () => this.runAction(); + this.tooltip = this._register(instantiationService.createInstance(UpdateTooltip)); + + this._register(this.updateService.onStateChange(state => this.updateContent(state))); + } + + public override render(container: HTMLElement) { + super.render(container); + + this.content = dom.append(container, dom.$('.update-indicator')); + this.updateTooltip(); + this.updateContent(this.updateService.state); + + if (this.showTooltipOnRender) { + this.showTooltipOnRender = false; + dom.scheduleAtNextAnimationFrame(dom.getWindow(container), () => this.showTooltip()); + } + } + + protected override getHoverContents(): IManagedHoverContent { + return this.tooltip.domNode; + } + + private runAction() { + switch (this.updateService.state.type) { + case StateType.AvailableForDownload: + this.commandService.executeCommand('update.downloadNow'); + break; + case StateType.Downloaded: + this.commandService.executeCommand('update.install'); + break; + case StateType.Ready: + this.commandService.executeCommand('update.restart'); + break; + default: + this.showTooltip(); + break; + } + } + + public showTooltip() { + if (!this.content?.isConnected) { + return; + } + + this.hoverService.showInstantHover({ + content: this.tooltip.domNode, + target: { + targetElements: [this.content], + dispose: () => this.onDisposeTooltip(), + }, + persistence: { sticky: true }, + appearance: { showPointer: true }, + }, true); + } + + private updateContent(state: State) { + if (!this.content) { + return; + } + + dom.clearNode(this.content); + this.content.classList.remove('prominent', 'progress-indefinite', 'progress-percent', 'update-disabled'); + this.content.style.removeProperty('--update-progress'); + + const label = dom.append(this.content, dom.$('.indicator-label')); + label.textContent = localize('updateIndicator.update', "Update"); + + switch (state.type) { + case StateType.Disabled: + this.content.classList.add('update-disabled'); + break; + + case StateType.CheckingForUpdates: + case StateType.Overwriting: + this.renderProgressState(this.content); + break; + + case StateType.AvailableForDownload: + case StateType.Downloaded: + case StateType.Ready: + this.content.classList.add('prominent'); + break; + + case StateType.Downloading: + this.renderProgressState(this.content, computeProgressPercent(state.downloadedBytes, state.totalBytes)); + break; + + case StateType.Updating: + this.renderProgressState(this.content, computeProgressPercent(state.currentProgress, state.maxProgress)); + break; + } + } + + private renderProgressState(content: HTMLElement, percentage?: number) { + if (percentage !== undefined) { + content.classList.add('progress-percent'); + content.style.setProperty('--update-progress', `${percentage}%`); + } else { + content.classList.add('progress-indefinite'); + } + } +} diff --git a/src/vs/workbench/contrib/update/browser/updateTooltip.ts b/src/vs/workbench/contrib/update/browser/updateTooltip.ts new file mode 100644 index 00000000000..ee3c452c3be --- /dev/null +++ b/src/vs/workbench/contrib/update/browser/updateTooltip.ts @@ -0,0 +1,378 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { toAction } from '../../../../base/common/actions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js'; +import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { AvailableForDownload, Disabled, DisablementReason, Downloaded, Downloading, Idle, IUpdate, IUpdateService, Overwriting, Ready, State, StateType, Updating } from '../../../../platform/update/common/update.js'; +import { computeDownloadSpeed, computeDownloadTimeRemaining, computeProgressPercent, formatBytes, formatDate, formatTimeRemaining, tryParseDate } from '../common/updateUtils.js'; +import './media/updateTooltip.css'; + +/** + * A stateful tooltip control for the update status. + */ +export class UpdateTooltip extends Disposable { + public readonly domNode: HTMLElement; + + // Header section + private readonly titleNode: HTMLElement; + + // Product info section + private readonly productNameNode: HTMLElement; + private readonly currentVersionNode: HTMLElement; + private readonly latestVersionNode: HTMLElement; + private readonly releaseDateNode: HTMLElement; + private readonly releaseNotesLink: HTMLAnchorElement; + + // Progress section + private readonly progressContainer: HTMLElement; + private readonly progressFill: HTMLElement; + private readonly progressPercentNode: HTMLElement; + private readonly progressSizeNode: HTMLElement; + + // Extra download info + private readonly downloadStatsContainer: HTMLElement; + private readonly timeRemainingNode: HTMLElement; + private readonly speedInfoNode: HTMLElement; + + // State-specific message + private readonly messageNode: HTMLElement; + + private releaseNotesVersion: string | undefined; + + constructor( + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IHoverService private readonly hoverService: IHoverService, + @IMeteredConnectionService private readonly meteredConnectionService: IMeteredConnectionService, + @IProductService private readonly productService: IProductService, + @IUpdateService updateService: IUpdateService, + ) { + super(); + + this.domNode = dom.$('.update-tooltip'); + + // Header section + const header = dom.append(this.domNode, dom.$('.header')); + this.titleNode = dom.append(header, dom.$('.title')); + + const actionBar = this._register(new ActionBar(header, { hoverDelegate: nativeHoverDelegate })); + actionBar.push(toAction({ + id: 'update.openSettings', + label: localize('updateTooltip.settingsTooltip', "Update Settings"), + class: ThemeIcon.asClassName(Codicon.gear), + run: () => this.runCommandAndClose('workbench.action.openSettings', '@id:update*'), + }), { icon: true, label: false }); + + // Product info section + const productInfo = dom.append(this.domNode, dom.$('.product-info')); + + const logoContainer = dom.append(productInfo, dom.$('.product-logo')); + logoContainer.setAttribute('role', 'img'); + logoContainer.setAttribute('aria-label', this.productService.nameLong); + + const details = dom.append(productInfo, dom.$('.product-details')); + + this.productNameNode = dom.append(details, dom.$('.product-name')); + this.productNameNode.textContent = this.productService.nameLong; + + this.currentVersionNode = dom.append(details, dom.$('.product-version')); + this.latestVersionNode = dom.append(details, dom.$('.product-version')); + this.releaseDateNode = dom.append(details, dom.$('.product-release-date')); + + this.releaseNotesLink = dom.append(details, dom.$('a.release-notes-link')) as HTMLAnchorElement; + this.releaseNotesLink.textContent = localize('updateTooltip.releaseNotesLink', "Release Notes"); + this.releaseNotesLink.href = '#'; + this._register(dom.addDisposableListener(this.releaseNotesLink, 'click', (e) => { + e.preventDefault(); + if (this.releaseNotesVersion) { + this.runCommandAndClose('update.showCurrentReleaseNotes', this.releaseNotesVersion); + } + })); + + // Progress section + this.progressContainer = dom.append(this.domNode, dom.$('.progress-container')); + const progressBar = dom.append(this.progressContainer, dom.$('.progress-bar')); + this.progressFill = dom.append(progressBar, dom.$('.progress-fill')); + + const progressText = dom.append(this.progressContainer, dom.$('.progress-text')); + this.progressPercentNode = dom.append(progressText, dom.$('span')); + this.progressSizeNode = dom.append(progressText, dom.$('span')); + + // Extra download stats + this.downloadStatsContainer = dom.append(this.progressContainer, dom.$('.download-stats')); + this.timeRemainingNode = dom.append(this.downloadStatsContainer, dom.$('.time-remaining')); + this.speedInfoNode = dom.append(this.downloadStatsContainer, dom.$('.speed-info')); + + // State-specific message + this.messageNode = dom.append(this.domNode, dom.$('.state-message')); + + // Populate static product info + this.updateCurrentVersion(); + + // Subscribe to state changes + this._register(updateService.onStateChange(state => this.onStateChange(state))); + this.onStateChange(updateService.state); + } + + private updateCurrentVersion() { + const productVersion = this.productService.version; + if (productVersion) { + const currentCommitId = this.productService.commit?.substring(0, 7); + this.currentVersionNode.textContent = currentCommitId + ? localize('updateTooltip.currentVersionLabelWithCommit', "Current Version: {0} ({1})", productVersion, currentCommitId) + : localize('updateTooltip.currentVersionLabel', "Current Version: {0}", productVersion); + this.currentVersionNode.style.display = ''; + } else { + this.currentVersionNode.style.display = 'none'; + } + } + + private onStateChange(state: State) { + this.progressContainer.style.display = 'none'; + this.speedInfoNode.textContent = ''; + this.timeRemainingNode.textContent = ''; + this.messageNode.style.display = 'none'; + + switch (state.type) { + case StateType.Uninitialized: + this.renderUninitialized(); + break; + case StateType.Disabled: + this.renderDisabled(state); + break; + case StateType.Idle: + this.renderIdle(state); + break; + case StateType.CheckingForUpdates: + this.renderCheckingForUpdates(); + break; + case StateType.AvailableForDownload: + this.renderAvailableForDownload(state); + break; + case StateType.Downloading: + this.renderDownloading(state); + break; + case StateType.Downloaded: + this.renderDownloaded(state); + break; + case StateType.Updating: + this.renderUpdating(state); + break; + case StateType.Ready: + this.renderReady(state); + break; + case StateType.Overwriting: + this.renderOverwriting(state); + break; + } + } + + private renderUninitialized() { + this.renderTitleAndInfo(localize('updateTooltip.initializingTitle', "Initializing")); + this.showMessage(localize('updateTooltip.initializingMessage', "Initializing update service...")); + } + + private renderDisabled({ reason }: Disabled) { + this.renderTitleAndInfo(localize('updateTooltip.updatesDisabledTitle', "Updates Disabled")); + switch (reason) { + case DisablementReason.NotBuilt: + this.showMessage( + localize('updateTooltip.disabledNotBuilt', "Updates are not available for this build."), + Codicon.info); + break; + case DisablementReason.DisabledByEnvironment: + this.showMessage( + localize('updateTooltip.disabledByEnvironment', "Updates are disabled by the --disable-updates command line flag."), + Codicon.warning); + break; + case DisablementReason.ManuallyDisabled: + this.showMessage( + localize('updateTooltip.disabledManually', "Updates are manually disabled. Change the \"update.mode\" setting to enable."), + Codicon.warning); + break; + case DisablementReason.Policy: + this.showMessage( + localize('updateTooltip.disabledByPolicy', "Updates are disabled by organization policy."), + Codicon.info); + break; + case DisablementReason.MissingConfiguration: + this.showMessage( + localize('updateTooltip.disabledMissingConfig', "Updates are disabled because no update URL is configured."), + Codicon.info); + break; + case DisablementReason.InvalidConfiguration: + this.showMessage( + localize('updateTooltip.disabledInvalidConfig', "Updates are disabled because the update URL is invalid."), + Codicon.error); + break; + case DisablementReason.RunningAsAdmin: + this.showMessage( + localize( + 'updateTooltip.disabledRunningAsAdmin', + "Updates are not available when running a user install of {0} as administrator.", + this.productService.nameShort), + Codicon.warning); + break; + default: + this.showMessage(localize('updateTooltip.disabledGeneric', "Updates are disabled."), Codicon.warning); + break; + } + } + + private renderIdle({ error, notAvailable }: Idle) { + if (error) { + this.renderTitleAndInfo(localize('updateTooltip.updateErrorTitle', "Update Error")); + this.showMessage(error, Codicon.error); + return; + } + + if (notAvailable) { + this.renderTitleAndInfo(localize('updateTooltip.noUpdateAvailableTitle', "No Update Available")); + this.showMessage(localize('updateTooltip.noUpdateAvailableMessage', "There are no updates currently available."), Codicon.info); + return; + } + + this.renderTitleAndInfo(localize('updateTooltip.upToDateTitle', "Up to Date")); + switch (this.configurationService.getValue('update.mode')) { + case 'none': + this.showMessage(localize('updateTooltip.autoUpdateNone', "Automatic updates are disabled."), Codicon.warning); + break; + case 'manual': + this.showMessage(localize('updateTooltip.autoUpdateManual', "Automatic updates will be checked but not installed automatically.")); + break; + case 'start': + this.showMessage(localize('updateTooltip.autoUpdateStart', "Updates will be applied on restart.")); + break; + case 'default': + if (this.meteredConnectionService.isConnectionMetered) { + this.showMessage( + localize('updateTooltip.meteredConnectionMessage', "Automatic updates are paused because the network connection is metered."), + Codicon.radioTower); + } else { + this.showMessage( + localize('updateTooltip.autoUpdateDefault', "Automatic updates are enabled. Happy Coding!"), + Codicon.smiley); + } + break; + } + } + + private renderCheckingForUpdates() { + this.renderTitleAndInfo(localize('updateTooltip.checkingForUpdatesTitle', "Checking for Updates")); + this.showMessage(localize('updateTooltip.checkingPleaseWait', "Checking for updates, please wait...")); + } + + private renderAvailableForDownload({ update }: AvailableForDownload) { + this.renderTitleAndInfo(localize('updateTooltip.updateAvailableTitle', "Update Available"), update); + } + + private renderDownloading(state: Downloading) { + this.renderTitleAndInfo(localize('updateTooltip.downloadingUpdateTitle', "Downloading Update"), state.update); + + const { downloadedBytes, totalBytes } = state; + if (downloadedBytes !== undefined && totalBytes !== undefined && totalBytes > 0) { + const percentage = computeProgressPercent(downloadedBytes, totalBytes) ?? 0; + this.progressFill.style.width = `${percentage}%`; + this.progressPercentNode.textContent = `${percentage}%`; + this.progressSizeNode.textContent = `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}`; + this.progressContainer.style.display = ''; + + const speed = computeDownloadSpeed(state); + if (speed !== undefined && speed > 0) { + this.speedInfoNode.textContent = localize('updateTooltip.downloadSpeed', '{0}/s', formatBytes(speed)); + } + + const timeRemaining = computeDownloadTimeRemaining(state); + if (timeRemaining !== undefined && timeRemaining > 0) { + this.timeRemainingNode.textContent = `~${formatTimeRemaining(timeRemaining)} ${localize('updateTooltip.timeRemaining', "remaining")}`; + } + + this.downloadStatsContainer.style.display = ''; + } else { + this.showMessage(localize('updateTooltip.downloadingPleaseWait', "Downloading update, please wait...")); + } + } + + private renderDownloaded({ update }: Downloaded) { + this.renderTitleAndInfo(localize('updateTooltip.updateReadyTitle', "Update is Ready to Install"), update); + } + + private renderUpdating({ update, currentProgress, maxProgress }: Updating) { + this.renderTitleAndInfo(localize('updateTooltip.installingUpdateTitle', "Installing Update"), update); + + const percentage = computeProgressPercent(currentProgress, maxProgress); + if (percentage !== undefined) { + this.progressFill.style.width = `${percentage}%`; + this.progressPercentNode.textContent = `${percentage}%`; + this.progressSizeNode.textContent = ''; + this.progressContainer.style.display = ''; + } else { + this.showMessage(localize('updateTooltip.installingPleaseWait', "Installing update, please wait...")); + } + } + + private renderReady({ update }: Ready) { + this.renderTitleAndInfo(localize('updateTooltip.updateInstalledTitle', "Update Installed"), update); + } + + private renderOverwriting({ update }: Overwriting) { + this.renderTitleAndInfo(localize('updateTooltip.downloadingNewerUpdateTitle', "Downloading Newer Update"), update); + this.showMessage(localize('updateTooltip.downloadingNewerPleaseWait', "A newer update was released. Downloading, please wait...")); + } + + private renderTitleAndInfo(title: string, update?: IUpdate) { + this.titleNode.textContent = title; + + // Latest version + const version = update?.productVersion; + if (version) { + const updateCommitId = update.version?.substring(0, 7); + this.latestVersionNode.textContent = updateCommitId + ? localize('updateTooltip.latestVersionLabelWithCommit', "Latest Version: {0} ({1})", version, updateCommitId) + : localize('updateTooltip.latestVersionLabel', "Latest Version: {0}", version); + this.latestVersionNode.style.display = ''; + } else { + this.latestVersionNode.style.display = 'none'; + } + + // Release date + const releaseDate = update?.timestamp ?? tryParseDate(this.productService.date); + if (typeof releaseDate === 'number' && releaseDate > 0) { + this.releaseDateNode.textContent = localize('updateTooltip.releasedLabel', "Released {0}", formatDate(releaseDate)); + this.releaseDateNode.style.display = ''; + } else { + this.releaseDateNode.style.display = 'none'; + } + + // Release notes link + this.releaseNotesVersion = version ?? this.productService.version; + this.releaseNotesLink.style.display = this.releaseNotesVersion ? '' : 'none'; + } + + private showMessage(message: string, icon?: ThemeIcon) { + dom.clearNode(this.messageNode); + if (icon) { + const iconNode = dom.append(this.messageNode, dom.$('.state-message-icon')); + iconNode.classList.add(...ThemeIcon.asClassNameArray(icon)); + } + dom.append(this.messageNode, document.createTextNode(message)); + this.messageNode.style.display = ''; + } + + private runCommandAndClose(command: string, ...args: unknown[]) { + this.commandService.executeCommand(command, ...args); + this.hoverService.hideHover(true); + } +} diff --git a/src/vs/workbench/contrib/update/common/updateUtils.ts b/src/vs/workbench/contrib/update/common/updateUtils.ts new file mode 100644 index 00000000000..3060873d29e --- /dev/null +++ b/src/vs/workbench/contrib/update/common/updateUtils.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../nls.js'; +import { Downloading } from '../../../../platform/update/common/update.js'; + +/** + * Returns the progress percentage based on the current and maximum progress values. + */ +export function computeProgressPercent(current: number | undefined, max: number | undefined): number | undefined { + if (current === undefined || max === undefined || max <= 0) { + return undefined; + } + + return Math.max(Math.min(Math.round((current / max) * 100), 100), 0); +} + +/** + * Computes an estimate of remaining download time in seconds. + */ +export function computeDownloadTimeRemaining(state: Downloading): number | undefined { + const { downloadedBytes, totalBytes, startTime } = state; + if (downloadedBytes === undefined || totalBytes === undefined || startTime === undefined) { + return undefined; + } + + const elapsedMs = Date.now() - startTime; + if (downloadedBytes <= 0 || totalBytes <= 0 || elapsedMs <= 0) { + return undefined; + } + + const remainingBytes = totalBytes - downloadedBytes; + if (remainingBytes <= 0) { + return 0; + } + + const bytesPerMs = downloadedBytes / elapsedMs; + if (bytesPerMs <= 0) { + return undefined; + } + + const remainingMs = remainingBytes / bytesPerMs; + return Math.ceil(remainingMs / 1000); +} + +/** + * Computes the current download speed in bytes per second. + */ +export function computeDownloadSpeed(state: Downloading): number | undefined { + const { downloadedBytes, startTime } = state; + if (downloadedBytes === undefined || startTime === undefined) { + return undefined; + } + + const elapsedMs = Date.now() - startTime; + if (elapsedMs <= 0 || downloadedBytes <= 0) { + return undefined; + } + + return (downloadedBytes / elapsedMs) * 1000; +} + +/** + * Computes the version to use for fetching update info. + * - If the minor version differs: returns `{major}.{minor}` (e.g., 1.108.2 -> 1.109.5 => 1.109) + * - If the same minor: returns the target version as-is (e.g., 1.109.2 -> 1.109.5 => 1.109.5) + */ +export function computeUpdateInfoVersion(currentVersion: string, targetVersion: string): string | undefined { + const current = tryParseVersion(currentVersion); + const target = tryParseVersion(targetVersion); + if (!current || !target) { + return undefined; + } + + if (current.minor !== target.minor || current.major !== target.major) { + return `${target.major}.${target.minor}`; + } + + return `${target.major}.${target.minor}.${target.patch}`; +} + +/** + * Computes the URL to fetch update info from. + * Follows the release notes URL pattern but with `_update` suffix. + */ +export function getUpdateInfoUrl(version: string): string { + const versionLabel = version.replace(/\./g, '_').replace(/_0$/, ''); + return `https://code.visualstudio.com/raw/v${versionLabel}_update.md`; +} + +/** + * Formats the time remaining as a human-readable string. + */ +export function formatTimeRemaining(seconds: number): string { + const hours = seconds / 3600; + if (hours >= 1) { + const formattedHours = formatDecimal(hours); + if (formattedHours === '1') { + return localize('update.timeRemainingHour', "{0} hour", formattedHours); + } else { + return localize('update.timeRemainingHours', "{0} hours", formattedHours); + } + } + + const minutes = Math.floor(seconds / 60); + if (minutes >= 1) { + return localize('update.timeRemainingMinutes', "{0} min", minutes); + } + + return localize('update.timeRemainingSeconds', "{0}s", seconds); +} + +/** + * Formats a byte count as a human-readable string. + */ +export function formatBytes(bytes: number): string { + if (bytes < 1024) { + return localize('update.bytes', "{0} B", bytes); + } + + const kb = bytes / 1024; + if (kb < 1024) { + return localize('update.kilobytes', "{0} KB", formatDecimal(kb)); + } + + const mb = kb / 1024; + if (mb < 1024) { + return localize('update.megabytes', "{0} MB", formatDecimal(mb)); + } + + const gb = mb / 1024; + return localize('update.gigabytes', "{0} GB", formatDecimal(gb)); +} + +/** + * Tries to parse a date string and returns the timestamp or undefined if parsing fails. + */ +export function tryParseDate(date: string | undefined): number | undefined { + if (date === undefined) { + return undefined; + } + + try { + const parsed = Date.parse(date); + return isNaN(parsed) ? undefined : parsed; + } catch { + return undefined; + } +} + +/** + * Formats a timestamp as a localized date string. + */ +export function formatDate(timestamp: number): string { + return new Date(timestamp).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +} + +/** + * Formats a number to 1 decimal place, omitting ".0" for whole numbers. + */ +export function formatDecimal(value: number): string { + const rounded = Math.round(value * 10) / 10; + return rounded % 1 === 0 ? rounded.toString() : rounded.toFixed(1); +} + +export interface IVersion { + major: number; + minor: number; + patch: number; +} + +/** + * Parses a version string in the format "major.minor.patch" and returns an object with the components. + */ +export function tryParseVersion(version: string | undefined): IVersion | undefined { + if (version === undefined) { + return undefined; + } + + const match = /^(\d{1,10})\.(\d{1,10})\.(\d{1,10})/.exec(version); + if (!match) { + return undefined; + } + + try { + return { + major: parseInt(match[1]), + minor: parseInt(match[2]), + patch: parseInt(match[3]) + }; + } catch { + return undefined; + } +} + +/** + * Processes an error message and returns a user-friendly version of it, or undefined if the error should be ignored. + */ +export function preprocessError(error?: string): string | undefined { + if (!error) { + return undefined; + } + + if (/The request timed out|The network connection was lost/i.test(error)) { + return undefined; + } + + return error.replace( + /See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, + 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information' + ); +} diff --git a/src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts b/src/vs/workbench/contrib/update/test/common/updateUtils.test.ts similarity index 56% rename from src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts rename to src/vs/workbench/contrib/update/test/common/updateUtils.test.ts index aa8c2a4693f..cfeb6123415 100644 --- a/src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts +++ b/src/vs/workbench/contrib/update/test/common/updateUtils.test.ts @@ -7,9 +7,9 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Downloading, StateType } from '../../../../../platform/update/common/update.js'; -import { computeDownloadSpeed, computeDownloadTimeRemaining, formatBytes, formatDate, formatTimeRemaining, getProgressPercent, tryParseDate } from '../../browser/updateStatusBarEntry.js'; +import { computeDownloadSpeed, computeDownloadTimeRemaining, computeProgressPercent, computeUpdateInfoVersion, formatBytes, formatDate, formatTimeRemaining, getUpdateInfoUrl, tryParseDate } from '../../common/updateUtils.js'; -suite('UpdateStatusBarEntry', () => { +suite('UpdateUtils', () => { ensureNoDisposablesAreLeakedInTestSuite(); let clock: sinon.SinonFakeTimers; @@ -22,30 +22,30 @@ suite('UpdateStatusBarEntry', () => { clock.restore(); }); - function createDownloadingState(downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading { + function DownloadingState(downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading { return { type: StateType.Downloading, explicit: true, overwrite: false, downloadedBytes, totalBytes, startTime }; } - suite('getProgressPercent', () => { + suite('computeProgressPercent', () => { test('handles invalid values', () => { - assert.strictEqual(getProgressPercent(undefined, 100), undefined); - assert.strictEqual(getProgressPercent(50, undefined), undefined); - assert.strictEqual(getProgressPercent(undefined, undefined), undefined); - assert.strictEqual(getProgressPercent(50, 0), undefined); - assert.strictEqual(getProgressPercent(50, -10), undefined); + assert.strictEqual(computeProgressPercent(undefined, 100), undefined); + assert.strictEqual(computeProgressPercent(50, undefined), undefined); + assert.strictEqual(computeProgressPercent(undefined, undefined), undefined); + assert.strictEqual(computeProgressPercent(50, 0), undefined); + assert.strictEqual(computeProgressPercent(50, -10), undefined); }); test('computes correct percentage', () => { - assert.strictEqual(getProgressPercent(0, 100), 0); - assert.strictEqual(getProgressPercent(50, 100), 50); - assert.strictEqual(getProgressPercent(100, 100), 100); - assert.strictEqual(getProgressPercent(1, 3), 33); - assert.strictEqual(getProgressPercent(2, 3), 67); + assert.strictEqual(computeProgressPercent(0, 100), 0); + assert.strictEqual(computeProgressPercent(50, 100), 50); + assert.strictEqual(computeProgressPercent(100, 100), 100); + assert.strictEqual(computeProgressPercent(1, 3), 33); + assert.strictEqual(computeProgressPercent(2, 3), 67); }); test('clamps to 0-100 range', () => { - assert.strictEqual(getProgressPercent(-10, 100), 0); - assert.strictEqual(getProgressPercent(200, 100), 100); + assert.strictEqual(computeProgressPercent(-10, 100), 0); + assert.strictEqual(computeProgressPercent(200, 100), 100); }); }); @@ -54,42 +54,110 @@ suite('UpdateStatusBarEntry', () => { const now = Date.now(); // Missing parameters - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState()), undefined); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(500, undefined, now)), undefined); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(undefined, 1000, now)), undefined); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(500, 1000, undefined)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState()), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(500, undefined, now)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(undefined, 1000, now)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(500, 1000, undefined)), undefined); // Zero or negative values - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(0, 1000, now - 1000)), undefined); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(500, 0, now - 1000)), undefined); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(500, 1000, now + 1000)), undefined); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(-100, 1000, now - 1000)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(0, 1000, now - 1000)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(500, 0, now - 1000)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(500, 1000, now + 1000)), undefined); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(-100, 1000, now - 1000)), undefined); }); test('returns 0 when download is complete or over-downloaded', () => { const now = Date.now(); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(1000, 1000, now - 1000)), 0); - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(1500, 1000, now - 1000)), 0); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(1000, 1000, now - 1000)), 0); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(1500, 1000, now - 1000)), 0); }); test('computes correct time remaining', () => { const now = Date.now(); // Simple case: Downloaded 500 bytes of 1000 in 1000ms => 1s remaining - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(500, 1000, now - 1000)), 1); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(500, 1000, now - 1000)), 1); // 10 seconds remaining: Downloaded 100MB of 200MB in 10s const downloadedBytes = 100 * 1024 * 1024; const totalBytes = 200 * 1024 * 1024; - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(downloadedBytes, totalBytes, now - 10000)), 10); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(downloadedBytes, totalBytes, now - 10000)), 10); // Rounds up: 900 of 1000 bytes in 900ms => 100ms remaining => rounds to 1s - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(900, 1000, now - 900)), 1); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(900, 1000, now - 900)), 1); // Realistic scenario: 50MB of 100MB in 50s => 50s remaining const downloaded50MB = 50 * 1024 * 1024; const total100MB = 100 * 1024 * 1024; - assert.strictEqual(computeDownloadTimeRemaining(createDownloadingState(downloaded50MB, total100MB, now - 50000)), 50); + assert.strictEqual(computeDownloadTimeRemaining(DownloadingState(downloaded50MB, total100MB, now - 50000)), 50); + }); + }); + + + suite('computeDownloadSpeed', () => { + test('returns undefined for invalid or incomplete input', () => { + const now = Date.now(); + assert.strictEqual(computeDownloadSpeed(DownloadingState(undefined, 1000, now - 1000)), undefined); + assert.strictEqual(computeDownloadSpeed(DownloadingState(500, 1000, undefined)), undefined); + assert.strictEqual(computeDownloadSpeed(DownloadingState(undefined, undefined, undefined)), undefined); + }); + + test('returns undefined for zero or negative elapsed time', () => { + const now = Date.now(); + assert.strictEqual(computeDownloadSpeed(DownloadingState(500, 1000, now + 1000)), undefined); + }); + + test('returns undefined for zero downloaded bytes', () => { + const now = Date.now(); + assert.strictEqual(computeDownloadSpeed(DownloadingState(0, 1000, now - 1000)), undefined); + }); + + test('computes correct download speed in bytes per second', () => { + const now = Date.now(); + + // 1000 bytes in 1 second = 1000 B/s + const speed1 = computeDownloadSpeed(DownloadingState(1000, 2000, now - 1000)); + assert.ok(speed1 !== undefined); + assert.ok(Math.abs(speed1 - 1000) < 50); // Allow small timing variance + + // 10 MB in 10 seconds = 1 MB/s = 1048576 B/s + const tenMB = 10 * 1024 * 1024; + const speed2 = computeDownloadSpeed(DownloadingState(tenMB, tenMB * 2, now - 10000)); + assert.ok(speed2 !== undefined); + const expectedSpeed = 1024 * 1024; // 1 MB/s + assert.ok(Math.abs(speed2 - expectedSpeed) < expectedSpeed * 0.01); // Within 1% + }); + }); + + suite('computeUpdateInfoVersion', () => { + test('returns minor .0 version when minor differs', () => { + assert.strictEqual(computeUpdateInfoVersion('1.108.2', '1.109.5'), '1.109'); + assert.strictEqual(computeUpdateInfoVersion('1.108.0', '1.109.0'), '1.109'); + assert.strictEqual(computeUpdateInfoVersion('1.107.3', '1.110.1'), '1.110'); + }); + + test('returns target version as-is when same minor', () => { + assert.strictEqual(computeUpdateInfoVersion('1.109.2', '1.109.5'), '1.109.5'); + assert.strictEqual(computeUpdateInfoVersion('1.109.0', '1.109.3'), '1.109.3'); + }); + + test('returns minor .0 version when major differs', () => { + assert.strictEqual(computeUpdateInfoVersion('1.109.2', '2.0.1'), '2.0'); + }); + + test('returns undefined for invalid versions', () => { + assert.strictEqual(computeUpdateInfoVersion('invalid', '1.109.5'), undefined); + assert.strictEqual(computeUpdateInfoVersion('1.109.2', 'invalid'), undefined); + }); + }); + + suite('getUpdateInfoUrl', () => { + test('constructs correct URL for .0 versions', () => { + assert.strictEqual(getUpdateInfoUrl('1.109.0'), 'https://code.visualstudio.com/raw/v1_109_update.md'); + }); + + test('constructs correct URL for patch versions', () => { + assert.strictEqual(getUpdateInfoUrl('1.109.5'), 'https://code.visualstudio.com/raw/v1_109_5_update.md'); }); }); @@ -177,39 +245,4 @@ suite('UpdateStatusBarEntry', () => { assert.ok(result.includes('2024')); }); }); - - suite('computeDownloadSpeed', () => { - test('returns undefined for invalid or incomplete input', () => { - const now = Date.now(); - assert.strictEqual(computeDownloadSpeed(createDownloadingState(undefined, 1000, now - 1000)), undefined); - assert.strictEqual(computeDownloadSpeed(createDownloadingState(500, 1000, undefined)), undefined); - assert.strictEqual(computeDownloadSpeed(createDownloadingState(undefined, undefined, undefined)), undefined); - }); - - test('returns undefined for zero or negative elapsed time', () => { - const now = Date.now(); - assert.strictEqual(computeDownloadSpeed(createDownloadingState(500, 1000, now + 1000)), undefined); - }); - - test('returns undefined for zero downloaded bytes', () => { - const now = Date.now(); - assert.strictEqual(computeDownloadSpeed(createDownloadingState(0, 1000, now - 1000)), undefined); - }); - - test('computes correct download speed in bytes per second', () => { - const now = Date.now(); - - // 1000 bytes in 1 second = 1000 B/s - const speed1 = computeDownloadSpeed(createDownloadingState(1000, 2000, now - 1000)); - assert.ok(speed1 !== undefined); - assert.ok(Math.abs(speed1 - 1000) < 50); // Allow small timing variance - - // 10 MB in 10 seconds = 1 MB/s = 1048576 B/s - const tenMB = 10 * 1024 * 1024; - const speed2 = computeDownloadSpeed(createDownloadingState(tenMB, tenMB * 2, now - 10000)); - assert.ok(speed2 !== undefined); - const expectedSpeed = 1024 * 1024; // 1 MB/s - assert.ok(Math.abs(speed2 - expectedSpeed) < expectedSpeed * 0.01); // Within 1% - }); - }); }); From bcc83ce10513c4ef7b88d7edeaef50739fcc0c8b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Mar 2026 15:39:28 -0400 Subject: [PATCH 441/448] Fix `cannot read properties of undefined (reading 'filter')` (#300518) prevent error from throwing --- .../browser/tools/monitoring/outputMonitor.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 612a59eb9bc..43ef48eca9d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -18,7 +18,7 @@ import { ChatElicitationRequestPart } from '../../../../../chat/common/model/cha import { ChatModel } from '../../../../../chat/common/model/chatModel.js'; import { ElicitationState, IChatService } from '../../../../../chat/common/chatService/chatService.js'; import { ChatAgentLocation, ChatPermissionLevel } from '../../../../../chat/common/constants.js'; -import { ChatMessageRole, getTextResponseFromStream, ILanguageModelsService } from '../../../../../chat/common/languageModels.js'; +import { ChatMessageRole, getTextResponseFromStream, type ILanguageModelChatSelector, ILanguageModelsService } from '../../../../../chat/common/languageModels.js'; import { IToolInvocationContext } from '../../../../../chat/common/tools/languageModelToolsService.js'; import { ITaskService } from '../../../../../tasks/common/taskService.js'; import { ILinkLocation } from '../../taskHelpers.js'; @@ -625,7 +625,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts) || this._isAutopilotMode(); let model = this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)[0]?.input.currentLanguageModel; if (model) { - const models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot', family: model.replaceAll('copilot/', '') }); + const models = await this._safeSelectLanguageModels({ vendor: 'copilot', family: model.replaceAll('copilot/', '') }); model = models[0]; } if (!model) { @@ -927,9 +927,18 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { } private async _getLanguageModel(): Promise { - const models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); + const models = await this._safeSelectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); return models.length ? models[0] : undefined; } + + private async _safeSelectLanguageModels(selector: ILanguageModelChatSelector): Promise { + try { + return await this._languageModelsService.selectLanguageModels(selector); + } catch (error) { + this._logService.trace('OutputMonitor: selectLanguageModels failed', { selector, error }); + return []; + } + } } function getMoreActions(suggestedOption: SuggestedOption, confirmationPrompt: IConfirmationPrompt): IAction[] | undefined { From ea9e9386aa53ce9ced254d8ce2e4490ce5f9a3c9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Mar 2026 12:51:52 -0700 Subject: [PATCH 442/448] chat: simplify symbol reference cache to an array (#300489) * chat: simplify symbol reference cache to an array Replaces the Map-based symbol reference cache with a simple array that keeps only the last 3 copied symbols. This removes the 15-second TTL expiration logic and relies on insertion order instead. - Changed symbolReferenceCache from Map to array - Removed symbolCacheTtlMs constant and pruning logic - Added symbolCacheMaxSize constant for max entries - Simplified cache lookups with array.find() - Automatically evicts oldest entry when exceeding capacity (Commit message generated by Copilot) * comment * skip flakey test, suggested by Megan --- .../test/node/terminalProcess.test.ts | 2 +- .../widget/input/editor/chatPasteProviders.ts | 75 ++++--------------- 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/src/vs/platform/terminal/test/node/terminalProcess.test.ts b/src/vs/platform/terminal/test/node/terminalProcess.test.ts index 07e1dd16ae8..b5aed3fed0a 100644 --- a/src/vs/platform/terminal/test/node/terminalProcess.test.ts +++ b/src/vs/platform/terminal/test/node/terminalProcess.test.ts @@ -132,7 +132,7 @@ function buildMultilineCommand(lineCount: number, outputFile: string): { command await runMultilineTest(20); }); - test('large multiline command (500 lines, ~32KB)', async function () { + test.skip('large multiline command (500 lines, ~32KB)', async function () { this.timeout(30000); await runMultilineTest(500); }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts index 0cd75d575cd..be28fc3feed 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts @@ -481,82 +481,39 @@ function createEditSession(edit: DocumentPasteEdit): DocumentPasteEditsSession { } const identifierPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; -const symbolCacheTtlMs = 15_000; +const symbolCacheMaxSize = 3; type SymbolReferenceCacheEntry = { - expiresAt: number; - value?: ResolvedSymbolReference; + key: string; promise?: Promise; }; -const symbolReferenceCache = new Map(); +const symbolReferenceCache: SymbolReferenceCacheEntry[] = []; function getSymbolReferenceCacheKey(uri: URI, range: IRange, text: string): string { return `${uri.toString()}|${range.startLineNumber}:${range.startColumn}-${range.endLineNumber}:${range.endColumn}|${text}`; } -function pruneSymbolReferenceCache(): void { - const now = Date.now(); - for (const [key, value] of symbolReferenceCache) { - if (value.expiresAt <= now) { - symbolReferenceCache.delete(key); - } - } -} - async function getCachedSymbolReference(uri: URI, range: IRange, text: string): Promise { const key = getSymbolReferenceCacheKey(uri, range, text); - const entry = symbolReferenceCache.get(key); - pruneSymbolReferenceCache(); - - if (!entry) { - return; - } - - if (entry.value) { - return entry.value; - } - - if (entry.promise) { - return entry.promise; - } - - return; + return symbolReferenceCache.find(e => e.key === key)?.promise; } function cacheSymbolReference(uri: URI, range: IRange, text: string, valuePromise: Promise): void { - const key = getSymbolReferenceCacheKey(uri, range, text); - const wrappedPromise = valuePromise.then(value => { - const current = symbolReferenceCache.get(key); - if (current?.promise !== wrappedPromise) { - return value; - } + const entry: SymbolReferenceCacheEntry = { + key: getSymbolReferenceCacheKey(uri, range, text), + promise: valuePromise, + }; + symbolReferenceCache.unshift(entry); + while (symbolReferenceCache.length > symbolCacheMaxSize) { + symbolReferenceCache.pop(); + } - if (!value) { - symbolReferenceCache.delete(key); - return; + valuePromise.catch(() => { + const i = symbolReferenceCache.indexOf(entry); + if (i !== -1) { + symbolReferenceCache.splice(i, 1); } - - symbolReferenceCache.set(key, { - value, - expiresAt: Date.now() + symbolCacheTtlMs - }); - pruneSymbolReferenceCache(); - return value; - }, () => { - const current = symbolReferenceCache.get(key); - if (current?.promise === wrappedPromise) { - symbolReferenceCache.delete(key); - } - return undefined; }); - - symbolReferenceCache.set(key, { - promise: wrappedPromise, - expiresAt: Date.now() + symbolCacheTtlMs - }); - pruneSymbolReferenceCache(); - - void wrappedPromise; } async function resolveSymbolReference( From 9a1fe573bbc1ece50c99bde2ff0f802b52d3a00e Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 10 Mar 2026 13:21:19 -0700 Subject: [PATCH 443/448] Fix registerToolDefinition tools from default chat agent being filtered out (#300529) Tools registered via registerToolDefinition by the default chat agent (copilot-chat) were incorrectly getting source type 'extension' instead of ToolDataSource.Internal. This caused them to be filtered out by the chat.extensionTools.enabled setting check in getAllToolsIncludingDisabled(). Package.json-contributed tools from the same extension correctly got ToolDataSource.Internal via the isBuiltinTool check in languageModelToolsContribution.ts. Apply the same logic in $registerToolWithDefinition on the main thread. --- .../api/browser/mainThreadLanguageModelTools.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 69a38597269..d4a4edb3cd4 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -10,6 +10,7 @@ import { ThemeIcon } from '../../../base/common/themables.js'; import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; +import { IProductService } from '../../../platform/product/common/productService.js'; import { toToolSetKey } from '../../contrib/chat/common/tools/languageModelToolsContribution.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolProgressStep, IToolResult, ToolDataSource, ToolProgress, toolResultHasBuffers, ToolSet } from '../../contrib/chat/common/tools/languageModelToolsService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; @@ -30,6 +31,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre extHostContext: IExtHostContext, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, @ILogService private readonly _logService: ILogService, + @IProductService private readonly _productService: IProductService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); @@ -118,8 +120,13 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre } } - // Convert source from DTO - const source = revive(definition.source); + // Convert source from DTO, matching the isBuiltinTool logic from languageModelToolsContribution + const isBuiltinTool = this._productService.defaultChatAgent?.chatExtensionId + ? ExtensionIdentifier.equals(extensionId, this._productService.defaultChatAgent.chatExtensionId) + : false; + const source: ToolDataSource = isBuiltinTool + ? ToolDataSource.Internal + : revive(definition.source); // Create the tool data const toolData: IToolData = { From f01d41c78427b8f7b2d3651a02500a05ed3e69ae Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:29:24 -0700 Subject: [PATCH 444/448] Keep Chat customizations section selected while active (#300528) Keep active customization section selected Ensure the left sections list re-applies selection/focus for the active section when the list selection is cleared, so the active view remains persistently selected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aiCustomizationManagementEditor.ts | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 82cfd7a06ad..8235c4644bb 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -362,17 +362,14 @@ export class AICustomizationManagementEditor extends EditorPane { )); this.sectionsList.splice(0, this.sectionsList.length, this.sections); - - // Select the saved section - const selectedIndex = this.sections.findIndex(s => s.id === this.selectedSection); - if (selectedIndex >= 0) { - this.sectionsList.setSelection([selectedIndex]); - } + this.ensureSectionsListReflectsActiveSection(); this.editorDisposables.add(this.sectionsList.onDidChangeSelection(e => { - if (e.elements.length > 0) { - this.selectSection(e.elements[0].id); + if (e.elements.length === 0) { + this.ensureSectionsListReflectsActiveSection(); + return; } + this.selectSection(e.elements[0].id); })); // Folder picker (sessions window only) @@ -539,6 +536,7 @@ export class AICustomizationManagementEditor extends EditorPane { private selectSection(section: AICustomizationManagementSection): void { if (this.selectedSection === section) { + this.ensureSectionsListReflectsActiveSection(section); return; } @@ -565,6 +563,29 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.isPromptsSection(section)) { void this.listWidget.setSection(section); } + + this.ensureSectionsListReflectsActiveSection(section); + } + + private ensureSectionsListReflectsActiveSection(section: AICustomizationManagementSection = this.selectedSection): void { + if (!this.sectionsList) { + return; + } + + const index = this.sections.findIndex(s => s.id === section); + if (index < 0) { + return; + } + + const selection = this.sectionsList.getSelection(); + if (selection.length !== 1 || selection[0] !== index) { + this.sectionsList.setSelection([index]); + } + + const focus = this.sectionsList.getFocus(); + if (focus.length !== 1 || focus[0] !== index) { + this.sectionsList.setFocus([index]); + } } private updateContentVisibility(): void { @@ -758,8 +779,7 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.isPromptsSection(sectionId)) { void this.listWidget.setSection(sectionId); } - this.sectionsList.setFocus([index]); - this.sectionsList.setSelection([index]); + this.ensureSectionsListReflectsActiveSection(sectionId); } } From 03968c207691d53fc3c265e7d665fbf80b3e4251 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:42:52 -0700 Subject: [PATCH 445/448] Re-remove webpack All of our extensions are now using esbuild --- build/gulpfile.extensions.ts | 12 - build/lib/extensions.ts | 208 +-- extensions/mangle-loader.js | 66 - extensions/shared.webpack.config.mjs | 209 --- package-lock.json | 1447 ------------------- package.json | 9 - test/monaco/package-lock.json | 1933 +++++++++++++++++++++++++- test/monaco/package.json | 12 +- 8 files changed, 1945 insertions(+), 1951 deletions(-) delete mode 100644 extensions/mangle-loader.js delete mode 100644 extensions/shared.webpack.config.mjs diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index 8f9ac9b2b21..e0137816c8c 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -309,13 +309,6 @@ async function buildWebExtensions(isWatch: boolean): Promise { { ignore: ['**/node_modules'] } ); - // Find all webpack configs, excluding those that will be esbuilt - const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p))); - const webpackConfigLocations = (await nodeUtil.promisify(glob)( - path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), - { ignore: ['**/node_modules'] } - )).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath))); - const promises: Promise[] = []; // Esbuild for extensions @@ -330,10 +323,5 @@ async function buildWebExtensions(isWatch: boolean): Promise { ); } - // Run webpack for remaining extensions - if (webpackConfigLocations.length > 0) { - promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })))); - } - await Promise.all(promises); } diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 5710f4d6919..aacf25cbbc1 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -20,10 +20,8 @@ import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; -import webpack from 'webpack'; import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; -import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; @@ -32,8 +30,8 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const root = path.dirname(path.dirname(import.meta.dirname)); -const commit = getVersion(root); -const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +// const commit = getVersion(root); +// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -65,32 +63,24 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { +function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb ? 'esbuild.browser.mts' : 'esbuild.mts'; - const webpackConfigFileName = forWeb - ? `extension-browser.webpack.config.js` - : `extension.webpack.config.js`; - const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); - const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step + // Esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), ...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)), ); isBundled = true; - } else if (hasWebpack) { - input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); - isBundled = true; } else { input = fromLocalNormal(extensionPath); } @@ -122,132 +112,6 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean) return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } -function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { - const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); - const webpack = require('webpack'); - const webpackGulp = require('webpack-stream'); - const result = es.through(); - - const packagedDependencies: string[] = []; - const stripOutSourceMaps: string[] = []; - const packageJsonConfig = require(path.join(extensionPath, 'package.json')); - if (packageJsonConfig.dependencies) { - const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); - const webpackRootConfig = webpackConfig.default; - for (const key in webpackRootConfig.externals) { - if (key in packageJsonConfig.dependencies) { - packagedDependencies.push(key); - } - } - - if (webpackConfig.StripOutSourceMaps) { - for (const filePath of webpackConfig.StripOutSourceMaps) { - stripOutSourceMaps.push(filePath); - } - } - } - - // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar - // to vsce.PackageManager.Yarn. - // A static analysis showed there are no webpack externals that are dependencies of the current - // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list - // as a temporary workaround. - vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { - const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ - path: filePath, - stat: fs.statSync(filePath), - base: extensionPath, - contents: fs.createReadStream(filePath) - })); - - // check for a webpack configuration files, then invoke webpack - // and merge its output with the files stream. - const webpackConfigLocations = (glob.sync( - path.join(extensionPath, '**', webpackConfigFileName), - { ignore: ['**/node_modules'] } - ) as string[]); - const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { - - const webpackDone = (err: Error | undefined, stats: any) => { - fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); - if (err) { - result.emit('error', err); - } - const { compilation } = stats; - if (compilation.errors.length > 0) { - result.emit('error', compilation.errors.join('\n')); - } - if (compilation.warnings.length > 0) { - result.emit('error', compilation.warnings.join('\n')); - } - }; - - const exportedConfig = require(webpackConfigPath).default; - return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { - const webpackConfig = { - ...config, - ...{ mode: 'production' } - }; - if (disableMangle) { - if (Array.isArray(config.module.rules)) { - for (const rule of config.module.rules) { - if (Array.isArray(rule.use)) { - for (const use of rule.use) { - if (String(use.loader).endsWith('mangle-loader.js')) { - use.options.disabled = true; - } - } - } - } - } - } - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data: File) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - if (path.extname(data.basename) === '.js') { - if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); - } else { - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); - } - } - - this.emit('data', data); - })); - }); - }); - - es.merge(...webpackStreams, es.readArray(files)) - // .pipe(es.through(function (data) { - // // debug - // console.log('out', data.path, data.contents.length); - // this.emit('data', data); - // })) - .pipe(result); - - }).catch(err => { - console.error(extensionPath); - console.error(packagedDependencies); - result.emit('error', err); - }); - - return result.pipe(createStatsStream(path.basename(extensionPath))); -} function fromLocalNormal(extensionPath: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -649,70 +513,6 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); -export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { - const webpack = require('webpack') as typeof import('webpack'); - - const webpackConfigs: webpack.Configuration[] = []; - - for (const { configPath, outputRoot } of webpackConfigLocations) { - const configOrFnOrArray = require(configPath).default; - function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { - for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { - const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; - if (outputRoot) { - config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!)); - } - webpackConfigs.push(config); - } - } - addConfig(configOrFnOrArray); - } - - function reporter(fullStats: any) { - if (Array.isArray(fullStats.children)) { - for (const stats of fullStats.children) { - const outputPath = stats.outputPath; - if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); - const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); - } - if (Array.isArray(stats.errors)) { - stats.errors.forEach((error: any) => { - fancyLog.error(error); - }); - } - if (Array.isArray(stats.warnings)) { - stats.warnings.forEach((warning: any) => { - fancyLog.warn(warning); - }); - } - } - } - } - return new Promise((resolve, reject) => { - if (isWatch) { - webpack(webpackConfigs).watch({}, (err, stats) => { - if (err) { - reject(); - } else { - reporter(stats?.toJson()); - } - }); - } else { - webpack(webpackConfigs).run((err, stats) => { - if (err) { - fancyLog.error(err); - reject(); - } else { - reporter(stats?.toJson()); - resolve(); - } - }); - } - }); -} - export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise { function reporter(stdError: string, script: string) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); diff --git a/extensions/mangle-loader.js b/extensions/mangle-loader.js deleted file mode 100644 index ed32a85e633..00000000000 --- a/extensions/mangle-loader.js +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -const fs = require('fs'); -const webpack = require('webpack'); -const fancyLog = require('fancy-log'); -const ansiColors = require('ansi-colors'); -const { Mangler } = require('../build/lib/mangle/index.js'); - -/** - * Map of project paths to mangled file contents - * - * @type {Map>>} - */ -const mangleMap = new Map(); - -/** - * @param {string} projectPath - */ -function getMangledFileContents(projectPath) { - let entry = mangleMap.get(projectPath); - if (!entry) { - const log = (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data); - log(`Mangling ${projectPath}`); - const ts2tsMangler = new Mangler(projectPath, log, { mangleExports: true, manglePrivateFields: true }); - entry = ts2tsMangler.computeNewFileContents(); - mangleMap.set(projectPath, entry); - } - - return entry; -} - -/** - * @type {webpack.LoaderDefinitionFunction} - */ -module.exports = async function (source, sourceMap, meta) { - if (this.mode !== 'production') { - // Only enable mangling in production builds - return source; - } - if (true) { - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - return source; - } - const options = this.getOptions(); - if (options.disabled) { - // Dynamically disabled - return source; - } - - if (source !== fs.readFileSync(this.resourcePath).toString()) { - // File content has changed by previous webpack steps. - // Skip mangling. - return source; - } - - const callback = this.async(); - - const fileContentsMap = await getMangledFileContents(options.configFile); - - const newContents = fileContentsMap.get(this.resourcePath); - callback(null, newContents?.out ?? source, sourceMap, meta); -}; diff --git a/extensions/shared.webpack.config.mjs b/extensions/shared.webpack.config.mjs deleted file mode 100644 index 12b1ea522a4..00000000000 --- a/extensions/shared.webpack.config.mjs +++ /dev/null @@ -1,209 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import path from 'node:path'; -import fs from 'node:fs'; -import merge from 'merge-options'; -import CopyWebpackPlugin from 'copy-webpack-plugin'; -import webpack from 'webpack'; -import { createRequire } from 'node:module'; - -/** @typedef {import('webpack').Configuration} WebpackConfig **/ - -const require = createRequire(import.meta.url); - -const tsLoaderOptions = { - compilerOptions: { - 'sourceMap': true, - }, - onlyCompileBundledFiles: true, -}; - -function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfig) { - const defaultConfig = { - mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') - target: 'node', // extensions run in a node context - node: { - __dirname: false // leave the __dirname-behaviour intact - }, - - resolve: { - conditionNames: ['import', 'require', 'node-addons', 'node'], - mainFields: ['module', 'main'], - extensions: ['.ts', '.js'], // support ts-files and js-files - extensionAlias: { - // this is needed to resolve dynamic imports that now require the .js extension - '.js': ['.js', '.ts'], - } - }, - module: { - rules: [{ - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - // configure TypeScript loader: - // * enable sources maps for end-to-end source maps - loader: 'ts-loader', - options: tsLoaderOptions - }, - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - // { - // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), - // options: { - // configFile: path.join(extConfig.context, 'tsconfig.json') - // }, - // }, - ] - }] - }, - externals: { - 'electron': 'commonjs electron', // ignored to avoid bundling from node_modules - 'vscode': 'commonjs vscode', // ignored because it doesn't exist, - 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module - '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use - '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module - '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module - '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module - }, - output: { - // all output goes into `dist`. - // packaging depends on that and this must always be like it - filename: '[name].js', - path: path.join(extConfig.context, 'dist'), - libraryTarget: 'commonjs', - }, - // yes, really source maps - devtool: 'source-map', - plugins: nodePlugins(extConfig.context), - }; - - return merge(defaultConfig, extConfig); -} - -/** - * - * @param {string} context - */ -function nodePlugins(context) { - // Need to find the top-most `package.json` file - const folderName = path.relative(import.meta.dirname, context).split(/[\\\/]/)[0]; - const pkgPath = path.join(import.meta.dirname, folderName, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - const id = `${pkg.publisher}.${pkg.name}`; - return [ - new CopyWebpackPlugin({ - patterns: [ - { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } - ] - }) - ]; -} -/** - * @typedef {{ - * configFile?: string - * }} AdditionalBrowserConfig - */ - -function withBrowserDefaults(/**@type WebpackConfig & { context: string }*/extConfig, /** @type AdditionalBrowserConfig */ additionalOptions = {}) { - /** @type WebpackConfig */ - const defaultConfig = { - mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') - target: 'webworker', // extensions run in a webworker context - resolve: { - mainFields: ['browser', 'module', 'main'], - extensions: ['.ts', '.js'], // support ts-files and js-files - fallback: { - 'path': require.resolve('path-browserify'), - 'os': require.resolve('os-browserify'), - 'util': require.resolve('util') - }, - extensionAlias: { - // this is needed to resolve dynamic imports that now require the .js extension - '.js': ['.js', '.ts'], - }, - }, - module: { - rules: [{ - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - // configure TypeScript loader: - // * enable sources maps for end-to-end source maps - loader: 'ts-loader', - options: { - ...tsLoaderOptions, - // ...(additionalOptions ? {} : { configFile: additionalOptions.configFile }), - } - }, - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - // { - // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), - // options: { - // configFile: path.join(extConfig.context, additionalOptions?.configFile ?? 'tsconfig.json') - // }, - // }, - ] - }, { - test: /\.wasm$/, - type: 'asset/inline' - }] - }, - externals: { - 'vscode': 'commonjs vscode', // ignored because it doesn't exist, - 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module - '@azure/functions-core': 'commonjs azure/functions-core', // optional dependency of appinsights that we don't use - '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module - '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module - '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module - }, - performance: { - hints: false - }, - output: { - // all output goes into `dist`. - // packaging depends on that and this must always be like it - filename: '[name].js', - path: path.join(extConfig.context, 'dist', 'browser'), - libraryTarget: 'commonjs', - }, - // yes, really source maps - devtool: 'source-map', - plugins: browserPlugins(extConfig.context) - }; - - return merge(defaultConfig, extConfig); -} - -/** - * - * @param {string} context - */ -function browserPlugins(context) { - // Need to find the top-most `package.json` file - // const folderName = path.relative(__dirname, context).split(/[\\\/]/)[0]; - // const pkgPath = path.join(__dirname, folderName, 'package.json'); - // const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - // const id = `${pkg.publisher}.${pkg.name}`; - return [ - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 - }), - new CopyWebpackPlugin({ - patterns: [ - { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } - ] - }), - new webpack.DefinePlugin({ - 'process.platform': JSON.stringify('web'), - 'process.env': JSON.stringify({}), - 'process.env.BROWSER_ENV': JSON.stringify('true') - }) - ]; -} - -export default withNodeDefaults; -export { withNodeDefaults as node, withBrowserDefaults as browser, nodePlugins, browserPlugins }; diff --git a/package-lock.json b/package-lock.json index 46b1b98b935..a5d9a2ffccb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,6 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", - "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -100,8 +99,6 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.8.0", @@ -111,7 +108,6 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", - "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -152,17 +148,12 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", - "style-loader": "^3.3.2", "tar": "^7.5.9", - "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260306", "typescript-eslint": "^8.45.0", "util": "^0.12.4", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4", - "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, @@ -1188,15 +1179,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", - "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@electron/get": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", @@ -2197,28 +2179,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", @@ -3253,17 +3213,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -3504,17 +3453,6 @@ "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", "dev": true }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@types/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", @@ -5069,167 +5007,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", @@ -5237,50 +5014,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -5409,20 +5142,6 @@ "addons/*" ] }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@zip.js/zip.js": { "version": "2.8.21", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.21.tgz", @@ -5481,19 +5200,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", - "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5560,55 +5266,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -6559,15 +6216,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -7124,24 +6772,6 @@ } } }, - "node_modules/chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/chrome-trace-event/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -7340,41 +6970,6 @@ "node": ">= 0.10" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clone-deep/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -7486,12 +7081,6 @@ "color-support": "bin.js" } }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -7802,73 +7391,6 @@ "is-plain-object": "^5.0.0" } }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", - "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -8081,32 +7603,6 @@ "source-map-resolve": "^0.6.0" } }, - "node_modules/css-loader": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz", - "integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -8161,18 +7657,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -8583,18 +8067,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -8903,15 +8375,6 @@ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -9016,30 +8479,6 @@ "node": ">=6" } }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -10235,12 +9674,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, "node_modules/fastq": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", @@ -10288,44 +9721,6 @@ "node": ">=16.0.0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -11128,13 +10523,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/glob-watcher": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", @@ -13322,18 +12710,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -13385,22 +12761,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -14043,37 +13403,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -14211,12 +13540,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -14672,34 +13995,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/locate-app": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", @@ -14770,12 +14065,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.clone": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", - "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", - "dev": true - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -14801,12 +14090,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", - "dev": true - }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -15255,34 +14538,6 @@ "timers-ext": "^0.1.7" } }, - "node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/memory-fs/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -15317,13 +14572,6 @@ "node": ">=4" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -15792,25 +15040,6 @@ "dev": true, "optional": true }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -15877,12 +15106,6 @@ "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -17187,15 +16410,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -17296,70 +16510,6 @@ "node": ">=16.20.0" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/playwright": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", @@ -17455,114 +16605,6 @@ "node": ">=0.10.0" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", - "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", - "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -17726,12 +16768,6 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY= sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true - }, "node_modules/pseudo-localization": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/pseudo-localization/-/pseudo-localization-2.4.0.tgz", @@ -18365,27 +17401,6 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", @@ -18748,61 +17763,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -19040,27 +18000,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallow-clone/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -19553,16 +18492,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-resolve": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", @@ -20086,22 +19015,6 @@ ], "license": "MIT" }, - "node_modules/style-loader": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", - "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -20459,78 +19372,6 @@ "node": ">=6.0.0" } }, - "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -20865,35 +19706,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/ts-morph": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz", @@ -21730,20 +20542,6 @@ "node": ">=10" } }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -21883,245 +20681,6 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, - "node_modules/webpack": { - "version": "5.105.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", - "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-cli/node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-cli/node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", - "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", - "dev": true, - "dependencies": { - "fancy-log": "^1.3.3", - "lodash.clone": "^4.3.2", - "lodash.some": "^4.2.2", - "memory-fs": "^0.5.0", - "plugin-error": "^1.0.1", - "supports-color": "^8.1.1", - "through": "^2.3.8", - "vinyl": "^2.2.1" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "webpack": "^5.21.2" - } - }, - "node_modules/webpack-stream/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/webpack-stream/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/webpack-stream/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -22209,12 +20768,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, "node_modules/windows-foreground-love": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/windows-foreground-love/-/windows-foreground-love-0.6.1.tgz", diff --git a/package.json b/package.json index f934c399373..1317110a930 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,6 @@ "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", "@types/vscode-notebook-renderer": "^1.72.0", - "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2023.10.7", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", @@ -170,8 +169,6 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.9.1", "debounce": "^1.0.0", "deemon": "^1.13.6", "electron": "39.8.0", @@ -181,7 +178,6 @@ "eslint-plugin-jsdoc": "^50.3.1", "event-stream": "3.3.4", "fancy-log": "^1.3.3", - "file-loader": "^6.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-azure-storage": "^0.12.1", @@ -222,17 +218,12 @@ "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", - "style-loader": "^3.3.2", "tar": "^7.5.9", - "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", "typescript": "^6.0.0-dev.20260306", "typescript-eslint": "^8.45.0", "util": "^0.12.4", - "webpack": "^5.105.0", - "webpack-cli": "^5.1.4", - "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 513d33eeb34..3e3df8a1775 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -8,11 +8,79 @@ "name": "test-monaco", "version": "1.0.0", "license": "MIT", + "dependencies": { + "postcss": "^8.5.6" + }, "devDependencies": { "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "warnings-to-errors-webpack-plugin": "^2.3.0" + "css-loader": "^6.9.1", + "file-loader": "^6.2.0", + "style-loader": "^3.3.2", + "warnings-to-errors-webpack-plugin": "^2.3.0", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@types/chai": { @@ -21,6 +89,42 @@ "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", "dev": true }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/junit-report-builder": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", @@ -28,6 +132,333 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", + "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -80,6 +511,91 @@ "playwright": ">1.0.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -106,6 +622,122 @@ "node": "*" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -118,6 +750,220 @@ "node": ">=0.12" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -128,6 +974,174 @@ "node": "*" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/junit-report-builder": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz", @@ -143,6 +1157,58 @@ "node": ">=16" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -166,6 +1232,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -176,6 +1272,104 @@ "mustache": "bin/mustache" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -189,9 +1383,229 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -202,6 +1616,242 @@ "semver": "bin/semver.js" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -211,6 +1861,61 @@ "node": ">=4" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/warnings-to-errors-webpack-plugin": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/warnings-to-errors-webpack-plugin/-/warnings-to-errors-webpack-plugin-2.3.0.tgz", @@ -220,6 +1925,230 @@ "webpack": "^2.2.0-rc || ^3 || ^4 || ^5" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/test/monaco/package.json b/test/monaco/package.json index c7373919431..89902f2304f 100644 --- a/test/monaco/package.json +++ b/test/monaco/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "compile": "node ../../node_modules/typescript/bin/tsc", - "bundle-webpack": "node ../../node_modules/webpack/bin/webpack --config ./webpack.config.js --bail", + "bundle-webpack": "webpack --config ./webpack.config.js --bail", "esm-check": "node esm-check/esm-check.js", "test": "node runner.js" }, @@ -14,6 +14,14 @@ "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "warnings-to-errors-webpack-plugin": "^2.3.0" + "css-loader": "^6.9.1", + "file-loader": "^6.2.0", + "style-loader": "^3.3.2", + "warnings-to-errors-webpack-plugin": "^2.3.0", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "postcss": "^8.5.6" } } From d71bb75a3ac543517584cd237d9e1c3c9550134b Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 10 Mar 2026 13:52:16 -0700 Subject: [PATCH 446/448] Remove `await` in discovery phase (#300537) --- .../promptSyntax/service/promptsService.ts | 8 - .../service/promptsServiceImpl.ts | 157 +++++++----------- .../service/mockPromptsService.ts | 2 - .../service/promptsService.test.ts | 27 --- 4 files changed, 63 insertions(+), 131 deletions(-) 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 d03f9f66669..87987d457fd 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -493,14 +493,6 @@ export interface IPromptsService extends IDisposable { */ readonly onDidChangeSkills: Event; - /** - * Gets detailed discovery information for a prompt type. - * This includes all files found and their load/skip status with reasons. - * Used for diagnostics and config-info displays. - * @param sessionResource Optional session resource to scope debug logging to a specific session. - */ - getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken, sessionResource?: URI): Promise; - /** * Gets all hooks collected from hooks.json files. * The result is cached and invalidated when hook files change. 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 32e00b8a10f..c9c614bf77b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -559,7 +559,24 @@ export class PromptsService extends Disposable implements IPromptsService { } public async getPromptSlashCommands(token: CancellationToken, sessionResource?: URI): Promise { - return await this.cachedSlashCommands.get(token); + const sw = StopWatch.create(); + const result = await this.cachedSlashCommands.get(token); + if (sessionResource) { + const elapsed = sw.elapsed(); + void this.getPromptSlashCommandDiscoveryInfo(token).catch(() => undefined).then(discoveryInfo => { + const details = result.length === 1 + ? localize("promptsService.resolvedSlashCommand", "Resolved {0} slash command in {1}ms", result.length, elapsed.toFixed(1)) + : localize("promptsService.resolvedSlashCommands", "Resolved {0} slash commands in {1}ms", result.length, elapsed.toFixed(1)); + this._onDidLogDiscovery.fire({ + sessionResource, + name: localize("promptsService.loadSlashCommands", "Load Slash Commands"), + details, + discoveryInfo, + category: 'discovery', + }); + }); + } + return result; } private async computePromptSlashCommands(token: CancellationToken): Promise { @@ -645,16 +662,17 @@ export class PromptsService extends Disposable implements IPromptsService { const result = await this.cachedCustomAgents.get(token); if (sessionResource) { const elapsed = sw.elapsed(); - const discoveryInfo = await this.getAgentDiscoveryInfo(token); - const details = result.length === 1 - ? localize("promptsService.resolvedAgent", "Resolved {0} agent in {1}ms", result.length, elapsed.toFixed(1)) - : localize("promptsService.resolvedAgents", "Resolved {0} agents in {1}ms", result.length, elapsed.toFixed(1)); - this._onDidLogDiscovery.fire({ - sessionResource, - name: localize("promptsService.loadAgents", "Load Agents"), - details, - discoveryInfo, - category: 'discovery', + void this.getAgentDiscoveryInfo(token).catch(() => undefined).then(discoveryInfo => { + const details = result.length === 1 + ? localize("promptsService.resolvedAgent", "Resolved {0} agent in {1}ms", result.length, elapsed.toFixed(1)) + : localize("promptsService.resolvedAgents", "Resolved {0} agents in {1}ms", result.length, elapsed.toFixed(1)); + this._onDidLogDiscovery.fire({ + sessionResource, + name: localize("promptsService.loadAgents", "Load Agents"), + details, + discoveryInfo, + category: 'discovery', + }); }); } return result; @@ -1061,16 +1079,17 @@ export class PromptsService extends Disposable implements IPromptsService { const result = await this.cachedSkills.get(token); if (sessionResource) { const elapsed = sw.elapsed(); - const discoveryInfo = await this.getSkillDiscoveryInfo(token); - const details = result.length === 1 - ? localize("promptsService.resolvedSkill", "Resolved {0} skill in {1}ms", result.length, elapsed.toFixed(1)) - : localize("promptsService.resolvedSkills", "Resolved {0} skills in {1}ms", result.length, elapsed.toFixed(1)); - this._onDidLogDiscovery.fire({ - sessionResource, - name: localize("promptsService.loadSkills", "Load Skills"), - details, - discoveryInfo, - category: 'discovery', + void this.getSkillDiscoveryInfo(token).catch(() => undefined).then(discoveryInfo => { + const details = result.length === 1 + ? localize("promptsService.resolvedSkill", "Resolved {0} skill in {1}ms", result.length, elapsed.toFixed(1)) + : localize("promptsService.resolvedSkills", "Resolved {0} skills in {1}ms", result.length, elapsed.toFixed(1)); + this._onDidLogDiscovery.fire({ + sessionResource, + name: localize("promptsService.loadSkills", "Load Skills"), + details, + discoveryInfo, + category: 'discovery', + }); }); } return result; @@ -1184,17 +1203,18 @@ export class PromptsService extends Disposable implements IPromptsService { const result = await this.cachedHooks.get(token); if (sessionResource) { const elapsed = sw.elapsed(); - const hookCount = result ? Object.values(result.hooks).reduce((sum, arr) => sum + arr.length, 0) : 0; - const discoveryInfo = await this.getHookDiscoveryInfo(token); - const details = hookCount === 1 - ? localize("promptsService.resolvedHook", "Resolved {0} hook in {1}ms", hookCount, elapsed.toFixed(1)) - : localize("promptsService.resolvedHooks", "Resolved {0} hooks in {1}ms", hookCount, elapsed.toFixed(1)); - this._onDidLogDiscovery.fire({ - sessionResource, - name: localize("promptsService.loadHooks", "Load Hooks"), - details, - discoveryInfo, - category: 'discovery', + void this.getHookDiscoveryInfo(token).catch(() => undefined).then(discoveryInfo => { + const hookCount = result ? Object.values(result.hooks).reduce((sum, arr) => sum + arr.length, 0) : 0; + const details = hookCount === 1 + ? localize("promptsService.resolvedHook", "Resolved {0} hook in {1}ms", hookCount, elapsed.toFixed(1)) + : localize("promptsService.resolvedHooks", "Resolved {0} hooks in {1}ms", hookCount, elapsed.toFixed(1)); + this._onDidLogDiscovery.fire({ + sessionResource, + name: localize("promptsService.loadHooks", "Load Hooks"), + details, + discoveryInfo, + category: 'discovery', + }); }); } return result; @@ -1205,16 +1225,17 @@ export class PromptsService extends Disposable implements IPromptsService { const result = await this.listPromptFiles(PromptsType.instructions, token); if (sessionResource) { const elapsed = sw.elapsed(); - const discoveryInfo = await this.getInstructionsDiscoveryInfo(token); - const details = result.length === 1 - ? localize("promptsService.resolvedInstruction", "Resolved {0} instruction in {1}ms", result.length, elapsed.toFixed(1)) - : localize("promptsService.resolvedInstructions", "Resolved {0} instructions in {1}ms", result.length, elapsed.toFixed(1)); - this._onDidLogDiscovery.fire({ - sessionResource, - name: localize("promptsService.loadInstructions", "Load Instructions"), - details, - discoveryInfo, - category: 'discovery', + void this.getInstructionsDiscoveryInfo(token).catch(() => undefined).then(discoveryInfo => { + const details = result.length === 1 + ? localize("promptsService.resolvedInstruction", "Resolved {0} instruction in {1}ms", result.length, elapsed.toFixed(1)) + : localize("promptsService.resolvedInstructions", "Resolved {0} instructions in {1}ms", result.length, elapsed.toFixed(1)); + this._onDidLogDiscovery.fire({ + sessionResource, + name: localize("promptsService.loadInstructions", "Load Instructions"), + details, + discoveryInfo, + category: 'discovery', + }); }); } return result; @@ -1318,58 +1339,6 @@ export class PromptsService extends Disposable implements IPromptsService { return { hooks: result, hasDisabledClaudeHooks }; } - public async getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken, sessionResource?: URI): Promise { - if (sessionResource) { - this._onDidLogDiscovery.fire({ - sessionResource, - name: localize("promptsService.discoveryStart", "Discovery {0} (Start)", type), - category: 'discovery', - }); - } - const files: IPromptFileDiscoveryResult[] = []; - - let result: IPromptDiscoveryInfo; - if (type === PromptsType.skill) { - result = await this.getSkillDiscoveryInfo(token); - } else if (type === PromptsType.agent) { - result = await this.getAgentDiscoveryInfo(token); - } else if (type === PromptsType.prompt) { - result = await this.getPromptSlashCommandDiscoveryInfo(token); - } else if (type === PromptsType.instructions) { - result = await this.getInstructionsDiscoveryInfo(token); - } else if (type === PromptsType.hook) { - result = await this.getHookDiscoveryInfo(token); - } else { - result = { type, files }; - } - - const loadedCount = result.files.filter(f => f.status === 'loaded').length; - const skippedCount = result.files.filter(f => f.status === 'skipped').length; - - // Add source folder diagnostics if not already present - if (!result.sourceFolders) { - const sourceFolders = await this._collectSourceFolderDiagnostics(type); - result = { ...result, sourceFolders }; - } - - if (sessionResource) { - const details = localize( - "promptsService.discoveryResult", - "{0} loaded, {1} skipped", - loadedCount, - skippedCount, - ); - this._onDidLogDiscovery.fire({ - sessionResource, - name: localize("promptsService.discoveryEnd", "Discovery {0} (End)", type), - details, - discoveryInfo: result, - category: 'discovery', - }); - } - return result; - } - private async getSkillDiscoveryInfo(token: CancellationToken): Promise { const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS); 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 131aafd3982..d9ef7bdde73 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 @@ -67,8 +67,6 @@ export class MockPromptsService implements IPromptsService { registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - getPromptDiscoveryInfo(_type: any, _token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any getHooks(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } getInstructionFiles(_token: CancellationToken, _sessionResource?: URI): Promise { throw new Error('Method not implemented.'); } dispose(): void { } 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 52884d9ad87..24374b4316e 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 @@ -3670,33 +3670,6 @@ suite('PromptsService', () => { assert.strictEqual(reTrustedResult.hooks[HookType.PreToolUse]?.length, 1); }); - test('discovery info marks hooks as skipped when workspace is untrusted', async function () { - workspaceContextService.setWorkspace(testWorkspace(URI.file('/test-workspace'))); - testConfigService.setUserConfiguration(PromptsConfig.USE_CHAT_HOOKS, true); - testConfigService.setUserConfiguration(PromptsConfig.HOOKS_LOCATION_KEY, { [HOOKS_SOURCE_FOLDER]: true }); - - await mockFiles(fileService, [ - { - path: '/test-workspace/.github/hooks/my-hook.json', - contents: [ - JSON.stringify({ - hooks: { - [HookType.PreToolUse]: [ - { type: 'command', command: 'echo test' }, - ], - }, - }), - ], - }, - ]); - - await workspaceTrustService.setWorkspaceTrust(false); - const discoveryInfo = await service.getPromptDiscoveryInfo(PromptsType.hook, CancellationToken.None); - assert.strictEqual(discoveryInfo.files.length, 1, 'Expected one discovery result'); - assert.strictEqual(discoveryInfo.files[0].status, 'skipped'); - assert.strictEqual(discoveryInfo.files[0].skipReason, 'workspace-untrusted'); - }); - test('suppresses plugin hooks when workspace is untrusted', async function () { testConfigService.setUserConfiguration(PromptsConfig.USE_CHAT_HOOKS, true); testConfigService.setUserConfiguration(PromptsConfig.HOOKS_LOCATION_KEY, {}); From 4863c500a866c2ea33870316c681fd32f368d523 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:11:42 +0100 Subject: [PATCH 447/448] Sessions - add prompt for the draft pull request (#300543) * Sessions - add prompt for the draft pull request * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/sessions/prompts/create-draft-pr.prompt.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/vs/sessions/prompts/create-draft-pr.prompt.md diff --git a/src/vs/sessions/prompts/create-draft-pr.prompt.md b/src/vs/sessions/prompts/create-draft-pr.prompt.md new file mode 100644 index 00000000000..b64c60e1ea7 --- /dev/null +++ b/src/vs/sessions/prompts/create-draft-pr.prompt.md @@ -0,0 +1,11 @@ +--- +description: Create a draft pull request for the current session +--- + + +Use the GitHub MCP server to create a draft pull request — do NOT use the `gh` CLI. + +1. Review all changes in the current session +2. Write a clear, concise PR title with a short area prefix (e.g. "sessions: …", "editor: …") +3. Write a description covering what changed, why, and anything reviewers should know +4. Create the draft pull request From a200e8b1663c0e1690fc1bc29c9690d0c9ab809b Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:43:53 -0700 Subject: [PATCH 448/448] ai-customizations: improve list visual scannability (#300551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ai-customizations: improve list visual scannability (#299211) - Add type-specific icon to each list item (agent, skill, instructions, prompt, hook) - Format item names: convert dashes/underscores to spaces and apply title case - Truncate descriptions to first sentence (max 120 chars fallback) to reduce visual noise - Make item name font-weight 500 so titles pop against secondary text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: add type icon + name/description polish for MCP servers and plugins (#299211) - Export formatDisplayName and truncateToFirstSentence helpers from aiCustomizationListWidget - Add mcpServerIcon to McpServerItemRenderer (local + builtin items) - Add pluginIcon to PluginInstalledItemRenderer - Apply formatDisplayName (dash/underscore → spaces, title case) to names - Apply truncateToFirstSentence to descriptions - Set font-weight: 500 on mcp-server-name to match AI customization list style - Remove left-indent padding on mcp-server-item now that the icon fills that space Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: mute group count badges (#299211) Replace badge-background/foreground pill styling with plain descriptionForeground text (opacity 0.8) on both the group-header count and the sidebar section count. This lets the section label dominate visually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: restore badge pill with reduced opacity (#299211) Keep badge-background/foreground colors but apply opacity: 0.6 so the pill is still visible but clearly secondary to the section label. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: use keybindingLabel tokens for count badges (#299211) Switch from badge-background (bright accent) to keybindingLabel-background/ foreground/border tokens, which are designed for subtle inline labels. No opacity hacks needed — the color itself is naturally muted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: use list.inactiveSelection tokens for count badges (#299211) Switch to list.inactiveSelectionBackground/Foreground — the semantically closest tokens for a secondary/muted count pill in a list/tree context. No opacity hacks needed and the name directly reflects the role. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: strip trailing .md from display names (#299211) formatDisplayName now strips a case-insensitive .md suffix before applying the title-case transform, so 'my-file.Md' no longer appears as a title. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: remove explicit font-weight from item titles (#299211) Drop the font-weight: 500 on item-name and mcp-server-name. The visual hierarchy is already established by the 13px full-foreground title vs the 11px muted descriptionForeground description below it, without needing an explicit weight bump. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: use star icon for built-in MCP server items (#299211) Built-in MCP items now show builtinIcon (star) instead of mcpServerIcon, consistent with the prompts built-in group. Icon is now set per-element in renderElement so the two item types can show different icons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "ai-customizations: use star icon for built-in MCP server items (#299211)" This reverts commit 6b08675a22a29dc8c36834232b46a0ace42a29a3. * ai-customizations: use star icon for Built-in MCP group header (#299211) Change the Built-in group header icon from extensionIcon to builtinIcon (starFull), consistent with the Built-in group in the prompts list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: remove unused extensionIcon import from mcpListWidget (#299211) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * prompts: add create-pr prompt with compile-check reminder Ensures the TypeScript compile check is run before opening a PR, catching unused import and type errors that tsgo would flag in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ai-customizations: fix IMatch highlight positions for formatted names (#299211) nameMatches are now computed against formatDisplayName(item.name) in filterItems so highlight positions align with the displayed string. The .md stripping in formatDisplayName changes string length, so matches against the raw name would produce incorrect highlight spans. Also removed the outdated '1:1 transformation' claim from the JSDoc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aiCustomizationListWidget.ts | 62 +++++++++++++++++-- .../browser/aiCustomization/mcpListWidget.ts | 19 +++--- .../media/aiCustomizationManagement.css | 24 ++++--- .../aiCustomization/pluginListWidget.ts | 11 +++- 4 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 2ae76f14240..96873795ec8 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -115,6 +115,7 @@ class AICustomizationListDelegate implements IListVirtualDelegate { interface IAICustomizationItemTemplateData { readonly container: HTMLElement; readonly actionsContainer: HTMLElement; + readonly typeIcon: HTMLElement; readonly nameLabel: HighlightedLabel; readonly description: HighlightedLabel; readonly disposables: DisposableStore; @@ -194,6 +195,47 @@ class GroupHeaderRenderer implements IListRenderer c.toUpperCase()); +} + +/** + * Truncates a description string to the first sentence, with a maximum character fallback. + */ +export function truncateToFirstSentence(text: string, maxChars = 120): string { + const match = text.match(/^[^.!?]*[.!?]/); + if (match && match[0].length <= maxChars) { + return match[0]; + } + if (text.length > maxChars) { + return text.substring(0, maxChars).trimEnd() + '\u2026'; + } + return text; +} + /** * Renderer for AI customization list items. */ @@ -212,6 +254,7 @@ class AICustomizationItemRenderer implements IListRenderer { const uriLabel = this.labelService.getUriLabel(element.uri, { relative: false }); @@ -245,11 +293,12 @@ class AICustomizationItemRenderer implements IListRenderer { interface IMcpServerItemTemplateData { readonly container: HTMLElement; + readonly typeIcon: HTMLElement; readonly name: HTMLElement; readonly description: HTMLElement; readonly status: HTMLElement; @@ -113,13 +115,16 @@ class McpServerItemRenderer implements IListRenderer { interface IPluginInstalledItemTemplateData { readonly container: HTMLElement; + readonly typeIcon: HTMLElement; readonly name: HTMLElement; readonly description: HTMLElement; readonly status: HTMLElement; @@ -112,21 +114,24 @@ class PluginInstalledItemRenderer implements IListRenderer