From 5cf238cecf88790cef65cb5720cbd2928e1debb9 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 6 May 2026 15:55:35 +0100 Subject: [PATCH 01/28] add selected foreground and icon foreground colors for editor suggest widget --- extensions/theme-defaults/themes/2026-light.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/theme-defaults/themes/2026-light.json b/extensions/theme-defaults/themes/2026-light.json index 76b2e09f3176be..40ee59eb2bc8fc 100644 --- a/extensions/theme-defaults/themes/2026-light.json +++ b/extensions/theme-defaults/themes/2026-light.json @@ -143,6 +143,8 @@ "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", + "editorSuggestWidget.selectedForeground": "#202020", + "editorSuggestWidget.selectedIconForeground": "#202020", "editorHoverWidget.background": "#FAFAFD", "editorHoverWidget.border": "#E4E5E6FF", "peekView.border": "#0069CC", From 644c7175527c3401967887329956a73085b80fab Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Wed, 6 May 2026 10:48:29 -0700 Subject: [PATCH 02/28] Guard terminal resize/dispose race against xterm.js dimension getters After bumping xterm.js to beta.213 (#313817), three error buckets keep firing the same Cannot read properties of undefined (reading 'dimensions') TypeError on the latest insider builds (1.120.0-insider 79d6cea3 / 0aed0a9b): - aaa283a2 - resize -> _resizeYCallback -> _updatePtyDimensions, triggered by config-change -> setVisible -> _resize. - 4826565b - debounced _debounceResizeX timer firing after the renderer is gone. - 1e83a096 - _onProcessExit -> dispose -> setVisible(false) -> _resize -> _resizeBothCallback. All three reach RenderService.get dimensions() in xterm.js, which dereferences this._renderer.value (undefined post-dispose) and throws synchronously. The optional chaining on rawXterm.dimensions doesn't help because the getter itself throws. The upstream beta.213 fix added isDisposed guards at the OverviewRuler call sites, not inside the getter, so the correct symmetrical fix here is to guard the call sites that VS Code owns: * terminalInstance.ts: bail out of the resize closures and _updatePtyDimensions when the instance is already disposed; defensively wrap the dimensions read in try/catch so a future renderer-dispose race fails silently instead of bubbling as an unhandled error. * terminalResizeDebouncer.ts: bail out of resize/flush and the runWhenWindowIdle / @debounce-scheduled callbacks when the debouncer's store is disposed - the @debounce decorator schedules a setTimeout that isn't tied to the disposable store. Refs microsoft/vscode#303546, microsoft/vscode#313817 --- .../terminal/browser/terminalInstance.ts | 27 +++++++++++++++++-- .../browser/terminalResizeDebouncer.ts | 18 +++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index a14f0adcfe9e9b..1499006feaac85 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -816,14 +816,23 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { () => this._isVisible, () => xterm, async (cols, rows) => { + if (this.isDisposed) { + return; + } xterm.resize(cols, rows); await this._updatePtyDimensions(xterm.raw); }, async (cols) => { + if (this.isDisposed) { + return; + } xterm.resize(cols, xterm.raw.rows); await this._updatePtyDimensions(xterm.raw); }, async (rows) => { + if (this.isDisposed) { + return; + } xterm.resize(xterm.raw.cols, rows); await this._updatePtyDimensions(xterm.raw); } @@ -2047,8 +2056,22 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _updatePtyDimensions(rawXterm: XTermTerminal): Promise { - const pixelWidth = rawXterm.dimensions?.css.canvas.width; - const pixelHeight = rawXterm.dimensions?.css.canvas.height; + if (this.isDisposed) { + return; + } + // `rawXterm.dimensions` proxies to xterm.js' RenderService which throws + // `Cannot read properties of undefined (reading 'dimensions')` if the + // renderer was disposed between scheduling and invocation (debounced or + // idle resize callbacks racing with terminal teardown). Guard against + // that here so the optional chaining short-circuits to undefined. + let pixelWidth: number | undefined; + let pixelHeight: number | undefined; + try { + pixelWidth = rawXterm.dimensions?.css.canvas.width; + pixelHeight = rawXterm.dimensions?.css.canvas.height; + } catch { + // Renderer disposed mid-flight; fall through with undefined dimensions. + } const roundedPixelWidth = pixelWidth ? Math.round(pixelWidth) : undefined; const roundedPixelHeight = pixelHeight ? Math.round(pixelHeight) : undefined; await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows, undefined, roundedPixelWidth, roundedPixelHeight); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts b/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts index 8afe11348cc2bf..88062ff24fb7f9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts @@ -33,6 +33,9 @@ export class TerminalResizeDebouncer extends Disposable { } async resize(cols: number, rows: number, immediate: boolean): Promise { + if (this._store.isDisposed) { + return; + } this._latestX = cols; this._latestY = rows; @@ -49,12 +52,18 @@ export class TerminalResizeDebouncer extends Disposable { if (win && !this._isVisible()) { if (!this._resizeXJob.value) { this._resizeXJob.value = runWhenWindowIdle(win, async () => { + if (this._store.isDisposed) { + return; + } this._resizeXCallback(this._latestX); this._resizeXJob.clear(); }); } if (!this._resizeYJob.value) { this._resizeYJob.value = runWhenWindowIdle(win, async () => { + if (this._store.isDisposed) { + return; + } this._resizeYCallback(this._latestY); this._resizeYJob.clear(); }); @@ -70,6 +79,9 @@ export class TerminalResizeDebouncer extends Disposable { } flush(): void { + if (this._store.isDisposed) { + return; + } if (this._resizeXJob.value || this._resizeYJob.value) { this._resizeXJob.clear(); this._resizeYJob.clear(); @@ -79,6 +91,12 @@ export class TerminalResizeDebouncer extends Disposable { @debounce(100) private _debounceResizeX(cols: number) { + // The @debounce decorator schedules a setTimeout that is not tied to the + // disposable store, so this can fire after the terminal/xterm renderer is + // disposed. Bail out to avoid throwing from xterm.js dimension getters. + if (this._store.isDisposed) { + return; + } this._resizeXCallback(cols); } } From fc2e884c744fe488808136a40efcf57a60f16df9 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 6 May 2026 23:17:34 +0200 Subject: [PATCH 03/28] enable extension in sessions window even though they would have been disabled --- .../browser/extensions.contribution.ts | 19 +++++++ .../extensionEnablementService.test.ts | 20 +++++++- .../extensionManifestPropertiesService.ts | 24 +++++++++ ...extensionManifestPropertiesService.test.ts | 49 ++++++++++++++++++- 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b60f49efcf557b..60db6589ab1116 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -56,6 +56,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkspaceExtensionsConfigService } from '../../../services/extensionRecommendations/common/workspaceExtensionsConfig.js'; +import { EXTENSIONS_SUPPORT_SESSIONS_WINDOW } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; @@ -214,6 +215,24 @@ Registry.as(ConfigurationExtensions.Configuration) } }] }, + [EXTENSIONS_SUPPORT_SESSIONS_WINDOW]: { + type: 'object', + scope: ConfigurationScope.APPLICATION, + markdownDescription: localize('extensions.supportSessionsWindow', "Override the Agents window support of an extension. Extensions using `true` will be enabled in the Agents window even when they would otherwise be disabled."), + patternProperties: { + '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + type: 'boolean', + default: false + } + }, + additionalProperties: false, + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': true + } + }] + }, 'extensions.experimental.affinity': { type: 'object', markdownDescription: localize('extensions.affinity', "Configure an extension to execute in a different extension host process."), diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 4c3d2f745c308e..4a23062fc708c3 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -30,7 +30,7 @@ import { IHostService } from '../../../host/browser/host.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { IExtensionBisectService } from '../../browser/extensionBisect.js'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, WorkspaceTrustRequestOptions } from '../../../../../platform/workspace/common/workspaceTrust.js'; -import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from '../../../extensions/common/extensionManifestPropertiesService.js'; +import { EXTENSIONS_SUPPORT_SESSIONS_WINDOW, ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from '../../../extensions/common/extensionManifestPropertiesService.js'; import { TestChatEntitlementService, TestContextService, TestProductService, TestWorkspaceTrustEnablementService, TestWorkspaceTrustManagementService } from '../../../../test/common/workbenchTestServices.js'; import { TestWorkspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; import { ExtensionManagementService } from '../../common/extensionManagementService.js'; @@ -98,7 +98,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.stub(IAllowedExtensionsService, disposables.add(new AllowedExtensionsService(instantiationService.get(IProductService), instantiationService.get(IConfigurationService)))), workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, - instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, disposables.add(new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustEnablementService(), new NullLogService()))), + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, disposables.add(new ExtensionManifestPropertiesService(TestProductService, instantiationService.get(IConfigurationService), new TestWorkspaceTrustEnablementService(), new NullLogService()))), chatEntitlementService ?? new TestChatEntitlementService(), instantiationService, new NullLogService(), @@ -1266,6 +1266,22 @@ suite('ExtensionEnablementService Test', () => { ]); }); + test('test configured extensions are enabled in sessions window', async () => { + await (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(EXTENSIONS_SUPPORT_SESSIONS_WINDOW, { 'pub.withMain': true, 'pub.nonThemeContrib': true }); + instantiationService.stub(IWorkbenchEnvironmentService, { isSessionsWindow: true }); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + + const withMain = aLocalExtension2('pub.withMain', { main: 'main.js', contributes: aContributes('themes') }); + const nonThemeContrib = aLocalExtension2('pub.nonThemeContrib', { contributes: aContributes('commands') }); + const withBrowser = aLocalExtension2('pub.withBrowser', { browser: 'main.browser.js', contributes: aContributes('themes') }); + + assert.deepStrictEqual([withMain, nonThemeContrib, withBrowser].map(ext => testObject.getEnablementState(ext)), [ + EnablementState.EnabledGlobally, + EnablementState.EnabledGlobally, + EnablementState.DisabledByEnvironment, + ]); + }); + test('test extensions are not disabled in non-sessions window', () => { const withMain = aLocalExtension2('pub.withMain', { main: 'main.js' }); const withBrowser = aLocalExtension2('pub.withBrowser', { browser: 'main.browser.js' }); diff --git a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts index d647721265701e..0fe9ce70ee097f 100644 --- a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts +++ b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -22,6 +22,8 @@ import { isWeb } from '../../../../base/common/platform.js'; export const IExtensionManifestPropertiesService = createDecorator('extensionManifestPropertiesService'); +export const EXTENSIONS_SUPPORT_SESSIONS_WINDOW = 'extensions.supportSessionsWindow'; + const SESSIONS_WINDOW_ALLOWED_CONTRIBUTION_POINTS: ReadonlySet = new Set([ 'themes', 'iconThemes', @@ -62,6 +64,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE private _productVirtualWorkspaceSupportMap: ExtensionIdentifierMap<{ default?: boolean; override?: boolean }> | null = null; private _configuredVirtualWorkspaceSupportMap: ExtensionIdentifierMap | null = null; + private _configuredSessionsWindowSupportMap: ExtensionIdentifierMap | null = null; private readonly _configuredExtensionWorkspaceTrustRequestMap: ExtensionIdentifierMap<{ supported: ExtensionUntrustedWorkspaceSupportType; version?: string }>; private readonly _productExtensionWorkspaceTrustRequestMap: Map; @@ -91,6 +94,11 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE } canExecuteOnSessionsWindow(manifest: IExtensionManifest): boolean { + const configuredSessionsWindowSupport = this.getConfiguredSessionsWindowSupport(manifest); + if (configuredSessionsWindowSupport !== undefined) { + return configuredSessionsWindowSupport; + } + // In the sessions window only extensions that have no code are currently allowed to run if (manifest.main || manifest.browser) { return false; @@ -371,6 +379,22 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE return this._configuredVirtualWorkspaceSupportMap.get(extensionId); } + private getConfiguredSessionsWindowSupport(manifest: IExtensionManifest): boolean | undefined { + if (this._configuredSessionsWindowSupportMap === null) { + const configuredSessionsWindowSupportMap = new ExtensionIdentifierMap(); + const configuredSessionsWindowSupport = this.configurationService.getValue<{ [key: string]: boolean }>(EXTENSIONS_SUPPORT_SESSIONS_WINDOW) || {}; + for (const id of Object.keys(configuredSessionsWindowSupport)) { + if (configuredSessionsWindowSupport[id] !== undefined) { + configuredSessionsWindowSupportMap.set(id, configuredSessionsWindowSupport[id]); + } + } + this._configuredSessionsWindowSupportMap = configuredSessionsWindowSupportMap; + } + + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._configuredSessionsWindowSupportMap.get(extensionId); + } + private getConfiguredExtensionWorkspaceTrustRequest(manifest: IExtensionManifest): ExtensionUntrustedWorkspaceSupportType | undefined { const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); const extensionWorkspaceTrustRequest = this._configuredExtensionWorkspaceTrustRequestMap.get(extensionId); diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index f822c23a330af3..1eb0eb3893df54 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -14,7 +14,7 @@ import { TestInstantiationService } from '../../../../../platform/instantiation/ import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IWorkspaceTrustEnablementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; -import { ExtensionManifestPropertiesService } from '../../common/extensionManifestPropertiesService.js'; +import { EXTENSIONS_SUPPORT_SESSIONS_WINDOW, ExtensionManifestPropertiesService } from '../../common/extensionManifestPropertiesService.js'; import { TestProductService, TestWorkspaceTrustEnablementService } from '../../../../test/common/workbenchTestServices.js'; suite('ExtensionManifestPropertiesService - ExtensionKind', () => { @@ -109,6 +109,53 @@ suite('ExtensionManifestPropertiesService - ExtensionKind', () => { }); }); +suite('ExtensionManifestPropertiesService - SessionsWindowSupport', () => { + + let disposables: DisposableStore; + let testConfigurationService: TestConfigurationService; + let testObject: ExtensionManifestPropertiesService; + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + disposables = new DisposableStore(); + testConfigurationService = new TestConfigurationService(); + }); + + teardown(() => { + testObject.dispose(); + disposables.dispose(); + }); + + function getExtensionManifest(properties: Partial = {}): IExtensionManifest { + return Object.create({ name: 'a', publisher: 'pub', version: '1.0.0', ...properties }) as IExtensionManifest; + } + + function createTestObject(): ExtensionManifestPropertiesService { + return disposables.add(new ExtensionManifestPropertiesService(TestProductService, testConfigurationService, new TestWorkspaceTrustEnablementService(), new NullLogService())); + } + + test('defaults to declarative extensions without executable code and supported contributions', () => { + testObject = createTestObject(); + + assert.deepStrictEqual([ + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ contributes: { themes: [] } })), + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ main: './out/extension.js', contributes: { themes: [] } })), + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ contributes: { commands: [] } })), + ], [true, false, false]); + }); + + test('uses configured sessions window support override', async () => { + await testConfigurationService.setUserConfiguration(EXTENSIONS_SUPPORT_SESSIONS_WINDOW, { 'pub.a': true, 'pub.b': false }); + testObject = createTestObject(); + + assert.deepStrictEqual([ + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ main: './out/extension.js', contributes: { commands: [] } })), + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ name: 'b', contributes: { themes: [] } })), + ], [true, false]); + }); +}); + // Workspace Trust is disabled in web at the moment if (!isWeb) { From 1936c06af216b9a34bcf46050db0e13c77d6f178 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 6 May 2026 23:46:28 -0700 Subject: [PATCH 04/28] Pick up latest TS native preview for building VS Code --- package-lock.json | 62 +++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4261db48d62390..fdd44c171bd9b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3424,9 +3424,9 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-SGKnvs5EA+V1spnraYJqum/lEajE0IQ2bVVPC72hFfWjoCfQ6N7iVYxLUGreiE3VFyQWWQBPgXZrRUFnawVvpQ==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-UcEslgHBaHYPAisVQcyARDfps7nKyugmUyXcsfE1HiHcVuvZ4tBJ5C93sG1FDeHWJ9skGQ68ec+Xsx086geAfg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3436,19 +3436,19 @@ "node": ">=16.20.0" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20260429.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20260429.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260506.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260506.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-+Rl8iPf+vYKq0fnb8euEOJxxvE/abEOWmhdllQIe+Shd8xhS7UVi+2WunsP1GyH2Ofc+N8rGYz0/dMnhrRYEZA==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-dAd7qG2J508+4CRSuoEA0EUxViIedQ0D+8xKoZiM0EQHCwww8glWYCo72UTjcRZctS3QbJY3PtGSvo3nzL4oVw==", "cpu": [ "arm64" ], @@ -3463,9 +3463,9 @@ } }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-be6Y7VVJz+usdI1ifCHy5mcldpxf8KXGYoyIp8w5Rd54zUtvtkYEJJWKzV5/bJt4bsQLLcp1i0vD4KJSr06Tmg==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-1Q7Elncpuiozvx3HCTgFbSxNz2m2FIkO1QW5f15igcZDG3vMW4QglNflmXosc69bzYI7KfYZuaGX3yGzJkGbfg==", "cpu": [ "x64" ], @@ -3480,9 +3480,9 @@ } }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-ngN6+qt5bPdp2zzasShoT4UONGXr+tvzHdz4NjuitwhiAF/d70CseXunb4syaudl1a+lJyTHro/ALTC0hRf6vA==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-MfYn1p+aOorZ2Y+7sqLvSoAXPEz/RfKgHfeYO240Udco30B4oapm7Hsq2PsS9Z2Oth/RorGjY0jLP2OhnkY2Ig==", "cpu": [ "arm" ], @@ -3497,9 +3497,9 @@ } }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-44amAEH/VxG6K/hrAmhiyOTnwoTzm7bj0ja7d8sV8Iuocv37oUiSB/8OgJLytLqfIh+Q6kipfTwY6Do3jh6THQ==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-Q1W4DHplR2urmtPwoz9tw6XUGWRNXF+CIXJQ8ZpIZFj/OHgvTw8vkYkKFuaEao3lSjTsR4lQe/wL2Xr5K0hxuA==", "cpu": [ "arm64" ], @@ -3514,9 +3514,9 @@ } }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-haAOqc0fJCZkt4RDi0/ZQGBdDfpDzr2N+mEcR+FbiYQD3Y00kOK34hXSrjZafO2kq56ZDWunvCaUTCev0fJDbA==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-b+sbLBCIchbrGQNbjIvVN2qd+ieqqp/nghi0n2zOAKGPsfd5wG6ceqxWJKADdBDCohsCCGt//rZccUwFugIsyA==", "cpu": [ "x64" ], @@ -3531,9 +3531,9 @@ } }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-J5O0tGVGqOZHbqm9ijRnZ5ADfPqYTjFIwZtYKpQL1yj1dZnUzMszO8P3bnOSfYD//DJhZINQyJzpPJxu29uiwQ==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-l59d8pZjFT7GoWpgCOy6aBcxLSALphA91X4Z/2XHo5HnM0bQ/yJjB7XMeUQZBdk5DZCdZL+sWTfmXLRggm7sFg==", "cpu": [ "arm64" ], @@ -3548,9 +3548,9 @@ } }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-/OZ99Hi/32huvZQ5fdqTwqLvZtKC3QrCXmLuKfMyVuBisV/TSd6LhlFQLolvIpr7/E530mnFZ4sXjgDEzVFqAw==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-dJDLSzaz2xjRYYmTSfcCepZUi3ITjQSJ6Gk5YGplMF57UmZCAGI+ns4Te/V74IJiQigXqTnyEIGorwsOqhW8gQ==", "cpu": [ "x64" ], From 6ef87aeef3cd51b8bbb0fdbf6d3b58f4158e21fd Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 6 May 2026 23:48:07 -0700 Subject: [PATCH 05/28] Switch back to using parcel's watcher for esbuild Esbuild's watcher (maybe go's?) is having continual 2-5% cpu usage even when nothing is happening. With 15 extensions, this is causing too much load compared to parcel --- extensions/esbuild-common.mts | 80 +++++++++++++++++++++++++ extensions/esbuild-extension-common.mts | 64 ++------------------ extensions/esbuild-webview-common.mts | 79 ++++-------------------- 3 files changed, 99 insertions(+), 124 deletions(-) create mode 100644 extensions/esbuild-common.mts diff --git a/extensions/esbuild-common.mts b/extensions/esbuild-common.mts new file mode 100644 index 00000000000000..8747583ef5e8af --- /dev/null +++ b/extensions/esbuild-common.mts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import path from 'node:path'; +import esbuild from 'esbuild'; + +export interface RunConfig { + readonly srcDir: string; + readonly outdir: string; + readonly entryPoints: esbuild.BuildOptions['entryPoints']; + readonly additionalOptions?: Partial; +} + +/** + * Shared build/watch runner for extension esbuild scripts. + */ +export async function runBuild( + config: RunConfig, + baseOptions: esbuild.BuildOptions, + args: string[], + didBuild?: (outDir: string) => unknown, +): Promise { + let outdir = config.outdir; + const outputRootIndex = args.indexOf('--outputRoot'); + if (outputRootIndex >= 0) { + const outputRoot = args[outputRootIndex + 1]; + const outputDirName = path.basename(outdir); + outdir = path.join(outputRoot, outputDirName); + } + + const resolvedOptions: esbuild.BuildOptions = { + ...baseOptions, + entryPoints: config.entryPoints, + outdir, + ...(config.additionalOptions || {}), + }; + + const isWatch = args.indexOf('--watch') >= 0; + if (isWatch) { + const ctx = await esbuild.context(resolvedOptions); + await watchWithParcel(ctx, config.srcDir, () => didBuild?.(outdir)); + } else { + try { + await esbuild.build(resolvedOptions); + await didBuild?.(outdir); + } catch { + process.exit(1); + } + } +} + +// We use @parcel/watcher as it has much lower cpu usage when idle compared to esbuild's watch mode +async function watchWithParcel(ctx: esbuild.BuildContext, srcDir: string, didBuild?: () => Promise | unknown): Promise { + let debounce: ReturnType | undefined; + const rebuild = () => { + if (debounce) { + clearTimeout(debounce); + } + debounce = setTimeout(async () => { + try { + await ctx.cancel(); + const result = await ctx.rebuild(); + if (result.errors.length === 0) { + await didBuild?.(); + } + } catch (error) { + console.error('[watch] build error:', error); + } + }, 100); + }; + + const watcher = await import('@parcel/watcher'); + await watcher.subscribe(srcDir, (_err, _events) => { + rebuild(); + }, { + ignore: ['**/node_modules/**', '**/dist/**', '**/out/**'] + }); + rebuild(); +} diff --git a/extensions/esbuild-extension-common.mts b/extensions/esbuild-extension-common.mts index cfbce204b66b0c..5b967a9140e747 100644 --- a/extensions/esbuild-extension-common.mts +++ b/extensions/esbuild-extension-common.mts @@ -5,24 +5,16 @@ /** * @fileoverview Common build script for extensions. */ -import path from 'node:path'; import esbuild from 'esbuild'; +import { runBuild, type RunConfig } from './esbuild-common.mts'; -type BuildOptions = Partial & { - outdir: string; -}; - -interface RunConfig { +interface ExtensionRunConfig extends RunConfig { readonly platform: 'node' | 'browser'; readonly format?: 'cjs' | 'esm'; - readonly srcDir: string; - readonly outdir: string; - readonly entryPoints: string[] | Record | { in: string; out: string }[]; - readonly additionalOptions?: Partial; } -function resolveOptions(config: RunConfig, outdir: string): BuildOptions { - const options: BuildOptions = { +function resolveBaseOptions(config: ExtensionRunConfig): esbuild.BuildOptions { + const options: esbuild.BuildOptions = { platform: config.platform, bundle: true, minify: true, @@ -31,12 +23,9 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { target: ['es2024'], external: ['vscode'], format: config.format ?? 'cjs', - entryPoints: config.entryPoints, - outdir, logOverride: { 'import-is-undefined': 'error', }, - ...(config.additionalOptions || {}), }; if (config.platform === 'node') { @@ -56,47 +45,6 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { return options; } -export async function run(config: RunConfig, args: string[], didBuild?: (outDir: string) => unknown): Promise { - let outdir = config.outdir; - const outputRootIndex = args.indexOf('--outputRoot'); - if (outputRootIndex >= 0) { - const outputRoot = args[outputRootIndex + 1]; - const outputDirName = path.basename(outdir); - outdir = path.join(outputRoot, outputDirName); - } - - const resolvedOptions = resolveOptions(config, outdir); - - const isWatch = args.indexOf('--watch') >= 0; - if (isWatch) { - if (didBuild) { - resolvedOptions.plugins = [ - ...(resolvedOptions.plugins || []), - { - name: 'did-build', setup(pluginBuild) { - pluginBuild.onEnd(async result => { - if (result.errors.length > 0) { - return; - } - - try { - await didBuild(outdir); - } catch (error) { - console.error('didBuild failed:', error); - } - }); - }, - } - ]; - } - const ctx = await esbuild.context(resolvedOptions); - await ctx.watch(); - } else { - try { - await esbuild.build(resolvedOptions); - await didBuild?.(outdir); - } catch { - process.exit(1); - } - } +export async function run(config: ExtensionRunConfig, args: string[], didBuild?: (outDir: string) => unknown): Promise { + return runBuild(config, resolveBaseOptions(config), args, didBuild); } diff --git a/extensions/esbuild-webview-common.mts b/extensions/esbuild-webview-common.mts index 7e7bbe60ec412d..294f9f6bd41288 100644 --- a/extensions/esbuild-webview-common.mts +++ b/extensions/esbuild-webview-common.mts @@ -6,77 +6,24 @@ /** * Common build script for extension scripts used in in webviews. */ -import path from 'node:path'; -import esbuild from 'esbuild'; +import { runBuild, type RunConfig } from './esbuild-common.mts'; -export type BuildOptions = Partial & { - readonly entryPoints: esbuild.BuildOptions['entryPoints']; - readonly outdir: string; +const baseOptions = { + bundle: true, + minify: true, + sourcemap: false, + format: 'esm' as const, + platform: 'browser' as const, + target: ['es2024'], + logOverride: { + 'import-is-undefined': 'error', + }, }; export async function run( - config: { - srcDir: string; - outdir: string; - entryPoints: BuildOptions['entryPoints']; - additionalOptions?: Partial; - }, + config: RunConfig, args: string[], didBuild?: (outDir: string) => unknown ): Promise { - let outdir = config.outdir; - const outputRootIndex = args.indexOf('--outputRoot'); - if (outputRootIndex >= 0) { - const outputRoot = args[outputRootIndex + 1]; - const outputDirName = path.basename(outdir); - outdir = path.join(outputRoot, outputDirName); - } - - const resolvedOptions: BuildOptions = { - bundle: true, - minify: true, - sourcemap: false, - format: 'esm', - platform: 'browser', - target: ['es2024'], - entryPoints: config.entryPoints, - outdir, - logOverride: { - 'import-is-undefined': 'error', - }, - ...(config.additionalOptions || {}), - }; - - const isWatch = args.indexOf('--watch') >= 0; - if (isWatch) { - if (didBuild) { - resolvedOptions.plugins = [ - ...(resolvedOptions.plugins || []), - { - name: 'did-build', setup(pluginBuild) { - pluginBuild.onEnd(async result => { - if (result.errors.length > 0) { - return; - } - - try { - await didBuild(outdir); - } catch (error) { - console.error('didBuild failed:', error); - } - }); - }, - } - ]; - } - const ctx = await esbuild.context(resolvedOptions); - await ctx.watch(); - } else { - try { - await esbuild.build(resolvedOptions); - await didBuild?.(outdir); - } catch { - process.exit(1); - } - } + return runBuild(config, baseOptions, args, didBuild); } From 24ae24bb0e98a262e048f9f6477e858fe51649a9 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 7 May 2026 16:49:55 +1000 Subject: [PATCH 06/28] sessions: fix duplicate session in list when new session is opened while another is active (#314913) sessions: fix duplicate session in list when new session opened while another is active When a new CLI session was created and the user opened another session before the new one fully graduated: 1. SessionAdded notification from the agent host arrived before _pendingSession was set, so _sessionCache already contained the committed session when the skeleton was added as _pendingSession. getSessions() then returned both, causing the duplicate row. 2. SessionsManagementService.onDidReplaceSession only fired _onDidChangeSessions when the active session was the 'from' session. If the user had navigated away, no refresh event fired and the SessionsList kept showing the duplicate indefinitely. Fix: - Clear _pendingSession before firing _onDidReplaceSession so any synchronous listener that calls getSessions() sees only the committed session (the finally block still clears it on the failure path). - Always fire _onDidChangeSessions in onDidReplaceSession; only the active-session reassignment remains conditional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/baseAgentHostSessionsProvider.ts | 6 ++++++ .../sessions/browser/sessionsManagementService.ts | 13 ++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts index 70e37d5fcf15ca..be602b61d49936 100644 --- a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -1401,12 +1401,18 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement if (this._newSession === newSession) { this._newSession = undefined; } + // Clear the pending session before firing the replace event so + // that any synchronous listener calling getSessions() sees only + // the committed session and not both. + this._pendingSession = undefined; this._onDidReplaceSession.fire({ from: skeleton, to: committedSession }); return committedSession; } } catch { // Connection lost or timeout — fall through to the failure cleanup. } finally { + // Defensive clear: covers the failure path where the try block + // never reached the explicit clear above. this._pendingSession = undefined; } diff --git a/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts index 95b679003a455e..065515701a963e 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts @@ -127,12 +127,15 @@ class SessionsManagementService extends Disposable implements ISessionsManagemen private onDidReplaceSession(from: ISession, to: ISession): void { if (this._activeSession.get()?.sessionId === from.sessionId) { this.setActiveSession(to); - this._onDidChangeSessions.fire({ - added: [], - removed: from.sessionId === to.sessionId ? [] : [from], - changed: [to], - }); } + // Always fire the change event so the SessionsList refreshes even when + // the user navigated to a different session while the new one was + // being created (which is how duplicate rows appeared in the list). + this._onDidChangeSessions.fire({ + added: [], + removed: from.sessionId === to.sessionId ? [] : [from], + changed: [to], + }); } private onDidChangeSessionsFromSessionsProviders(e: ISessionChangeEvent): void { From ce78d28dc2b0702c522804ed4790d4139c4b8409 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 6 May 2026 23:50:38 -0700 Subject: [PATCH 07/28] Move test file to correct layer to fix build break --- .../chat/test/{ => common}/chatQuotaServiceImpl.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename extensions/copilot/src/platform/chat/test/{ => common}/chatQuotaServiceImpl.spec.ts (98%) diff --git a/extensions/copilot/src/platform/chat/test/chatQuotaServiceImpl.spec.ts b/extensions/copilot/src/platform/chat/test/common/chatQuotaServiceImpl.spec.ts similarity index 98% rename from extensions/copilot/src/platform/chat/test/chatQuotaServiceImpl.spec.ts rename to extensions/copilot/src/platform/chat/test/common/chatQuotaServiceImpl.spec.ts index 1dac4cd8ffdbc7..ca06e0d01fba0f 100644 --- a/extensions/copilot/src/platform/chat/test/chatQuotaServiceImpl.spec.ts +++ b/extensions/copilot/src/platform/chat/test/common/chatQuotaServiceImpl.spec.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, test } from 'vitest'; -import { Emitter } from '../../../util/vs/base/common/event'; -import { IAuthenticationService } from '../../authentication/common/authentication'; -import { ChatQuotaService } from '../common/chatQuotaServiceImpl'; +import { Emitter } from '../../../../util/vs/base/common/event'; +import { IAuthenticationService } from '../../../authentication/common/authentication'; +import { ChatQuotaService } from '../../common/chatQuotaServiceImpl'; function createMockAuthService(): IAuthenticationService { return { From 1f3ae3b7c79924e16e72b497c7b80d6a6cd2b8d4 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 7 May 2026 08:58:51 +0200 Subject: [PATCH 08/28] fix test --- .../test/common/extensionManifestPropertiesService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index 1eb0eb3893df54..6147ec33ac54d4 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -115,8 +115,6 @@ suite('ExtensionManifestPropertiesService - SessionsWindowSupport', () => { let testConfigurationService: TestConfigurationService; let testObject: ExtensionManifestPropertiesService; - ensureNoDisposablesAreLeakedInTestSuite(); - setup(() => { disposables = new DisposableStore(); testConfigurationService = new TestConfigurationService(); @@ -127,6 +125,8 @@ suite('ExtensionManifestPropertiesService - SessionsWindowSupport', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + function getExtensionManifest(properties: Partial = {}): IExtensionManifest { return Object.create({ name: 'a', publisher: 'pub', version: '1.0.0', ...properties }) as IExtensionManifest; } From 2839bf53557dd2cabe1bf41445659dd93f6ad980 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 7 May 2026 00:00:08 -0700 Subject: [PATCH 09/28] Disable unified quick access when AI features are disabled --- src/vs/workbench/browser/actions/quickAccessActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index fe18d7a076f70e..734412e75b3ad3 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -179,7 +179,8 @@ registerAction2(class QuickAccessAction extends Action2 { const configurationService = accessor.get(IConfigurationService); const commandService = accessor.get(ICommandService); - const useUnifiedQuickAccess = configurationService.getValue(UNIFIED_AGENTS_BAR_SETTING) === true; + const aiFeaturesDisabled = configurationService.getValue('chat.disableAIFeatures') === true; + const useUnifiedQuickAccess = !aiFeaturesDisabled && configurationService.getValue(UNIFIED_AGENTS_BAR_SETTING) === true; if (useUnifiedQuickAccess) { try { await commandService.executeCommand('workbench.action.unifiedQuickAccess'); From fbdf54506e9f6b9ebe6f6a0e9d7a185f28f28f0b Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 7 May 2026 00:16:39 -0700 Subject: [PATCH 10/28] Add document diff api proposal This adds a propose api for computing the diff between two files. Custom diff editors can use this to make sure they have the same diffs that VS Code would use in it's built-in diff editor --- .../markdown-language-features/package.json | 3 +- .../src/preview/lineDiff.ts | 362 ++---------------- .../markdown-language-features/tsconfig.json | 3 +- .../common/extensionsApiProposals.ts | 3 + .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadDocumentDiff.ts | 63 +++ .../workbench/api/common/extHost.api.impl.ts | 56 ++- .../workbench/api/common/extHost.protocol.ts | 24 ++ .../vscode.proposed.documentDiff.d.ts | 145 +++++++ 9 files changed, 321 insertions(+), 339 deletions(-) create mode 100644 src/vs/workbench/api/browser/mainThreadDocumentDiff.ts create mode 100644 src/vscode-dts/vscode.proposed.documentDiff.d.ts diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 1e2a3253a1b58f..5736e5fbdf37aa 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -8,7 +8,8 @@ "license": "MIT", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "enabledApiProposals": [ - "customEditorDiffs" + "customEditorDiffs", + "documentDiff" ], "engines": { "vscode": "^1.70.0" diff --git a/extensions/markdown-language-features/src/preview/lineDiff.ts b/extensions/markdown-language-features/src/preview/lineDiff.ts index 3a9eda944bd698..a842b4ff64aa5b 100644 --- a/extensions/markdown-language-features/src/preview/lineDiff.ts +++ b/extensions/markdown-language-features/src/preview/lineDiff.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { API as GitAPI, GitExtension, Repository as GitRepository } from '../../../git/src/api/git'; import type { MarkdownPreviewLineChanges } from '../../types/previewMessaging'; interface LineChanges { @@ -19,17 +18,6 @@ interface LineMappings { readonly modifiedToOriginal: number[]; } -interface GitUriParams { - readonly path: string; - readonly ref: string; - readonly submoduleOf?: string; -} - -interface GitPatch { - readonly patch: string; - readonly isFullRepositoryDiff: boolean; -} - export class MarkdownPreviewLineDiffProvider { readonly #originalDocument: vscode.TextDocument; @@ -77,348 +65,50 @@ export class MarkdownPreviewLineDiffProvider { } async function computeLineChanges(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument): Promise { - return await computeGitLineChanges(originalDocument, modifiedDocument) - ?? computeContentLineChanges(getDocumentLines(originalDocument), getDocumentLines(modifiedDocument)); -} - -async function computeGitLineChanges(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument): Promise { - const gitApi = await getGitApi(); - if (!gitApi) { - return undefined; - } - - const originalUri = originalDocument.uri; - const modifiedUri = modifiedDocument.uri; - const originalGitUri = fromGitUri(originalUri); - const modifiedGitUri = fromGitUri(modifiedUri); - const filePath = originalGitUri?.path ?? modifiedGitUri?.path ?? (modifiedUri.scheme === 'file' ? modifiedUri.fsPath : undefined); - if (!filePath || originalGitUri?.submoduleOf || modifiedGitUri?.submoduleOf) { - return undefined; - } - - const repository = gitApi.getRepository(vscode.Uri.file(filePath)); - if (!repository) { - return undefined; - } - - const diff = await getGitPatch(repository, filePath, originalUri, originalGitUri, modifiedUri, modifiedGitUri); - if (!diff) { - return undefined; - } - - const relativePath = diff.isFullRepositoryDiff ? getRepositoryRelativePath(repository.rootUri, filePath) : undefined; - return diff.isFullRepositoryDiff && relativePath === undefined ? undefined : parseGitPatchLineChanges(diff.patch, relativePath, originalDocument.lineCount, modifiedDocument.lineCount); -} - -async function getGitApi(): Promise { - const gitExtension = vscode.extensions.getExtension('vscode.git'); - if (!gitExtension) { - return undefined; - } - - try { - return (gitExtension.isActive ? gitExtension.exports : await gitExtension.activate()).getAPI(1); - } catch { - return undefined; - } -} - -async function getGitPatch( - repository: GitRepository, - filePath: string, - originalUri: vscode.Uri, - originalGitUri: GitUriParams | undefined, - modifiedUri: vscode.Uri, - modifiedGitUri: GitUriParams | undefined, -): Promise { - try { - if (originalGitUri && !modifiedGitUri && modifiedUri.scheme === 'file' && samePath(originalGitUri.path, modifiedUri.fsPath)) { - if (originalGitUri.ref === '~') { - return { patch: await repository.diff(false), isFullRepositoryDiff: true }; - } - if (originalGitUri.ref === 'HEAD') { - return { patch: await repository.diffWithHEAD(filePath), isFullRepositoryDiff: false }; - } - return { patch: await repository.diffWith(originalGitUri.ref, filePath), isFullRepositoryDiff: false }; - } - - if (originalGitUri && modifiedGitUri && samePath(originalGitUri.path, modifiedGitUri.path)) { - if (modifiedGitUri.ref === '') { - return { - patch: originalGitUri.ref === 'HEAD' - ? await repository.diffIndexWithHEAD(filePath) - : await repository.diffIndexWith(originalGitUri.ref, filePath), - isFullRepositoryDiff: false - }; - } - - return { patch: await repository.diffBetween(originalGitUri.ref, modifiedGitUri.ref, filePath), isFullRepositoryDiff: false }; - } + const diff = vscode.workspace.getTextDiff(originalDocument, modifiedDocument, { + ignoreTrimWhitespace: false, + maxComputationTimeMs: 5000, + }); - if (!originalGitUri && modifiedGitUri && originalUri.scheme === 'file' && samePath(originalUri.fsPath, modifiedGitUri.path)) { - return { - patch: modifiedGitUri.ref === 'HEAD' - ? await repository.diffWithHEAD(filePath) - : await repository.diffWith(modifiedGitUri.ref, filePath), - isFullRepositoryDiff: false - }; - } - } catch { - return undefined; - } - - return undefined; -} - -function fromGitUri(uri: vscode.Uri): GitUriParams | undefined { - if (uri.scheme !== 'git') { - return undefined; - } - - try { - const value = JSON.parse(uri.query) as GitUriParams; - return typeof value.path === 'string' && typeof value.ref === 'string' ? value : undefined; - } catch { - return undefined; - } -} - -function getRepositoryRelativePath(rootUri: vscode.Uri, filePath: string): string | undefined { - const root = normalizePath(rootUri.fsPath).replace(/\/+$/, ''); - const file = normalizePath(filePath); - if (file === root) { - return ''; - } - - return file.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? file.slice(root.length + 1) : undefined; -} - -function samePath(a: string, b: string): boolean { - return normalizePath(a).toLowerCase() === normalizePath(b).toLowerCase(); -} - -function normalizePath(value: string): string { - return value.replace(/\\/g, '/'); -} - -function getDocumentLines(document: vscode.TextDocument): string[] { - const lines: string[] = []; - for (let i = 0; i < document.lineCount; ++i) { - lines.push(document.lineAt(i).text); - } - return lines; -} - -function computeContentLineChanges(originalLines: readonly string[], modifiedLines: readonly string[]): LineChanges { - let start = 0; - while (start < originalLines.length && start < modifiedLines.length && originalLines[start] === modifiedLines[start]) { - ++start; - } - - let originalEnd = originalLines.length; - let modifiedEnd = modifiedLines.length; - while (originalEnd > start && modifiedEnd > start && originalLines[originalEnd - 1] === modifiedLines[modifiedEnd - 1]) { - --originalEnd; - --modifiedEnd; - } - - const originalCount = originalEnd - start; - const modifiedCount = modifiedEnd - start; - if (!originalCount && !modifiedCount) { - return createIdentityLineChanges(originalLines.length, modifiedLines.length); - } - - if (originalCount * modifiedCount > 500_000) { - return computeFallbackLineChanges(originalLines, modifiedLines, start, originalEnd, modifiedEnd); - } - - return computeLcsLineChanges(originalLines, modifiedLines, start, originalEnd, modifiedEnd); -} - -function parseGitPatchLineChanges(patch: string, relativePath: string | undefined, originalLineCount: number, modifiedLineCount: number): LineChanges { + const originalLineCount = originalDocument.lineCount; + const modifiedLineCount = modifiedDocument.lineCount; const added: number[] = []; const deleted: number[] = []; const mappings = createEmptyLineMappings(originalLineCount, modifiedLineCount); - const lines = patch.split(/\r?\n/); - let originalLine = 0; - let modifiedLine = 0; - let inHunk = false; - let fileMatches = !relativePath; - let matchedFile = !relativePath; - let oldPath: string | undefined; - let deletedBlockStart: number | undefined; - - const finishFile = () => { - if (fileMatches && matchedFile) { - fillUnchangedLineMappings(mappings, originalLine, originalLineCount, modifiedLine, modifiedLineCount); - } - }; - - for (const line of lines) { - if (line.startsWith('diff --git ')) { - finishFile(); - inHunk = false; - fileMatches = !relativePath; - matchedFile = !relativePath; - originalLine = 0; - modifiedLine = 0; - oldPath = undefined; - deletedBlockStart = undefined; - continue; - } - - if (!inHunk && line.startsWith('--- ')) { - oldPath = parseGitDiffPath(line.slice(4)); - continue; - } - - if (!inHunk && line.startsWith('+++ ')) { - const newPath = parseGitDiffPath(line.slice(4)); - fileMatches = !relativePath || oldPath === relativePath || newPath === relativePath; - matchedFile = matchedFile || fileMatches; - continue; - } - - const hunkMatch = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line); - if (hunkMatch) { - inHunk = true; - const nextOriginalLine = Math.max(0, Number(hunkMatch[1]) - 1); - const nextModifiedLine = Math.max(0, Number(hunkMatch[2]) - 1); - if (fileMatches) { - fillUnchangedLineMappings(mappings, originalLine, nextOriginalLine, modifiedLine, nextModifiedLine); - } - originalLine = nextOriginalLine; - modifiedLine = nextModifiedLine; - deletedBlockStart = undefined; - continue; - } - - if (!inHunk || !fileMatches || !line) { - continue; - } - - switch (line[0]) { - case ' ': - deletedBlockStart = undefined; - mappings.originalToModified[originalLine] = clampLine(modifiedLine, modifiedLineCount); - mappings.modifiedToOriginal[modifiedLine] = clampLine(originalLine, originalLineCount); - ++originalLine; - ++modifiedLine; - break; - case '-': - deletedBlockStart ??= originalLine; - mappings.originalToModified[originalLine] = clampLine(modifiedLine, modifiedLineCount); - deleted.push(originalLine++); - break; - case '+': - mappings.modifiedToOriginal[modifiedLine] = clampLine(deletedBlockStart ?? originalLine, originalLineCount); - added.push(modifiedLine++); - break; - case '\\': - break; - } - } - finishFile(); - fillMissingLineMappings(mappings); - return { added, deleted, ...mappings }; -} - -function parseGitDiffPath(rawPath: string): string | undefined { - if (rawPath === '/dev/null') { - return undefined; - } + let lastOriginalEnd = 0; + let lastModifiedEnd = 0; - const path = rawPath.startsWith('"') && rawPath.endsWith('"') ? rawPath.slice(1, -1) : rawPath; - return path.startsWith('a/') || path.startsWith('b/') ? path.slice(2) : path; -} + for await (const change of diff.changes) { + const origStart = change.originalRange.start.line; + const origEnd = change.originalRange.end.line; + const modStart = change.modifiedRange.start.line; + const modEnd = change.modifiedRange.end.line; -function computeLcsLineChanges(originalLines: readonly string[], modifiedLines: readonly string[], start: number, originalEnd: number, modifiedEnd: number): LineChanges { - const originalCount = originalEnd - start; - const modifiedCount = modifiedEnd - start; - const mappings = createEmptyLineMappings(originalLines.length, modifiedLines.length); - fillUnchangedLineMappings(mappings, 0, start, 0, start); - fillUnchangedLineMappings(mappings, originalEnd, originalLines.length, modifiedEnd, modifiedLines.length); - const lcsLengths: Uint32Array[] = []; - for (let i = 0; i <= originalCount; ++i) { - lcsLengths.push(new Uint32Array(modifiedCount + 1)); - } + // Map unchanged lines before this change + fillUnchangedLineMappings(mappings, lastOriginalEnd, origStart, lastModifiedEnd, modStart); - for (let i = originalCount - 1; i >= 0; --i) { - for (let j = modifiedCount - 1; j >= 0; --j) { - lcsLengths[i][j] = originalLines[start + i] === modifiedLines[start + j] - ? lcsLengths[i + 1][j + 1] + 1 - : Math.max(lcsLengths[i + 1][j], lcsLengths[i][j + 1]); + // Mark deleted and added lines within this change + for (let i = origStart; i < origEnd; ++i) { + deleted.push(i); + mappings.originalToModified[i] = clampLine(modStart, modifiedLineCount); } - } - - const added: number[] = []; - const deleted: number[] = []; - let originalIndex = 0; - let modifiedIndex = 0; - let deletedBlockStart: number | undefined; - let addedBlockStart: number | undefined; - while (originalIndex < originalCount || modifiedIndex < modifiedCount) { - if (originalIndex < originalCount && modifiedIndex < modifiedCount && originalLines[start + originalIndex] === modifiedLines[start + modifiedIndex]) { - deletedBlockStart = undefined; - addedBlockStart = undefined; - mappings.originalToModified[start + originalIndex] = clampLine(start + modifiedIndex, modifiedLines.length); - mappings.modifiedToOriginal[start + modifiedIndex] = clampLine(start + originalIndex, originalLines.length); - ++originalIndex; - ++modifiedIndex; - } else if (modifiedIndex < modifiedCount && (originalIndex === originalCount || lcsLengths[originalIndex][modifiedIndex + 1] >= lcsLengths[originalIndex + 1][modifiedIndex])) { - added.push(start + modifiedIndex); - addedBlockStart ??= start + modifiedIndex; - mappings.modifiedToOriginal[start + modifiedIndex] = clampLine(deletedBlockStart ?? start + originalIndex, originalLines.length); - ++modifiedIndex; - } else { - deleted.push(start + originalIndex); - deletedBlockStart ??= start + originalIndex; - mappings.originalToModified[start + originalIndex] = clampLine(addedBlockStart ?? start + modifiedIndex, modifiedLines.length); - ++originalIndex; + for (let i = modStart; i < modEnd; ++i) { + added.push(i); + mappings.modifiedToOriginal[i] = clampLine(origStart, originalLineCount); } - } - fillMissingLineMappings(mappings); - return { added, deleted, ...mappings }; -} - -function computeFallbackLineChanges(originalLines: readonly string[], modifiedLines: readonly string[], start: number, originalEnd: number, modifiedEnd: number): LineChanges { - const added: number[] = []; - const deleted: number[] = []; - const mappings = createEmptyLineMappings(originalLines.length, modifiedLines.length); - fillUnchangedLineMappings(mappings, 0, start, 0, start); - fillUnchangedLineMappings(mappings, originalEnd, originalLines.length, modifiedEnd, modifiedLines.length); - const sharedCount = Math.min(originalEnd - start, modifiedEnd - start); - for (let i = 0; i < sharedCount; ++i) { - mappings.originalToModified[start + i] = clampLine(start + i, modifiedLines.length); - mappings.modifiedToOriginal[start + i] = clampLine(start + i, originalLines.length); - if (originalLines[start + i] !== modifiedLines[start + i]) { - deleted.push(start + i); - added.push(start + i); - } + lastOriginalEnd = origEnd; + lastModifiedEnd = modEnd; } - for (let i = start + sharedCount; i < originalEnd; ++i) { - deleted.push(i); - mappings.originalToModified[i] = clampLine(modifiedEnd, modifiedLines.length); - } - for (let i = start + sharedCount; i < modifiedEnd; ++i) { - added.push(i); - mappings.modifiedToOriginal[i] = clampLine(originalEnd, originalLines.length); - } + // Map unchanged lines after the last change + fillUnchangedLineMappings(mappings, lastOriginalEnd, originalLineCount, lastModifiedEnd, modifiedLineCount); fillMissingLineMappings(mappings); return { added, deleted, ...mappings }; } -function createIdentityLineChanges(originalLineCount: number, modifiedLineCount: number): LineChanges { - const mappings = createEmptyLineMappings(originalLineCount, modifiedLineCount); - fillUnchangedLineMappings(mappings, 0, originalLineCount, 0, modifiedLineCount); - fillMissingLineMappings(mappings); - return { added: [], deleted: [], ...mappings }; -} - function createEmptyLineMappings(originalLineCount: number, modifiedLineCount: number): LineMappings { return { originalToModified: new Array(originalLineCount), diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index b1b21973b6073c..dc420d653c812f 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -11,6 +11,7 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.customEditorDiffs.d.ts" + "../../src/vscode-dts/vscode.proposed.customEditorDiffs.d.ts", + "../../src/vscode-dts/vscode.proposed.documentDiff.d.ts" ] } diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index ff9440d907a9b4..1953604537dcab 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -234,6 +234,9 @@ const _allApiProposals = { diffContentOptions: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', }, + documentDiff: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentDiff.d.ts', + }, documentFiltersExclusive: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', }, diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 6a5528bdf1337f..c5bc15deaf9e1d 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -51,6 +51,7 @@ import './mainThreadManagedSockets.js'; import './mainThreadOutputService.js'; import './mainThreadProgress.js'; import './mainThreadQuickDiff.js'; +import './mainThreadDocumentDiff.js'; import './mainThreadQuickOpen.js'; import './mainThreadRemoteConnectionData.js'; import './mainThreadSaveParticipant.js'; diff --git a/src/vs/workbench/api/browser/mainThreadDocumentDiff.ts b/src/vs/workbench/api/browser/mainThreadDocumentDiff.ts new file mode 100644 index 00000000000000..5ec1bb5f6c70f4 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadDocumentDiff.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 { IRange } from '../../../editor/common/core/range.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; +import { IEditorWorkerService } from '../../../editor/common/services/editorWorker.js'; +import { IDocumentDiffLineChangeDto, IDocumentDiffResultDto, MainContext, MainThreadDocumentDiffShape } from '../common/extHost.protocol.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; + +@extHostNamedCustomer(MainContext.MainThreadDocumentDiff) +export class MainThreadDocumentDiff implements MainThreadDocumentDiffShape { + + constructor( + _extHostContext: IExtHostContext, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + ) { + } + + async $computeDocumentDiff(originalUri: UriComponents, modifiedUri: UriComponents, ignoreTrimWhitespace: boolean, maxComputationTimeMs: number, computeMoves: boolean): Promise { + const original = URI.revive(originalUri); + const modified = URI.revive(modifiedUri); + const result = await this._editorWorkerService.computeDiff(original, modified, { + ignoreTrimWhitespace, + maxComputationTimeMs, + computeMoves, + }, 'advanced'); + if (!result) { + return null; + } + const toLineRange = (r: { startLineNumber: number; endLineNumberExclusive: number }): IRange => ({ + startLineNumber: r.startLineNumber, + startColumn: 1, + endLineNumber: r.endLineNumberExclusive, + endColumn: 1, + }); + + const mapChange = (c: typeof result.changes[0]): IDocumentDiffLineChangeDto => ({ + originalRange: toLineRange(c.original), + modifiedRange: toLineRange(c.modified), + innerChanges: c.innerChanges?.map(ic => ({ + originalRange: ic.originalRange, + modifiedRange: ic.modifiedRange, + })), + }); + + return { + identical: result.identical, + quitEarly: result.quitEarly, + changes: result.changes.map(mapChange), + moves: result.moves.map(m => ({ + originalRange: toLineRange(m.lineRangeMapping.original), + modifiedRange: toLineRange(m.lineRangeMapping.modified), + changes: m.changes.map(mapChange), + })), + }; + } + + dispose(): void { + // nothing to dispose + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ebe8580ca5d3a2..74203370830126 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -5,6 +5,7 @@ import type * as vscode from 'vscode'; import { CancellationTokenSource } from '../../../base/common/cancellation.js'; +import { AsyncIterableObject, raceCancellationError } from '../../../base/common/async.js'; import * as errors from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { combinedDisposable } from '../../../base/common/lifecycle.js'; @@ -29,7 +30,7 @@ import { UIKind } from '../../services/extensions/common/extensionHostProtocol.j import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; import { AISearchKeyword, ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; -import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainContext } from './extHost.protocol.js'; +import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, IDocumentDiffLineChangeDto, MainContext } from './extHost.protocol.js'; import { ExtHostRelatedInformation } from './extHostAiRelatedInformation.js'; import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js'; import { ExtHostApiCommands } from './extHostApiCommands.js'; @@ -1164,6 +1165,59 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'textSearchProvider2'); return extHostWorkspace.findTextInFiles2(query, options, extension.identifier, token); }, + getTextDiff(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument, options?: vscode.TextDiffOptions, token?: vscode.CancellationToken): vscode.TextDiffResponse { + checkProposedApiEnabled(extension, 'documentDiff'); + const proxy = rpcProtocol.getProxy(MainContext.MainThreadDocumentDiff); + if (token?.isCancellationRequested) { + const error = new errors.CancellationError(); + return { + changes: AsyncIterableObject.EMPTY, + complete: Promise.reject(error), + }; + } + const resultPromise = proxy.$computeDocumentDiff( + originalDocument.uri, + modifiedDocument.uri, + options?.ignoreTrimWhitespace ?? false, + options?.maxComputationTimeMs ?? 5000, + options?.computeMoves ?? false, + ); + const diffPromise = token ? raceCancellationError(resultPromise, token) : resultPromise; + const mappedPromise = diffPromise.then(result => { + if (!result) { + throw new Error('Could not compute diff. Make sure both documents are available.'); + } + return result; + }); + + const mapChange = (c: IDocumentDiffLineChangeDto) => ({ + originalRange: typeConverters.Range.to(c.originalRange), + modifiedRange: typeConverters.Range.to(c.modifiedRange), + innerChanges: c.innerChanges?.map(ic => ({ + originalRange: typeConverters.Range.to(ic.originalRange), + modifiedRange: typeConverters.Range.to(ic.modifiedRange), + })), + }); + + // TODO@API currently the diff is computed in one shot and all changes are emitted at once. + // In the future, we may want to stream changes incrementally as they are computed + // (e.g. by having the worker yield partial results). + return { + changes: new AsyncIterableObject(async emitter => { + const result = await mappedPromise; + emitter.emitMany(result.changes.map(mapChange)); + }), + complete: mappedPromise.then(result => ({ + identical: result.identical, + mayBeIncomplete: result.quitEarly, + moves: result.moves.map(m => ({ + originalRange: typeConverters.Range.to(m.originalRange), + modifiedRange: typeConverters.Range.to(m.modifiedRange), + changes: m.changes.map(mapChange), + })), + })), + }; + }, save: (uri) => { return extHostWorkspace.save(uri); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f9514d62fea433..7c94812d301e4c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2149,6 +2149,29 @@ export interface MainThreadQuickDiffShape extends IDisposable { $unregisterQuickDiffProvider(handle: number): Promise; } +export interface IDocumentDiffLineChangeDto { + originalRange: IRange; + modifiedRange: IRange; + innerChanges: { originalRange: IRange; modifiedRange: IRange }[] | undefined; +} + +export interface IDocumentDiffMoveDto { + originalRange: IRange; + modifiedRange: IRange; + changes: IDocumentDiffLineChangeDto[]; +} + +export interface IDocumentDiffResultDto { + identical: boolean; + quitEarly: boolean; + changes: IDocumentDiffLineChangeDto[]; + moves: IDocumentDiffMoveDto[]; +} + +export interface MainThreadDocumentDiffShape extends IDisposable { + $computeDocumentDiff(originalUri: UriComponents, modifiedUri: UriComponents, ignoreTrimWhitespace: boolean, maxComputationTimeMs: number, computeMoves: boolean): Promise; +} + export type DebugSessionUUID = string; export interface IDebugConfiguration { @@ -3920,6 +3943,7 @@ export const MainContext = { MainThreadOutputService: createProxyIdentifier('MainThreadOutputService'), MainThreadProgress: createProxyIdentifier('MainThreadProgress'), MainThreadQuickDiff: createProxyIdentifier('MainThreadQuickDiff'), + MainThreadDocumentDiff: createProxyIdentifier('MainThreadDocumentDiff'), MainThreadQuickOpen: createProxyIdentifier('MainThreadQuickOpen'), MainThreadStatusBar: createProxyIdentifier('MainThreadStatusBar'), MainThreadSecretState: createProxyIdentifier('MainThreadSecretState'), diff --git a/src/vscode-dts/vscode.proposed.documentDiff.d.ts b/src/vscode-dts/vscode.proposed.documentDiff.d.ts new file mode 100644 index 00000000000000..2e9c5320c0200d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.documentDiff.d.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export namespace workspace { + + /** + * Compute the diff between two text documents. + * + * This uses the same diff algorithm that powers the built-in diff editor, + * returning line-level and character-level change mappings. + * + * @param originalDocument The original (left-hand side) document. + * @param modifiedDocument The modified (right-hand side) document. + * @param options Options to control the diff computation. + * @param token A cancellation token. + * + * @returns A response object with streaming changes and a completion promise. + */ + export function getTextDiff(originalDocument: TextDocument, modifiedDocument: TextDocument, options?: TextDiffOptions, token?: CancellationToken): TextDiffResponse; + } + + /** + * Options for computing a text diff. + */ + export interface TextDiffOptions { + /** + * When `true`, the diff algorithm ignores changes in leading and trailing whitespace. + * Defaults to `false`. + */ + readonly ignoreTrimWhitespace?: boolean; + + /** + * Maximum time in milliseconds to spend computing the diff. + * `0` means no limit. Defaults to `5000`. + */ + readonly maxComputationTimeMs?: number; + + /** + * When `true`, the diff algorithm also computes moved text blocks. + * Defaults to `false`. + */ + readonly computeMoves?: boolean; + } + + /** + * The response from {@link workspace.getTextDiff}. + */ + export interface TextDiffResponse { + /** + * The line-level changes between the two documents, streamed as they are computed. + */ + readonly changes: AsyncIterable; + + /** + * Resolves when the diff computation is complete, with summary information. + */ + readonly complete: Thenable; + } + + /** + * Completion information for a text diff computation. + */ + export interface TextDiffComplete { + /** + * `true` if both documents are identical (byte-wise). + * + * A diff may return 0 changes but still have `identical` be `false`. This can happen if different diff options + * are passed in for example. + */ + readonly identical: boolean; + + /** + * `true` if the diff computation timed out and the result may be inaccurate. + */ + readonly mayBeIncomplete: boolean; + + /** + * Detected text moves (blocks of text that were moved from one location to another). + * Only populated when {@link DocumentDiffOptions.computeMoves} is `true`. + */ + readonly moves: readonly TextDiffMove[]; + } + + /** + * Represents a line-level change between two documents, optionally + * containing character-level (inner) changes. + */ + export interface TextDiffChange { + /** + * The line range in the original document. + */ + readonly originalRange: Range; + + /** + * The line range in the modified document. + */ + readonly modifiedRange: Range; + + /** + * Character-level changes within this line range change. + * May be `undefined` if inner changes were not computed. + */ + readonly innerChanges: readonly TextDiffInnerChange[] | undefined; + } + + /** + * Represents a character-level change within a {@link TextDiffChange}. + */ + export interface TextDiffInnerChange { + /** + * The range in the original document. + */ + readonly originalRange: Range; + + /** + * The range in the modified document. + */ + readonly modifiedRange: Range; + } + + /** + * Represents a detected text move between two documents. + */ + export interface TextDiffMove { + /** + * The line range in the original document that was moved. + */ + readonly originalRange: Range; + + /** + * The line range in the modified document where the text was moved to. + */ + readonly modifiedRange: Range; + + /** + * The changes within the moved text (differences between the original + * and the moved copy). + */ + readonly changes: readonly TextDiffChange[]; + } +} From d8733f54417da5d3dd49f260f0c74446b8a55b79 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 7 May 2026 00:30:34 -0700 Subject: [PATCH 11/28] in autopiltot and bypass we should never show global auto dialogue (#314920) in autopilto and bypass we should never show global auto dialogue --- .../tools/languageModelToolsService.ts | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 0a0fc3726cd381..5c3da450d93bfc 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -1085,6 +1085,19 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return !!widget && isAutoApproveLevel(widget.input.currentModeInfo.permissionLevel); } + /** + * True if the session is in an auto-approve level (Auto-Approve / Autopilot), + * via either the last request's stamped level or the live picker level. + */ + private _isSessionInAutoApproveLevel(chatSessionResource: URI | undefined): boolean { + if (!chatSessionResource) { + return false; + } + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + return isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource); + } + private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined { if (toolData.id === 'vscode_fetchWebPage_internal') { return 'fetch'; @@ -1135,19 +1148,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - // Auto-Approve All permission level bypasses all tool confirmations, - // unless enterprise policy has explicitly disabled global auto-approve. - // 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._isSessionLiveAutoApproveLevel(chatSessionResource)) { - // CLI sessions must always show their multi-option confirmation dialogs - // (e.g. uncommitted-changes prompt) even under Bypass Approvals - if (!(toolIdsThatCannotBeAutoApproved.has(tool.data.id) && getChatSessionType(chatSessionResource) !== localChatSessionType)) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; - } + // Bypass confirmation under Auto-Approve / Autopilot, unless enterprise + // policy disables global auto-approve. + if (chatSessionResource && !this._isAutoApprovePolicyRestricted() && this._isSessionInAutoApproveLevel(chatSessionResource)) { + // CLI sessions still need their multi-option dialogs (e.g. uncommitted changes). + if (!(toolIdsThatCannotBeAutoApproved.has(tool.data.id) && getChatSessionType(chatSessionResource) !== localChatSessionType)) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } @@ -1183,20 +1189,18 @@ 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. - // 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._isSessionLiveAutoApproveLevel(chatSessionResource)) { - if (!(toolIdsThatCannotBeAutoApproved.has(toolId) && getChatSessionType(chatSessionResource) !== localChatSessionType)) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; - } + // Bypass post-execution confirmation under Auto-Approve / Autopilot, + // unless enterprise policy disables global auto-approve. + const sessionAutoApprove = chatSessionResource && !this._isAutoApprovePolicyRestricted() && this._isSessionInAutoApproveLevel(chatSessionResource); + if (sessionAutoApprove) { + if (!(toolIdsThatCannotBeAutoApproved.has(toolId) && getChatSessionType(chatSessionResource!) !== localChatSessionType)) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } - if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { + // Don't show the YOLO opt-in dialog under autopilot: this runs after the + // tool result is already back in the agent loop, so it can't block anything. + if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && !sessionAutoApprove && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } From 6c2aba20dc0a41065f6d84fdd35f65ca4d7c6051 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 7 May 2026 00:33:46 -0700 Subject: [PATCH 12/28] chore: npm audit fix (#314855) --- .../vscode-pr-pinger/package-lock.json | 141 +++++++++++++++--- build/package-lock.json | 50 ++++--- build/vite/package-lock.json | 6 +- extensions/copilot/package-lock.json | 14 +- .../mermaid-chat-features/package-lock.json | 6 +- package-lock.json | 61 +++----- remote/package-lock.json | 30 ++-- test/mcp/package-lock.json | 14 +- 8 files changed, 201 insertions(+), 121 deletions(-) diff --git a/.vscode/extensions/vscode-pr-pinger/package-lock.json b/.vscode/extensions/vscode-pr-pinger/package-lock.json index ad347b7bd78be3..73cc0b7100c401 100644 --- a/.vscode/extensions/vscode-pr-pinger/package-lock.json +++ b/.vscode/extensions/vscode-pr-pinger/package-lock.json @@ -29,31 +29,48 @@ } }, "node_modules/@octokit/graphql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz", - "integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", + "license": "MIT", "dependencies": { "@octokit/request": "^6.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "universal-user-agent": "^6.0.0" }, "engines": { "node": ">= 14" } }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==", + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@octokit/openapi-types": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.10.0.tgz", "integrity": "sha512-wPQDpTyy35D6VS/lekXDaKcxy6LI2hzcbmXBnP180Pdgz3dXRzoHdav0w09yZzzWX8HHLGuqwAeyMqEPtWY2XA==" }, "node_modules/@octokit/request": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz", - "integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", + "license": "MIT", "dependencies": { "@octokit/endpoint": "^7.0.0", "@octokit/request-error": "^3.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "is-plain-object": "^5.0.0", "node-fetch": "^2.6.7", "universal-user-agent": "^6.0.0" @@ -63,11 +80,12 @@ } }, "node_modules/@octokit/request-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz", - "integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "license": "MIT", "dependencies": { - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" }, @@ -75,6 +93,36 @@ "node": ">= 14" } }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==", + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==", + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@octokit/types": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.4.0.tgz", @@ -165,13 +213,28 @@ } }, "@octokit/graphql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz", - "integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", "requires": { "@octokit/request": "^6.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "requires": { + "@octokit/openapi-types": "^18.0.0" + } + } } }, "@octokit/openapi-types": { @@ -180,26 +243,56 @@ "integrity": "sha512-wPQDpTyy35D6VS/lekXDaKcxy6LI2hzcbmXBnP180Pdgz3dXRzoHdav0w09yZzzWX8HHLGuqwAeyMqEPtWY2XA==" }, "@octokit/request": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz", - "integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", "requires": { "@octokit/endpoint": "^7.0.0", "@octokit/request-error": "^3.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "is-plain-object": "^5.0.0", "node-fetch": "^2.6.7", "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "requires": { + "@octokit/openapi-types": "^18.0.0" + } + } } }, "@octokit/request-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz", - "integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", "requires": { - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "requires": { + "@octokit/openapi-types": "^18.0.0" + } + } } }, "@octokit/types": { diff --git a/build/package-lock.json b/build/package-lock.json index 92f3b6a4a3e70f..4e26bb1882781c 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1092,6 +1092,19 @@ "node": ">= 12.13.0" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2299,9 +2312,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -3495,9 +3508,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "dev": true, "funding": [ { @@ -3511,9 +3524,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.7", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz", - "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "dev": true, "funding": [ { @@ -3523,9 +3536,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5106,9 +5120,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "dev": true, "funding": [ { @@ -6166,9 +6180,9 @@ } }, "node_modules/strnum": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", - "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "dev": true, "funding": [ { diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index c932ead6277d72..28febbbb9998a3 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -1081,9 +1081,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index ba939ae5bccf91..7ed70c12cf1e88 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -10115,12 +10115,12 @@ } }, "node_modules/express-rate-limit": { - "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==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -11286,9 +11286,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/extensions/mermaid-chat-features/package-lock.json b/extensions/mermaid-chat-features/package-lock.json index 718056e9037e00..68890279e7d811 100644 --- a/extensions/mermaid-chat-features/package-lock.json +++ b/extensions/mermaid-chat-features/package-lock.json @@ -1234,9 +1234,9 @@ "license": "MIT" }, "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==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package-lock.json b/package-lock.json index fdd44c171bd9b5..028214dc95322e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4374,9 +4374,9 @@ "license": "BSD-3-Clause" }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -5189,12 +5189,12 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -8222,13 +8222,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -8240,16 +8240,6 @@ "express": ">= 4.11" } }, - "node_modules/express-rate-limit/node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/express/node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -11669,13 +11659,10 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -12641,11 +12628,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, "node_modules/jschardet": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", @@ -17376,11 +17358,12 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -17508,7 +17491,9 @@ "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "optional": true }, "node_modules/ssh2": { "version": "1.17.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index 83eabd2b0bfcd2..9a4258aae75c84 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -1119,13 +1119,10 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -1154,11 +1151,6 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, "node_modules/jschardet": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", @@ -1531,11 +1523,12 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -1556,11 +1549,6 @@ "node": ">= 14" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index bae3bfa5d94623..bd24534e9a0f63 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -489,12 +489,12 @@ } }, "node_modules/express-rate-limit": { - "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==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -753,9 +753,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" From f645d405cbb7b1619d2f20595137c9882bcb6f4b Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 7 May 2026 17:15:58 +0900 Subject: [PATCH 13/28] refactor: remove sub application support (#314409) * refactor: remove sub application support * chore: remove OS entries * chore: update additional shortcut location on windows * chore: show one time deprecation banner * remove other indirect instances of embedded app --------- Co-authored-by: Sandeep Somavarapu --- build/darwin/sign.ts | 41 --- build/gulpfile.vscode.ts | 65 +--- build/gulpfile.vscode.win32.ts | 20 -- build/lib/embeddedType.ts | 20 -- build/lib/i18n.resources.json | 4 + build/win32/code.iss | 91 +---- src/bootstrap-meta.ts | 25 -- src/typings/electron-cross-app-ipc.d.ts | 61 ---- src/vs/base/common/platform.ts | 1 - src/vs/base/common/product.ts | 20 -- .../electron-main/agentsLastRunningTracker.ts | 70 ++-- src/vs/code/electron-main/app.ts | 168 ++++------ src/vs/code/node/cli.ts | 22 +- .../electron-main/crossAppIpcService.ts | 140 -------- .../electron-main/encryptionMainService.ts | 43 +-- src/vs/platform/environment/common/argv.ts | 1 - .../environment/common/environment.ts | 38 --- .../environment/common/environmentService.ts | 60 +--- src/vs/platform/environment/node/argv.ts | 1 - .../environment/node/environmentService.ts | 59 +--- .../platform/environment/node/userDataPath.ts | 7 +- .../launch/electron-main/launchMainService.ts | 18 - src/vs/platform/native/common/native.ts | 8 - .../electron-main/nativeHostMainService.ts | 30 -- src/vs/platform/native/node/siblingApp.ts | 95 ------ .../macOSCrossAppSecretSharing.ts | 315 ------------------ .../storage/electron-main/storageMain.ts | 152 +-------- .../electron-main/storageMainService.ts | 32 +- .../electron-main/storageMainService.test.ts | 103 +----- .../electron-main/abstractUpdateService.ts | 19 -- .../update/electron-main/crossAppUpdateIpc.ts | 310 ----------------- .../electron-main/updateService.darwin.ts | 12 +- .../electron-main/updateService.win32.ts | 12 +- .../url/electron-main/electronUrlListener.ts | 5 +- .../electron-main/userDataProfile.ts | 26 +- .../common/userDataProfileService.test.ts | 2 - .../userDataProfileMainService.test.ts | 2 - src/vs/platform/window/common/window.ts | 3 - .../windows/electron-main/windowImpl.ts | 7 +- .../platform/windows/electron-main/windows.ts | 7 +- .../electron-main/windowsMainService.ts | 30 +- .../workspacesHistoryMainService.ts | 14 +- .../vscode/browser/themeImporterService.ts | 27 -- .../services/vscode/common/themeImporter.ts | 48 --- .../electron-browser/themeImporterService.ts | 257 -------------- src/vs/sessions/sessions.desktop.main.ts | 1 - src/vs/sessions/sessions.web.main.ts | 1 - .../agentsAppMergedBanner.contribution.ts | 57 ++++ .../browser/workspace.contribution.ts | 11 +- .../electron-browser/environmentService.ts | 5 +- .../electron-browser/workbenchTestServices.ts | 2 - src/vs/workbench/workbench.desktop.main.ts | 3 + 52 files changed, 212 insertions(+), 2359 deletions(-) delete mode 100644 build/lib/embeddedType.ts delete mode 100644 src/typings/electron-cross-app-ipc.d.ts delete mode 100644 src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts delete mode 100644 src/vs/platform/native/node/siblingApp.ts delete mode 100644 src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts delete mode 100644 src/vs/platform/update/electron-main/crossAppUpdateIpc.ts delete mode 100644 src/vs/sessions/services/vscode/browser/themeImporterService.ts delete mode 100644 src/vs/sessions/services/vscode/common/themeImporter.ts delete mode 100644 src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts create mode 100644 src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index ed12a46473ace6..aa8fc806f2e94f 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -76,9 +76,6 @@ async function main(buildDir?: string): Promise { const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); const appName = product.nameLong + '.app'; const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); - const embeddedInfoPlistPath = product.embedded - ? path.resolve(appRoot, appName, 'Contents', 'Applications', `${product.embedded.nameLong}.app`, 'Contents', 'Info.plist') - : undefined; const appOpts: SignOptions = { app: path.join(appRoot, appName), @@ -132,44 +129,6 @@ async function main(buildDir?: string): Promise { 'The app uses your local network for DNS resolution and to connect to locally running services.', `${infoPlistPath}` ]); - - if (embeddedInfoPlistPath && fs.existsSync(embeddedInfoPlistPath)) { - await spawn('plutil', [ - '-insert', - 'NSAppleEventsUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use AppleScript.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-replace', - 'NSMicrophoneUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use the Microphone.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-replace', - 'NSCameraUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use the Camera.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-replace', - 'NSAudioCaptureUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use Audio Capture.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-insert', - 'NSLocalNetworkUsageDescription', - '-string', - `The app uses your local network for DNS resolution and to connect to locally running services.`, - `${embeddedInfoPlistPath}` - ]); - } } await retrySignOnKeychainError(() => sign(appOpts)); diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 6205bfe48ed2ed..a636b48cd5f078 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -32,7 +32,6 @@ import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from '. import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask, compileCopilotExtensionBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; import { getCopilotExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; -import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; import globCallback from 'glob'; @@ -288,10 +287,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const name = product.nameShort; const packageJsonUpdates: Record = { name, version }; - const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; - const embedded = isInsiderOrExploration - ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded - : undefined; if (platform === 'linux') { packageJsonUpdates.desktopName = `${product.applicationName}.desktop`; @@ -312,11 +307,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d json.date = readISODate(out); json.checksums = checksums; json.version = version; - if (embedded) { - json['darwinSiblingBundleIdentifier'] = embedded.darwinBundleIdentifier; - const embeddedObj = json['embedded'] as EmbeddedProductInfo; - embeddedObj['darwinSiblingBundleIdentifier'] = json['darwinBundleIdentifier'] as string; - } return json; })) .pipe(es.through(function (file) { @@ -324,32 +314,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d this.emit('data', file); })); - const packageSubJsonStream = embedded - ? gulp.src(['package.json'], { base: '.' }) - .pipe(jsonEditor((json: Record) => { - json.name = embedded.nameShort; - return json; - })) - .pipe(rename('package.sub.json')) - : undefined; - - const productSubJsonStream = embedded - ? gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor((json: Record) => { - // Preserve the host's mutex name before overlaying embedded properties, - // so the embedded app can poll for the correct InnoSetup -ready mutex. - const hostMutexName = json['win32MutexName']; - Object.keys(embedded).forEach(key => { - json[key] = embedded[key as keyof EmbeddedProductInfo]; - }); - if (hostMutexName) { - json['win32SetupMutexName'] = hostMutexName; - } - return json; - })) - .pipe(rename('product.sub.json')) - : undefined; - const license = gulp.src([product.licenseFileName, 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.', allowEmpty: true }); // TODO the API should be copied to `out` during compile, not here @@ -401,12 +365,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d sources, deps ]; - if (packageSubJsonStream) { - mergeStreams.push(packageSubJsonStream); - } - if (productSubJsonStream) { - mergeStreams.push(productSubJsonStream); - } let all = es.merge(...mergeStreams); if (platform === 'win32') { @@ -442,9 +400,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d 'resources/win32/code_70x70.png', 'resources/win32/code_150x150.png' ], { base: '.' })); - if (embedded) { - all = es.merge(all, gulp.src('resources/win32/sessions.ico', { base: '.' })); - } } else if (platform === 'linux') { const policyDest = gulp.src('.build/policies/linux/**', { base: '.build/policies/linux' }) .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); @@ -463,21 +418,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, - ffmpegChromium: false, - ...(embedded ? { - darwinMiniAppName: embedded.nameShort, - darwinMiniAppDisplayName: embedded.nameLong, - darwinMiniAppBundleIdentifier: embedded.darwinBundleIdentifier, - darwinMiniAppIcon: 'resources/darwin/agents.icns', - darwinMiniAppAssetsCar: 'resources/darwin/agents.car', - darwinMiniAppBundleURLTypes: [{ - role: 'Viewer', - name: embedded.nameLong, - urlSchemes: [embedded.urlProtocol] - }], - win32ProxyAppName: embedded.nameShort, - win32ProxyIcon: 'resources/win32/sessions.ico', - } : {}) + ffmpegChromium: false }; let result: NodeJS.ReadWriteStream = all @@ -489,8 +430,8 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d '**', '!LICENSE', '!version', - ...(platform === 'darwin' && !isInsiderOrExploration ? ['!**/Contents/Applications', '!**/Contents/Applications/**'] : []), - ...(platform === 'win32' && !isInsiderOrExploration ? ['!**/electron_proxy.exe'] : []), + ...(platform === 'darwin' ? ['!**/Contents/Applications', '!**/Contents/Applications/**'] : []), + ...(platform === 'win32' ? ['!**/electron_proxy.exe'] : []), ], { dot: true })); if (platform === 'linux') { diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 01070c9503c051..667057fcb09bf3 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -14,7 +14,6 @@ import product from '../product.json' with { type: 'json' }; import { getVersion } from './lib/getVersion.ts'; import * as task from './lib/task.ts'; import * as util from './lib/util.ts'; -import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); @@ -112,25 +111,6 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { Quality: quality }; - const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; - const embedded = isInsiderOrExploration - ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded - : undefined; - - if (embedded) { - // VS Code's sibling is the embedded app. - productJson['win32SiblingExeBasename'] = embedded.nameShort; - // The embedded app's sibling is VS Code. - if (productJson['embedded']) { - productJson['embedded']['win32SiblingExeBasename'] = product.nameShort; - } - definitions['ProxyExeBasename'] = embedded.nameShort; - definitions['ProxyAppUserId'] = embedded.win32AppUserModelId; - definitions['ProxyNameLong'] = embedded.nameLong; - definitions['ProxyExeUrlProtocol'] = embedded.urlProtocol; - definitions['ProxyMutex'] = embedded.win32MutexName; - } - if (quality === 'stable' || quality === 'insider') { definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; diff --git a/build/lib/embeddedType.ts b/build/lib/embeddedType.ts deleted file mode 100644 index b0b3ad7d833ec4..00000000000000 --- a/build/lib/embeddedType.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export type EmbeddedProductInfo = { - nameShort: string; - nameLong: string; - applicationName: string; - dataFolderName: string; - darwinBundleIdentifier: string; - darwinSiblingBundleIdentifier?: string; - urlProtocol: string; - win32AppUserModelId: string; - win32MutexName: string; - win32RegValueName: string; - win32NameVersion: string; - win32VersionedUpdate: boolean; - win32SiblingExeBasename?: string; -}; diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 951401e17612cd..a9bc64352d706b 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -290,6 +290,10 @@ "name": "vs/workbench/contrib/externalUriOpener", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/agentsAppMergedBanner", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/welcomeGettingStarted", "project": "vscode-workbench" diff --git a/build/win32/code.iss b/build/win32/code.iss index 594453ed7b989c..3e9657e65da691 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -74,10 +74,15 @@ Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\nod Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate -#ifdef ProxyExeBasename -; Clean up legacy Start Menu shortcut that used ProxyExeBasename instead of ProxyNameLong -Type: files; Name: "{group}\{#ProxyExeBasename}.lnk" -#endif + +; Remove leftover shortcuts and pinned entries from the previous Agents sub-application. +Type: files; Name: "{group}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userprograms}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{commonprograms}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{autodesktop}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\User Pinned\StartMenu\Agents - Insiders.lnk"; Check: QualityIsInsiders [UninstallDelete] Type: filesandordirs; Name: "{app}\_" @@ -99,12 +104,9 @@ Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{ Name: "{app}"; AfterInstall: DisableAppDirInheritance [Files] -Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,{#ifdef ProxyExeBasename}\{#ProxyExeBasename}.exe,{#endif}\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#ExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetExeBasename}"; Flags: ignoreversion Source: "{#ExeBasename}.VisualElementsManifest.xml"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetVisualElementsManifest}"; Flags: ignoreversion -#ifdef ProxyExeBasename -Source: "{#ProxyExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetProxyExeBasename}"; Flags: ignoreversion -#endif Source: "tools\*"; DestDir: "{app}\{#VersionedResourcesFolder}\tools"; Flags: ignoreversion Source: "policies\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\policies"; Flags: ignoreversion skipifsourcedoesntexist Source: "bin\{#TunnelApplicationName}.exe"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirTunnelApplicationFilename}"; Flags: ignoreversion skipifsourcedoesntexist @@ -120,18 +122,10 @@ Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourc Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#NameLong}.lnk')) Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#NameLong}.lnk')) Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}.lnk')) -#ifdef ProxyExeBasename -Name: "{group}\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#ProxyNameLong}.lnk')) -Name: "{autodesktop}\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#ProxyNameLong}.lnk')) -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}.lnk')) -#endif [Run] Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent -#ifdef ProxyExeBasename -Filename: "{app}\{#ProxyExeBasename}.exe"; Description: "{cm:LaunchProgram,{#ProxyNameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunProxyAfterUpdate -#endif [Registry] #if "user" == InstallTarget @@ -1301,15 +1295,6 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValu Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Check: ShouldInstallLegacyFolderContextMenu Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Check: ShouldInstallLegacyFolderContextMenu -; URL Protocol handler for proxy executable -#ifdef ProxyExeBasename -#ifdef ProxyExeUrlProtocol -Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}"; ValueType: string; ValueName: ""; ValueData: "URL:{#ProxyExeUrlProtocol}"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ProxyExeBasename}.exe"" --open-url -- ""%1"""; Flags: uninsdeletekey -#endif -#endif - ; Environment #if "user" == InstallTarget #define EnvironmentRootKey "HKCU" @@ -1421,10 +1406,6 @@ end; var ShouldRestartTunnelService: Boolean; -#ifdef ProxyMutex - ProxyWasRunning: Boolean; - AppWasRunning: Boolean; -#endif function StopTunnelOtherProcesses(): Boolean; var @@ -1520,27 +1501,11 @@ end; function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then -#ifdef ProxyMutex - Result := (not LockFileExists()) and AppWasRunning -#else Result := not LockFileExists() -#endif else Result := True; end; -#ifdef ProxyMutex -function ShouldRunProxyAfterUpdate(): Boolean; -begin - // Relaunch the proxy app after a background update if it was - // running when the update started (detected via its mutex). - if IsBackgroundUpdate() then - Result := (not LockFileExists()) and ProxyWasRunning - else - Result := False; -end; -#endif - function IsWindows11OrLater(): Boolean; begin Result := (GetWindowsVersion >= $0A0055F0); @@ -1630,11 +1595,7 @@ begin if IsBackgroundUpdate() then Result := '' else -#ifdef ProxyMutex - Result := '{#AppMutex},{#ProxyMutex}'; -#else Result := '{#AppMutex}'; -#endif end; function GetSetupMutex(Value: string): string; @@ -1643,11 +1604,7 @@ begin // During background updates, also create a -updating mutex that VS Code checks // to avoid launching while an update is in progress. if IsBackgroundUpdate() then -#ifdef ProxyMutex - Result := '{#AppMutex}setup,{#AppMutex}-updating,{#ProxyMutex}-updating' -#else Result := '{#AppMutex}setup,{#AppMutex}-updating' -#endif else Result := '{#AppMutex}setup'; end; @@ -1676,16 +1633,6 @@ begin Result := ExpandConstant('{#ExeBasename}.exe'); end; -#ifdef ProxyExeBasename -function GetProxyExeBasename(Value: string): string; -begin - if IsBackgroundUpdate() and IsVersionedUpdate() then - Result := ExpandConstant('new_{#ProxyExeBasename}.exe') - else - Result := ExpandConstant('{#ProxyExeBasename}.exe'); -end; -#endif - function GetBinDirTunnelApplicationFilename(Value: string): string; begin if IsBackgroundUpdate() and IsVersionedUpdate() then @@ -1850,24 +1797,12 @@ begin if IsBackgroundUpdate() then begin -#ifdef ProxyMutex - // Snapshot whether each app is running before we wait for them to exit - ProxyWasRunning := CheckForMutexes('{#ProxyMutex}'); - AppWasRunning := CheckForMutexes('{#AppMutex}'); - Log('App was running: ' + BoolToStr(AppWasRunning)); - Log('Proxy app was running: ' + BoolToStr(ProxyWasRunning)); -#endif - SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False); CreateMutex('{#AppMutex}-ready'); DeleteFile(GetUpdateProgressFilePath()); Log('Checking whether application is still running...'); -#ifdef ProxyMutex - while (CheckForMutexes('{#AppMutex},{#ProxyMutex}')) do -#else while (CheckForMutexes('{#AppMutex}')) do -#endif begin if CancelFileExists() then begin @@ -1882,14 +1817,14 @@ begin if not SessionEndFileExists() and not CancelFileExists() then begin StopTunnelServiceIfNeeded(); Log('Invoking inno_updater for background update'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); DeleteFile(ExpandConstant('{app}\updating_version')); Log('inno_updater completed successfully'); #if "system" == InstallTarget if IsVersionedUpdate() then begin KillContextMenuComSurrogate(); Log('Invoking inno_updater to remove previous installation folder'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); Log('inno_updater completed gc successfully'); end; #endif @@ -1900,7 +1835,7 @@ begin if IsVersionedUpdate() then begin KillContextMenuComSurrogate(); Log('Invoking inno_updater to remove previous installation folder'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); Log('inno_updater completed gc successfully'); end; end; diff --git a/src/bootstrap-meta.ts b/src/bootstrap-meta.ts index b1d8213d893426..1e5affb0a97543 100644 --- a/src/bootstrap-meta.ts +++ b/src/bootstrap-meta.ts @@ -5,7 +5,6 @@ import { createRequire } from 'node:module'; import type { IProductConfiguration } from './vs/base/common/product.js'; -import type { INodeProcess } from './vs/base/common/platform.js'; const require = createRequire(import.meta.url); @@ -19,30 +18,6 @@ if (pkgObj['BUILD_INSERT_PACKAGE_CONFIGURATION']) { pkgObj = require('../package.json'); // Running out of sources } -// Load sub files -if ((process as INodeProcess).isEmbeddedApp) { - // Preserve the parent VS Code's policy identity before the - // embedded app overrides win32RegValueName / darwinBundleIdentifier. - productObj.parentPolicyConfig = { - win32RegValueName: productObj.win32RegValueName, - darwinBundleIdentifier: productObj.darwinBundleIdentifier, - urlProtocol: productObj.urlProtocol, - }; - - try { - const productSubObj = require('../product.sub.json'); - if (productObj.embedded && productSubObj.embedded) { - Object.assign(productObj.embedded, productSubObj.embedded); - delete productSubObj.embedded; - } - Object.assign(productObj, productSubObj); - } catch (error) { /* ignore */ } - try { - const pkgSubObj = require('../package.sub.json'); - pkgObj = Object.assign(pkgObj, pkgSubObj); - } catch (error) { /* ignore */ } -} - let productOverridesObj = {}; if (process.env['VSCODE_DEV']) { try { diff --git a/src/typings/electron-cross-app-ipc.d.ts b/src/typings/electron-cross-app-ipc.d.ts deleted file mode 100644 index 4a184909159bbb..00000000000000 --- a/src/typings/electron-cross-app-ipc.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Type definitions for Electron's crossAppIPC module (custom build). - * - * This module provides secure IPC between an Electron host app and an - * embedded Electron app (MiniApp) within nested bundles. Communication - * is authenticated via code-signature verification (macOS: Mach ports, - * Windows: named pipes). - */ - -declare namespace Electron { - - interface CrossAppIPCMessageEvent { - /** The deserialized message data sent by the peer app. */ - data: any; - /** Array of transferred MessagePortMain objects (if any). */ - ports: Electron.MessagePortMain[]; - } - - type CrossAppIPCDisconnectReason = - | 'peer-disconnected' - | 'handshake-failed' - | 'connection-failed' - | 'connection-timeout'; - - interface CrossAppIPC extends NodeJS.EventEmitter { - on(event: 'connected', listener: () => void): this; - once(event: 'connected', listener: () => void): this; - removeListener(event: 'connected', listener: () => void): this; - - on(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; - once(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; - removeListener(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; - - on(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; - once(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; - removeListener(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; - - connect(): void; - close(): void; - postMessage(message: any, transferables?: Electron.MessagePortMain[]): void; - readonly connected: boolean; - readonly isServer: boolean; - } - - interface CrossAppIPCModule { - createCrossAppIPC(): CrossAppIPC; - } - - namespace Main { - const crossAppIPC: CrossAppIPCModule | undefined; - } - - namespace CrossProcessExports { - const crossAppIPC: CrossAppIPCModule | undefined; - } -} diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 91831fa4b78b18..3013e09489b22f 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -44,7 +44,6 @@ export interface INodeProcess { chrome?: string; }; type?: string; - isEmbeddedApp?: boolean; cwd: () => string; } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index f594d9451aefaa..cc90fc6f0694d7 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -79,7 +79,6 @@ export interface IProductConfiguration { readonly win32RegValueName?: string; readonly win32NameVersion?: string; readonly win32VersionedUpdate?: boolean; - readonly win32SiblingExeBasename?: string; readonly win32ContextMenu?: { readonly [arch: string]: { readonly clsid: string } }; readonly applicationName: string; readonly embedderIdentifier?: string; @@ -224,7 +223,6 @@ export interface IProductConfiguration { readonly 'editSessions.store'?: Omit; readonly darwinUniversalAssetId?: string; readonly darwinBundleIdentifier?: string; - readonly darwinSiblingBundleIdentifier?: string; readonly profileTemplatesUrl?: string; readonly commonlyUsedSettings?: string[]; @@ -242,8 +240,6 @@ export interface IProductConfiguration { readonly onboardingKeymaps?: readonly IProductOnboardingKeymap[]; readonly onboardingThemes?: readonly IProductOnboardingTheme[]; - readonly embedded?: IEmbeddedProductConfiguration; - /** * When running as an embedded app, the parent VS Code's policy * identity (win32RegValueName / darwinBundleIdentifier) so that @@ -270,22 +266,6 @@ export interface IProductOnboardingTheme { readonly type: 'dark' | 'light' | 'hcDark' | 'hcLight'; } -export type IEmbeddedProductConfiguration = Pick; - export interface ITunnelApplicationConfig { authenticationProviders: IStringDictionary<{ scopes: string[] }>; editorWebUrl: string; diff --git a/src/vs/code/electron-main/agentsLastRunningTracker.ts b/src/vs/code/electron-main/agentsLastRunningTracker.ts index d62f05767a71b0..42abc6f391bd87 100644 --- a/src/vs/code/electron-main/agentsLastRunningTracker.ts +++ b/src/vs/code/electron-main/agentsLastRunningTracker.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../base/common/buffer.js'; -import { Disposable } from '../../base/common/lifecycle.js'; import { Schemas } from '../../base/common/network.js'; import { joinPath } from '../../base/common/resources.js'; import { URI } from '../../base/common/uri.js'; -import { ICrossAppIPCService } from '../../platform/crossAppIpc/electron-main/crossAppIpcService.js'; -import { IFileService } from '../../platform/files/common/files.js'; +import { FileOperationResult, IFileService, toFileOperationResult } from '../../platform/files/common/files.js'; import { ILogService } from '../../platform/log/common/log.js'; /** * Marker file written by the Agents sub-application into the host VS Code's - * user-data directory while the Agents app is running. After a future update - * removes the sub-application, the host VS Code can detect this marker on - * first launch and restore the user's last-known windows state. + * user-data directory while it was running. After the update which removes + * the sub-application, the host VS Code detects this marker on first launch, + * restores the appropriate windows and removes the marker. */ export const AGENTS_LAST_RUNNING_MARKER_FILE_NAME = 'agentsLastRunning.json'; @@ -27,38 +24,35 @@ export interface IAgentsLastRunningMarker { readonly writtenAt: number; } -export class AgentsLastRunningTracker extends Disposable { - - private readonly markerResource: URI; - - constructor( - hostUserRoamingDataHome: URI, - @ICrossAppIPCService private readonly crossAppIPCService: ICrossAppIPCService, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - ) { - super(); - - this.markerResource = joinPath(hostUserRoamingDataHome.with({ scheme: Schemas.file }), AGENTS_LAST_RUNNING_MARKER_FILE_NAME); - - // Write marker now and refresh whenever the host's liveness changes - // so the recorded `vscodeRunning` snapshot reflects the latest state. - this.writeMarker(); - this._register(this.crossAppIPCService.onDidConnect(() => this.writeMarker())); - this._register(this.crossAppIPCService.onDidDisconnect(() => this.writeMarker())); +export async function tryConsumeAgentsLastRunningMarker(userRoamingDataHome: URI, fileService: IFileService, logService: ILogService): Promise { + const markerResource = joinPath(userRoamingDataHome.with({ scheme: Schemas.file }), AGENTS_LAST_RUNNING_MARKER_FILE_NAME); + + let parsed: IAgentsLastRunningMarker | undefined; + try { + const contents = await fileService.readFile(markerResource); + const json = JSON.parse(contents.value.toString()) as Partial; + if (typeof json.agentsRunning === 'boolean' && typeof json.vscodeRunning === 'boolean') { + parsed = { + agentsRunning: json.agentsRunning, + vscodeRunning: json.vscodeRunning, + writtenAt: typeof json.writtenAt === 'number' ? json.writtenAt : 0 + }; + } + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + logService.warn(`[agents] failed to read last-running marker at ${markerResource.fsPath}: ${error}`); + } + return undefined; } - private async writeMarker(): Promise { - const payload: IAgentsLastRunningMarker = { - agentsRunning: true, - vscodeRunning: this.crossAppIPCService.connected, - writtenAt: Date.now() - }; - try { - await this.fileService.writeFile(this.markerResource, VSBuffer.fromString(JSON.stringify(payload))); - this.logService.trace(`[agents] wrote last-running marker at ${this.markerResource.fsPath} (vscodeRunning=${payload.vscodeRunning})`); - } catch (error) { - this.logService.warn(`[agents] failed to write last-running marker at ${this.markerResource.fsPath}: ${error}`); - } + try { + await fileService.del(markerResource); + } catch (error) { + logService.warn(`[agents] failed to delete last-running marker at ${markerResource.fsPath}: ${error}`); } + + logService.info(`[agents] consumed last-running marker at ${markerResource.fsPath} (agentsRunning=${parsed?.agentsRunning}, vscodeRunning=${parsed?.vscodeRunning})`); + + return parsed; } + diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 75c1a577f60414..48eeec065d58ab 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -6,8 +6,8 @@ import { app, Details, GPUFeatureStatus, powerMonitor, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js'; import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js'; -import { execFile } from 'child_process'; import { hostname, release } from 'os'; +import { exec } from 'child_process'; import { initWindowsVersionInfo } from '../../base/node/windowsVersion.js'; import { VSBuffer } from '../../base/common/buffer.js'; import { toErrorMessage } from '../../base/common/errorMessage.js'; @@ -17,7 +17,7 @@ import { getPathLabel } from '../../base/common/labels.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../base/common/lifecycle.js'; import { Schemas, VSCODE_AUTHORITY } from '../../base/common/network.js'; import { join, posix } from '../../base/common/path.js'; -import { INodeProcess, IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from '../../base/common/platform.js'; +import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from '../../base/common/platform.js'; import { assertType } from '../../base/common/types.js'; import { URI } from '../../base/common/uri.js'; import { generateUuid } from '../../base/common/uuid.js'; @@ -83,10 +83,7 @@ import { ITelemetryServiceConfig, TelemetryService } from '../../platform/teleme import { getPiiPathsFromEnvironment, getTelemetryLevel, isInternalTelemetry, NullTelemetryService, supportsTelemetry } from '../../platform/telemetry/common/telemetryUtils.js'; import { IUpdateService } from '../../platform/update/common/update.js'; import { UpdateChannel } from '../../platform/update/common/updateIpc.js'; -import { AbstractUpdateService } from '../../platform/update/electron-main/abstractUpdateService.js'; -import { CrossAppUpdateCoordinator } from '../../platform/update/electron-main/crossAppUpdateIpc.js'; import { NotAvailableUpdateDialog } from '../../platform/update/electron-main/notAvailableUpdateDialog.js'; -import { MacOSCrossAppSecretSharing } from '../../platform/secrets/electron-main/macOSCrossAppSecretSharing.js'; import { DarwinUpdateService } from '../../platform/update/electron-main/updateService.darwin.js'; import { LinuxUpdateService } from '../../platform/update/electron-main/updateService.linux.js'; import { SnapUpdateService } from '../../platform/update/electron-main/updateService.snap.js'; @@ -145,8 +142,7 @@ import { IWebContentExtractorService } from '../../platform/webContentExtractor/ import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js'; import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../platform/networkFilter/common/networkFilterService.js'; import { ITerminalSandboxService, NullTerminalSandboxService } from '../../platform/sandbox/common/terminalSandboxService.js'; -import { CrossAppIPCService, ICrossAppIPCService } from '../../platform/crossAppIpc/electron-main/crossAppIpcService.js'; -import { AgentsLastRunningTracker } from './agentsLastRunningTracker.js'; +import { tryConsumeAgentsLastRunningMarker } from './agentsLastRunningTracker.js'; import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js'; /** @@ -581,6 +577,13 @@ export class CodeApplication extends Disposable { this.logService.error(error); } + // One-time cleanup of the previous Agents sub-application on macOS (Insiders only). + // The agents experience now ships as a window of VS Code itself, so any leftover + // Dock pinned entry and Launch Services registration of the old sub-app should be removed. + if (isMacintosh && this.productService.quality === 'insider') { + this.cleanupAgentsApplication(); + } + // Main process server (electron IPC based) const mainProcessElectronServer = new ElectronIPCServer(); Event.once(this.lifecycleMainService.onWillShutdown)(e => { @@ -757,11 +760,6 @@ export class CodeApplication extends Disposable { const windowOpenable = this.getWindowOpenableFromProtocolUrl(protocolUrl.uri); if (windowOpenable) { - if ((process as INodeProcess).isEmbeddedApp) { - this.logService.trace('app#resolveInitialProtocolUrls() agents app skipping window openable:', protocolUrl.uri.toString(true)); - continue; // Agents app: skip all window openables (file/folder/workspace) - } - if (await this.shouldBlockOpenable(windowOpenable, windowsMainService, dialogMainService)) { this.logService.trace('app#resolveInitialProtocolUrls() protocol url was blocked:', protocolUrl.uri.toString(true)); @@ -907,27 +905,6 @@ export class CodeApplication extends Disposable { private async handleProtocolUrl(windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService, urlService: IURLService, uri: URI, options?: IOpenURLOptions): Promise { this.logService.trace('app#handleProtocolUrl():', uri.toString(true), options); - // Agents app: ensure the agents window is open, then let other handlers process the URL. - if ((process as INodeProcess).isEmbeddedApp) { - this.logService.trace('app#handleProtocolUrl() agents app handling protocol URL:', uri.toString(true)); - - // Skip window openables (file/folder/workspace) for security - const windowOpenable = this.getWindowOpenableFromProtocolUrl(uri); - if (windowOpenable) { - this.logService.trace('app#handleProtocolUrl() agents app skipping window openable:', uri.toString(true)); - return true; - } - - // Ensure agents window is open to receive the URL - const windows = await windowsMainService.openAgentsWindow({ context: OpenContext.LINK, cli: this.environmentMainService.args }); - const window = windows.at(0); - window?.focus(); - await window?.ready(); - - // Return false to let subsequent handlers (e.g., URLHandlerChannelClient) forward the URL - return false; - } - // Support 'workspace' URLs (https://github.com/microsoft/vscode/issues/124263) if (uri.scheme === this.productService.urlProtocol && uri.path === 'workspace') { uri = uri.with({ @@ -1094,9 +1071,6 @@ export class CodeApplication extends Disposable { // Encryption services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService)); - // Cross-app IPC - services.set(ICrossAppIPCService, new SyncDescriptor(CrossAppIPCService)); - // Browser View services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */)); services.set(IBrowserViewGroupMainService, new SyncDescriptor(BrowserViewGroupMainService, undefined, false /* proxied to other processes */)); @@ -1244,46 +1218,14 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService); sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService)); - // Initialize cross-app IPC on supported platforms so all consumers - // (update coordination, secret sharing, etc.) share one connection. - const crossAppIPCService = accessor.get(ICrossAppIPCService); - if (isMacintosh || isWindows) { - crossAppIPCService.initialize(); - } - - // Update (with cross-app coordination on macOS/Windows where crossAppIPC is available) - const localUpdateService = accessor.get(IUpdateService); - let effectiveUpdateService: IUpdateService = localUpdateService; - const isInsiderOrExploration = this.productService.quality === 'insider' || this.productService.quality === 'exploration'; - if ((isMacintosh || isWindows) && isInsiderOrExploration) { - const updateCoordinator = this._register(new CrossAppUpdateCoordinator( - localUpdateService as AbstractUpdateService, - this.logService, - this.lifecycleMainService, - crossAppIPCService, - )); - effectiveUpdateService = updateCoordinator; - } - const updateChannel = new UpdateChannel(effectiveUpdateService); + // Update + const updateService = accessor.get(IUpdateService); + const updateChannel = new UpdateChannel(updateService); mainProcessElectronServer.registerChannel('update', updateChannel); // Show a native "no updates available" dialog from the focused app's main // process to avoid double dialogs across apps and ensure a native dialog. - this._register(new NotAvailableUpdateDialog(effectiveUpdateService, accessor.get(IDialogMainService))); - - // Cross-app secret sharing (macOS only, demand-driven) - if (isMacintosh) { - this._register(new MacOSCrossAppSecretSharing( - accessor.get(IStorageMainService), - accessor.get(IEncryptionMainService), - accessor.get(IStateService), - this.logService, - this.environmentMainService, - accessor.get(ILaunchMainService), - this.lifecycleMainService, - crossAppIPCService, - )); - } + this._register(new NotAvailableUpdateDialog(updateService, accessor.get(IDialogMainService))); // Metered Connection const meteredConnectionChannel = new MeteredConnectionChannel(accessor.get(IMeteredConnectionService) as MeteredConnectionMainService); @@ -1394,16 +1336,8 @@ export class CodeApplication extends Disposable { const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP; const args = this.environmentMainService.args; - // If launched solely for cross-app secret sharing, don't open any windows - if (args['share-secrets-with-agents-app']) { - const hasOtherArgs = args._.length > 0 || args['folder-uri'] || args['file-uri']; - if (!hasOtherArgs) { - return []; - } - } - // Handle agents window first based on context - if ((process as INodeProcess).isEmbeddedApp || (args['agents'] && this.productService.quality !== 'stable')) { + if ((args['agents'] && this.productService.quality !== 'stable')) { return windowsMainService.openAgentsWindow({ context, cli: args, @@ -1411,6 +1345,21 @@ export class CodeApplication extends Disposable { }); } + const agentsLastRunning = this.productService.quality !== 'stable' + ? await tryConsumeAgentsLastRunningMarker(this.environmentMainService.userRoamingDataHome, this.fileService, this.logService) + : undefined; + if (agentsLastRunning?.agentsRunning) { + const agentsWindows = await windowsMainService.openAgentsWindow({ + context, + cli: args, + initialStartup: true + }); + if (!agentsLastRunning.vscodeRunning) { + return agentsWindows; + } + // Otherwise also restore the editor windows below. + } + // Then check for windows from protocol links to open if (initialProtocolUrls) { @@ -1699,16 +1648,6 @@ export class CodeApplication extends Disposable { }); } - // Agents app: write a marker into the host VS Code's user-data dir so - // that, after a future update which removes the sub-application, the - // host VS Code can detect that the Agents app was running and restore - // the appropriate windows on next launch. - if ((process as INodeProcess).isEmbeddedApp) { - const hostUserRoamingDataHome = this.environmentMainService.parentAppUserRoamingDataHome; - if (hostUserRoamingDataHome) { - this._register(instantiationService.createInstance(AgentsLastRunningTracker, hostUserRoamingDataHome)); - } - } } private async installMutex(): Promise { @@ -1790,33 +1729,40 @@ export class CodeApplication extends Disposable { // Validate Device ID is up to date (delay this as it has shown significant perf impact) // Refs: https://github.com/microsoft/vscode/issues/234064 validateDevDeviceId(this.stateService, this.logService); - - // macOS: eagerly register the embedded app with Launch Services - this.registerEmbeddedAppWithLaunchServices(); } - private registerEmbeddedAppWithLaunchServices(): void { - if (!isMacintosh || (process as INodeProcess).isEmbeddedApp || !this.productService.embedded?.nameShort || this.productService.quality === 'stable') { + private cleanupAgentsApplication(): void { + const cleanupKey = 'macAgentsSubAppCleanup.v1'; + if (this.stateService.getItem(cleanupKey, false)) { return; } - const stateKey = 'launchServices.registeredEmbeddedApp'; - const currentVersion = this.productService.version; - if (this.stateService.getItem(stateKey) === currentVersion) { - this.logService.trace('Embedded app already registered with Launch Services for this version, skipping.'); - return; - } - - // appRoot points to Contents/Resources/app on macOS - const embeddedAppPath = join(this.environmentMainService.appRoot, '..', '..', 'Applications', `${this.productService.embedded.nameLong}.app`); - const lsregister = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister'; - this.logService.trace('Registering embedded app with Launch Services:', embeddedAppPath); - const child = execFile(lsregister, ['-f', embeddedAppPath], { timeout: 30_000 }, (error) => { + const bundleId = 'com.microsoft.VSCodeAgentsInsiders'; + const script = [ + `plist="$HOME/Library/Preferences/com.apple.dock.plist"`, + `[ -f "$plist" ] || exit 0`, + `n=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps" "$plist" 2>/dev/null | grep -c "^ Dict {")`, + `changed=0`, + `i=$((n-1))`, + `while [ $i -ge 0 ]; do`, + ` bid=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:bundle-identifier" "$plist" 2>/dev/null)`, + ` if [ "$bid" = "${bundleId}" ]; then`, + ` /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2>/dev/null`, + ` changed=1`, + ` fi`, + ` i=$((i-1))`, + `done`, + `[ "$changed" = "1" ] && /usr/bin/killall -HUP Dock`, + `exit 0` + ].join('\n'); + + const child = exec(script, { timeout: 10_000, killSignal: 'SIGKILL' }, (error, _stdout, stderr) => { if (error) { - this.logService.error('Failed to register embedded app with Launch Services:', error.message); - } else { - this.stateService.setItem(stateKey, currentVersion); + this.logService.warn(`[agents] legacy sub-app cleanup failed: ${error.message}${stderr ? ` (${stderr.trim()})` : ''}`); + return; } + this.stateService.setItem(cleanupKey, true); + this.logService.info('[agents] legacy sub-app cleanup completed'); }); child.unref(); } diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index d12791a2f5d802..e390b34f4f6f5e 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -20,7 +20,6 @@ import { addArg, parseCLIProcessArgv } from '../../platform/environment/node/arg import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from '../../platform/environment/node/stdin.js'; import { createWaitMarkerFileSync } from '../../platform/environment/node/wait.js'; import product from '../../platform/product/common/product.js'; -import { resolveSiblingWindowsExePath } from '../../platform/native/node/siblingApp.js'; import { CancellationTokenSource } from '../../base/common/cancellation.js'; import { isUNC, randomPath } from '../../base/common/extpath.js'; import { Utils } from '../../platform/profiling/common/profiling.js'; @@ -493,18 +492,8 @@ export async function main(argv: string[]): Promise { options['stdio'] = ['ignore', 'pipe', 'ignore']; // restore ability to see output when --status is used } - // Figure out the app to launch: with --agents we try to launch the embedded app on Windows - let execToLaunch = process.execPath; - if (isWindows && args.agents) { - const siblingExe = resolveSiblingWindowsExePath(product); - if (siblingExe) { - execToLaunch = siblingExe; - argv = argv.filter(arg => arg !== '--agents'); - } - } - // We spawn the resolved executable directly - child = spawn(execToLaunch, argv.slice(2), options); + child = spawn(process.execPath, argv.slice(2), options); } else { // On macOS, we spawn using the open command to obtain behavior // similar to if the app was launched from the dock @@ -518,14 +507,7 @@ export async function main(argv: string[]): Promise { // This way, Mac does not automatically try to foreground the new instance, which causes // focusing issues when the new instance only sends data to a previous instance and then closes. const spawnArgs = ['-n', '-g']; - - // Figure out the app to launch: with --agents we try to launch the embedded app - if (args.agents && product.darwinSiblingBundleIdentifier) { - spawnArgs.push('-b', product.darwinSiblingBundleIdentifier); - argv = argv.filter(arg => arg !== '--agents'); - } else { - spawnArgs.push('-a', process.execPath); // -a opens the given application. - } + spawnArgs.push('-a', process.execPath); // -a opens the given application. if (args.verbose || args.status) { spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running) diff --git a/src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts b/src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts deleted file mode 100644 index 3c5173607cf037..00000000000000 --- a/src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts +++ /dev/null @@ -1,140 +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 * as electron from 'electron'; -import { TimeoutTimer } from '../../../base/common/async.js'; -import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; -import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { ILogService } from '../../log/common/log.js'; - -export const ICrossAppIPCService = createDecorator('crossAppIPCService'); - -export interface ICrossAppIPCMessage { - readonly type: string; - readonly data?: unknown; -} - -export interface ICrossAppIPCService { - readonly _serviceBrand: undefined; - - /** Whether the Electron crossAppIPC API is supported in this build. */ - readonly isSupported: boolean; - - /** Whether initialize() has been called and successfully set up the IPC. */ - readonly initialized: boolean; - - /** Whether the IPC connection is active. */ - readonly connected: boolean; - - /** Whether this app is the IPC server (`true`) or client (`false`). Only meaningful when connected. */ - readonly isServer: boolean; - - /** Fires when the peer connects. The boolean indicates whether this app is the server. */ - readonly onDidConnect: Event; - - /** Fires when the peer disconnects. The string is the disconnect reason. */ - readonly onDidDisconnect: Event; - - /** Fires when a message is received from the peer. */ - readonly onDidReceiveMessage: Event; - - /** Send a message to the peer. No-op if not connected. */ - sendMessage(msg: ICrossAppIPCMessage): void; - - /** Initialize the IPC connection. Call once during startup. */ - initialize(): void; -} - -/** - * Manages the single crossAppIPC connection for the entire application. - */ -export class CrossAppIPCService extends Disposable implements ICrossAppIPCService { - - declare readonly _serviceBrand: undefined; - - private ipc: Electron.CrossAppIPC | undefined; - private _connected = false; - private _isServer = false; - private readonly reconnectTimer = this._register(new TimeoutTimer()); - - private readonly _onDidConnect = this._register(new Emitter()); - readonly onDidConnect: Event = this._onDidConnect.event; - - private readonly _onDidDisconnect = this._register(new Emitter()); - readonly onDidDisconnect: Event = this._onDidDisconnect.event; - - private readonly _onDidReceiveMessage = this._register(new Emitter()); - readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; - - get isSupported(): boolean { - const crossAppIPC: Electron.CrossAppIPCModule | undefined = (electron as typeof electron & { crossAppIPC?: Electron.CrossAppIPCModule }).crossAppIPC; - return crossAppIPC !== undefined; - } - get initialized(): boolean { return this.ipc !== undefined; } - get connected(): boolean { return this._connected; } - get isServer(): boolean { return this._isServer; } - - constructor( - @ILogService private readonly logService: ILogService, - ) { - super(); - } - - initialize(): void { - if (this.ipc) { - return; // Already initialized - } - - const crossAppIPC: Electron.CrossAppIPCModule | undefined = (electron as typeof electron & { crossAppIPC?: Electron.CrossAppIPCModule }).crossAppIPC; - - if (!crossAppIPC) { - this.logService.info('CrossAppIPCService: crossAppIPC not available'); - return; - } - - const ipc = crossAppIPC.createCrossAppIPC(); - this.ipc = ipc; - - ipc.on('connected', () => { - this._connected = true; - this._isServer = ipc.isServer; - this.logService.info(`CrossAppIPCService: connected (isServer=${ipc.isServer})`); - this._onDidConnect.fire(ipc.isServer); - }); - - ipc.on('message', (messageEvent) => { - this._onDidReceiveMessage.fire(messageEvent.data as ICrossAppIPCMessage); - }); - - ipc.on('disconnected', (reason) => { - this.logService.info(`CrossAppIPCService: disconnected (${reason})`); - this._connected = false; - this._isServer = false; - this._onDidDisconnect.fire(reason); - - // Reconnect to wait for the peer's next launch. - // Delay briefly to allow the old Mach bootstrap service to be - // deregistered before re-creating the server endpoint (macOS). - if (reason === 'peer-disconnected') { - this.reconnectTimer.cancelAndSet(() => ipc.connect(), 1000); - } - }); - - ipc.connect(); - this.logService.info('CrossAppIPCService: connecting to peer'); - } - - sendMessage(msg: ICrossAppIPCMessage): void { - if (this.ipc?.connected) { - this.ipc.postMessage(msg); - } - } - - override dispose(): void { - this.ipc?.close(); - super.dispose(); - } -} diff --git a/src/vs/platform/encryption/electron-main/encryptionMainService.ts b/src/vs/platform/encryption/electron-main/encryptionMainService.ts index 93987884ac9f4a..1b4b1e05f28c77 100644 --- a/src/vs/platform/encryption/electron-main/encryptionMainService.ts +++ b/src/vs/platform/encryption/electron-main/encryptionMainService.ts @@ -4,19 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { safeStorage as safeStorageElectron, app } from 'electron'; -import { join } from '../../../base/common/path.js'; -import { INodeProcess, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { isMacintosh, isWindows } from '../../../base/common/platform.js'; import { KnownStorageProvider, IEncryptionMainService, PasswordStoreCLIOption } from '../common/encryptionService.js'; -import { getDefaultUserDataPath } from '../../environment/node/userDataPath.js'; import { ILogService } from '../../log/common/log.js'; -import { IProductService } from '../../product/common/productService.js'; // These APIs are currently only supported in our custom build of electron so // we need to guard against them not being available. interface ISafeStorageAdditionalAPIs { setUsePlainTextEncryption(usePlainText: boolean): void; getSelectedStorageBackend(): string; - initWithExistingKey(localStatePath: string): boolean; } const safeStorage: typeof import('electron').safeStorage & Partial = safeStorageElectron; @@ -25,8 +21,7 @@ export class EncryptionMainService implements IEncryptionMainService { _serviceBrand: undefined; constructor( - @ILogService private readonly logService: ILogService, - @IProductService private readonly productService: IProductService + @ILogService private readonly logService: ILogService ) { // if this commandLine switch is set, the user has opted in to using basic text encryption if (app.commandLine.getSwitchValue('password-store') === PasswordStoreCLIOption.basic) { @@ -34,40 +29,6 @@ export class EncryptionMainService implements IEncryptionMainService { safeStorage.setUsePlainTextEncryption?.(true); this.logService.trace('[EncryptionMainService] set usePlainTextEncryption to true'); } - - if (isWindows && (process as INodeProcess).isEmbeddedApp) { - this.initializeWithHostEncryptionKey(); - } - } - - private initializeWithHostEncryptionKey(): void { - if (!safeStorage.initWithExistingKey) { - this.logService.trace('[EncryptionMainService] initWithExistingKey API is not available'); - return; - } - - // embedded.win32SiblingExeBasename is derived from the host app's product.nameShort - // at build time, which is also the folder name used for the host's user data path. - const hostProductName = this.productService.embedded?.win32SiblingExeBasename; - if (!hostProductName) { - this.logService.warn('[EncryptionMainService] Host product name not available in embedded product config'); - return; - } - - const hostUserDataPath = getDefaultUserDataPath(hostProductName); - const localStatePath = join(hostUserDataPath, 'Local State'); - - this.logService.info(`[EncryptionMainService] Initializing encryption with host app key from: ${localStatePath}`); - try { - const result = safeStorage.initWithExistingKey(localStatePath); - if (result) { - this.logService.info('[EncryptionMainService] Successfully initialized encryption with host app key'); - } else { - this.logService.error('[EncryptionMainService] Failed to initialize encryption with host app key'); - } - } catch (e) { - this.logService.error('[EncryptionMainService] Error initializing encryption with host app key:', e); - } } async encrypt(value: string): Promise { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index fbec7dfe5c3949..5c0fa0cc32041d 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -109,7 +109,6 @@ export interface NativeParsedArgs { 'locate-extension'?: string[]; // undefined or array of 1 or more 'enable-proposed-api'?: string[]; // undefined or array of 1 or more 'open-url'?: boolean; - 'open-chat-session'?: string; 'skip-release-notes'?: boolean; 'skip-welcome'?: boolean; 'disable-telemetry'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index d36711390f280c..4fdf722707eb49 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -84,7 +84,6 @@ export interface IEnvironmentService { extensionLogLevel?: [string, string][]; verbose: boolean; isBuilt: boolean; - isEmbeddedApp?: boolean; // --- telemetry/exp disableTelemetry: boolean; @@ -93,43 +92,6 @@ export interface IEnvironmentService { // --- agent sessions workspace agentSessionsWorkspace?: URI; - - /** - * When running as the embedded app, the user roaming data home of - * the host VS Code application (i.e. the default profile's settings/User - * directory). `undefined` when not running as embedded. - */ - readonly parentAppUserRoamingDataHome?: URI; - - /** - * When running as the embedded app, the data home of the host - * VS Code application (e.g. `~/.vscode-insiders`). This identifies the - * host application's home/data directory and is used alongside other - * host-specific paths such as `hostUserRoamingDataHome` and - * `hostExtensionsHome`. `undefined` when not running as embedded. - */ - readonly parentAppUserHome?: URI; - - /** - * When running as the embedded app, the extensions directory of - * the host VS Code application. `undefined` when not running as embedded. - */ - readonly parentAppExtensionsHome?: URI; - - /** - * When running as the embedded app, the short display name of the - * parent VS Code application (e.g. "VS Code Insiders"). - * `undefined` when not running as embedded. - */ - readonly parentAppNameShort?: string; - - /** - * When running as the embedded app, the long display name of the - * parent VS Code application (e.g. "Visual Studio Code Insiders"). - * `undefined` when not running as embedded. - */ - readonly parentAppNameLong?: string; - // --- Policy policyFile?: URI; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 35ebd864985d7e..004d0614c938a3 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -37,20 +37,6 @@ export interface INativeEnvironmentPaths { * OS tmp dir. */ tmpDir: string; - - /** - * The parent application user data directory, if the current instance is running as an embedded application. - * This can be used to access data from the parent application that is not shared with the embedded application. - * This is only set when running as an embedded application and is `undefined` otherwise. - */ - parentAppUserDataDir: string | undefined; - - /** - * The parent application home directory, if the current instance is running as an embedded application. - * This can be used to access data from the parent application that is not shared with the embedded application. - * This is only set when running as an embedded application and is `undefined` otherwise. - */ - parentAppUserHomeDir: string | undefined; } export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService { @@ -313,41 +299,12 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron this.args['continueOn'] = value; } - @memoize - get parentAppUserRoamingDataHome(): URI | undefined { - return this.paths.parentAppUserDataDir ? URI.file(this.paths.parentAppUserDataDir).with({ scheme: Schemas.vscodeUserData }) : undefined; - } - - @memoize - get parentAppUserHome(): URI | undefined { - return this.paths.parentAppUserHomeDir ? URI.file(this.paths.parentAppUserHomeDir) : undefined; - } - - @memoize - get parentAppExtensionsHome(): URI | undefined { - if (!this.parentAppUserHome) { - return undefined; - } - return joinPath(this.parentAppUserHome, 'extensions'); - } - - @memoize - get parentAppNameShort(): string | undefined { - return getParentAppName(this.productService, this.isEmbeddedApp, 'short'); - } - - @memoize - get parentAppNameLong(): string | undefined { - return getParentAppName(this.productService, this.isEmbeddedApp, 'long'); - } - get args(): NativeParsedArgs { return this._args; } constructor( private readonly _args: NativeParsedArgs, private readonly paths: INativeEnvironmentPaths, - protected readonly productService: IProductService, - readonly isEmbeddedApp: boolean = false + protected readonly productService: IProductService ) { } } @@ -370,18 +327,3 @@ export function parseDebugParams(debugArg: string | undefined, debugBrkArg: stri return { port, break: brk, debugId, env }; } - -function getParentAppName(productService: IProductService, isEmbeddedApp: boolean, variant: 'short' | 'long'): string | undefined { - if (!isEmbeddedApp) { - return undefined; - } - const quality = productService.quality; - if (quality === 'stable') { - return variant === 'short' ? 'VS Code' : 'Visual Studio Code'; - } else if (quality === 'insider') { - return variant === 'short' ? 'VS Code Insiders' : 'Visual Studio Code Insiders'; - } else if (quality === 'exploration') { - return variant === 'short' ? 'VS Code Exploration' : 'Visual Studio Code Exploration'; - } - return undefined; -} diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 95216df31a4989..86800f721cf1dc 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -198,7 +198,6 @@ export const OPTIONS: OptionDescriptions> = { 'crash-reporter-id': { type: 'string' }, 'skip-add-to-recently-opened': { type: 'boolean' }, 'open-url': { type: 'boolean' }, - 'open-chat-session': { type: 'string' }, 'file-write': { type: 'boolean' }, 'file-chmod': { type: 'boolean' }, 'install-builtin-extension': { type: 'string[]' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 28d299dd8e3672..8652144b5634c0 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -9,9 +9,6 @@ import { IDebugParams } from '../common/environment.js'; import { AbstractNativeEnvironmentService, parseDebugParams } from '../common/environmentService.js'; import { getUserDataPath } from './userDataPath.js'; import { IProductService } from '../../product/common/productService.js'; -import { INodeProcess } from '../../../base/common/platform.js'; -import { join } from '../../../base/common/path.js'; -import { env } from '../../../base/common/process.js'; export class NativeEnvironmentService extends AbstractNativeEnvironmentService { @@ -21,9 +18,7 @@ export class NativeEnvironmentService extends AbstractNativeEnvironmentService { homeDir, tmpDir: tmpdir(), userDataDir: getUserDataPath(args, productService.nameShort), - parentAppUserDataDir: getParentAppUserDataDir(args, productService), - parentAppUserHomeDir: getParentAppUserHomeDir(homeDir, productService) - }, productService, isEmbeddedApp()); + }, productService); } } @@ -38,55 +33,3 @@ export function parseAgentHostDebugPort(args: NativeParsedArgs, isBuilt: boolean export function parseSharedProcessDebugPort(args: NativeParsedArgs, isBuilt: boolean): IDebugParams { return parseDebugParams(args['inspect-sharedprocess'], args['inspect-brk-sharedprocess'], 5879, isBuilt, args.extensionEnvironment); } - - -function getParentAppUserDataDir(args: NativeParsedArgs, productService: IProductService): string | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { - return undefined; - } - if (env['VSCODE_DEV']) { - return undefined; - } - const quality = productService.quality; - let hostProductName: string; - if (quality === 'stable') { - hostProductName = 'Code'; - } else if (quality === 'insider') { - hostProductName = 'Code - Insiders'; - } else if (quality === 'exploration') { - hostProductName = 'Code - Exploration'; - } else { - return undefined; - } - - // Honor the same env-var overrides that the host VS Code itself uses - // (portable mode and VSCODE_APPDATA), but intentionally skip --user-data-dir - // because that CLI arg belongs to the Agents app, not the host. - const hostUserDataPath = getUserDataPath(args, hostProductName); - return join(hostUserDataPath, 'User'); -} - -function getParentAppUserHomeDir(homeDir: string, productService: IProductService): string | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { - return undefined; - } - if (env['VSCODE_DEV']) { - return undefined; - } - const quality = productService.quality; - let hostDataFolderName: string; - if (quality === 'stable') { - hostDataFolderName = '.vscode'; - } else if (quality === 'insider') { - hostDataFolderName = '.vscode-insiders'; - } else if (quality === 'exploration') { - hostDataFolderName = '.vscode-exploration'; - } else { - return undefined; - } - return join(homeDir, hostDataFolderName); -} - -function isEmbeddedApp(): boolean { - return !!(process as INodeProcess).isEmbeddedApp; -} diff --git a/src/vs/platform/environment/node/userDataPath.ts b/src/vs/platform/environment/node/userDataPath.ts index be9d71b601bdd5..98b4411b4217fe 100644 --- a/src/vs/platform/environment/node/userDataPath.ts +++ b/src/vs/platform/environment/node/userDataPath.ts @@ -5,7 +5,6 @@ import { homedir } from 'os'; import { NativeParsedArgs } from '../common/argv.js'; -import { INodeProcess } from '../../../base/common/platform.js'; // This file used to be a pure JS file and was always // importing `path` from node.js even though we ship // our own version of the library and prefer to use @@ -46,11 +45,7 @@ function doGetUserDataPath(cliArgs: NativeParsedArgs, productName: string): stri // 0. Running out of sources has a fixed productName if (process.env['VSCODE_DEV']) { - if ((process as INodeProcess).isEmbeddedApp) { - productName = 'agents-oss-dev'; - } else { - productName = 'code-oss-dev'; - } + productName = 'code-oss-dev'; } // 1. Support portable mode diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 7815fdff84402a..b45f24b6889115 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -5,7 +5,6 @@ import { app } from 'electron'; import { coalesce } from '../../../base/common/arrays.js'; -import { Emitter, Event } from '../../../base/common/event.js'; import { IProcessEnvironment, isMacintosh } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { whenDeleted } from '../../../base/node/pfs.js'; @@ -36,21 +35,12 @@ export interface ILaunchMainService { start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise; getMainProcessId(): Promise; - - /** - * Fires when a second instance sends `--share-secrets-with-agents-app`. - * Used for cross-app secret migration. - */ - readonly onDidRequestShareSecrets: Event; } export class LaunchMainService implements ILaunchMainService { declare readonly _serviceBrand: undefined; - private readonly _onDidRequestShareSecrets = new Emitter(); - readonly onDidRequestShareSecrets: Event = this._onDidRequestShareSecrets.event; - constructor( @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @@ -62,14 +52,6 @@ export class LaunchMainService implements ILaunchMainService { async start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { this.logService.trace('Received data from other instance: ', args, userEnv); - // Handle --share-secrets-with-agents-app from a second instance: - // trigger the secret sharing handshake without opening any window. - if (args['share-secrets-with-agents-app']) { - this.logService.info('Received --share-secrets-with-agents-app from second instance'); - this._onDidRequestShareSecrets.fire(); - return; - } - // macOS: Electron > 7.x changed its behaviour to not // bring the application to the foreground when a window // is focused programmatically. Only via `app.focus` and diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index d255fb14f3aad1..968960dc58b4a4 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -131,14 +131,6 @@ export interface ICommonNativeHostService { openAgentsWindow(options?: { folderUri?: UriComponents }): Promise; - /** - * Launches the sibling application (host ↔ embedded). - * The launched process is detached with its own process group. - * - * @param args CLI arguments to pass to the sibling application. - */ - launchSiblingApp(args?: string[]): Promise; - isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 1cfa2a170cb2cc..a809171d570db9 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -18,7 +18,6 @@ import { AddFirstParameterToFunctions } from '../../../base/common/types.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { virtualMachineHint } from '../../../base/node/id.js'; import { Promises, SymlinkSupport } from '../../../base/node/pfs.js'; -import { launchSiblingApp } from '../node/siblingApp.js'; import { findFreePort, isPortFree } from '../../../base/node/ports.js'; import { localize } from '../../../nls.js'; import { ISerializableCommandAction } from '../../action/common/action.js'; @@ -316,35 +315,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } - async launchSiblingApp(_windowId: number | undefined, args?: string[]): Promise { - const finalArgs = [...(args ?? [])]; - - // Forward transient dirs to the sibling app so it runs fully isolated - const agentsUserDataDir = this.environmentMainService.args['agents-user-data-dir']; - if (agentsUserDataDir) { - finalArgs.push('--user-data-dir', agentsUserDataDir); - } - const agentsExtensionsDir = this.environmentMainService.args['agents-extensions-dir']; - if (agentsExtensionsDir) { - finalArgs.push('--extensions-dir', agentsExtensionsDir); - } - const sharedDataDir = this.environmentMainService.args['shared-data-dir']; - if (sharedDataDir) { - finalArgs.push('--shared-data-dir', sharedDataDir); - } - const agentPluginsDir = this.environmentMainService.args['agent-plugins-dir']; - if (agentPluginsDir) { - finalArgs.push('--agent-plugins-dir', agentPluginsDir); - } - - const result = launchSiblingApp(this.productService, finalArgs, err => { - this.logService.error('[launchSiblingApp] Failed to spawn sibling app:', err.message); - }); - if (!result) { - this.logService.warn('[launchSiblingApp] Could not resolve sibling app on this platform'); - } - } - async isFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); return window?.isFullScreen ?? false; diff --git a/src/vs/platform/native/node/siblingApp.ts b/src/vs/platform/native/node/siblingApp.ts deleted file mode 100644 index 78228bf9f3e928..00000000000000 --- a/src/vs/platform/native/node/siblingApp.ts +++ /dev/null @@ -1,95 +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 { ChildProcess, spawn } from 'child_process'; -import { statSync } from 'fs'; -import { dirname, join } from '../../../base/common/path.js'; -import { isMacintosh, isWindows, INodeProcess } from '../../../base/common/platform.js'; -import { IProductConfiguration } from '../../../base/common/product.js'; - -export interface ISiblingAppLaunchResult { - readonly child: ChildProcess; -} - -/** - * Launches the sibling application (host ↔ embedded) using a detached - * child process with its own process group. - * - * @param product The product configuration of the **current** process. - * @param args CLI arguments to forward to the sibling app. - * @param onError Optional callback invoked when the spawned process emits an error. - * @returns The spawned detached child process, or `undefined` if the - * sibling could not be resolved on the current platform. - */ -export function launchSiblingApp(product: IProductConfiguration, args: string[] = [], onError?: (err: Error) => void): ISiblingAppLaunchResult | undefined { - if (isMacintosh) { - const bundleId = resolveSiblingDarwinBundleIdentifier(product); - if (!bundleId) { - return undefined; - } - const spawnArgs = ['-n', '-g', '-b', bundleId]; - if (args.length > 0) { - spawnArgs.push('--args', ...args); - } - const child = spawn('open', spawnArgs, { - detached: true, - stdio: 'ignore', - }); - child.on('error', err => onError?.(err)); - child.unref(); - return { child }; - } - - if (isWindows) { - const exePath = resolveSiblingWindowsExePath(product); - if (!exePath) { - return undefined; - } - const child = spawn(exePath, args, { - detached: true, - stdio: 'ignore', - }); - child.on('error', err => onError?.(err)); - child.unref(); - return { child }; - } - - return undefined; -} - -/** - * Returns the macOS bundle identifier for the sibling app. - */ -function resolveSiblingDarwinBundleIdentifier(product: IProductConfiguration): string | undefined { - const isEmbedded = !!(process as INodeProcess).isEmbeddedApp; - return isEmbedded - ? product.embedded?.darwinSiblingBundleIdentifier - : product.darwinSiblingBundleIdentifier; -} - -/** - * Resolves the sibling app's Windows executable path. - */ -export function resolveSiblingWindowsExePath(product: IProductConfiguration): string | undefined { - const isEmbedded = !!(process as INodeProcess).isEmbeddedApp; - const siblingBasename = isEmbedded - ? product.embedded?.win32SiblingExeBasename - : product.win32SiblingExeBasename; - - if (!siblingBasename) { - return undefined; - } - - const siblingExe = join(dirname(process.execPath), `${siblingBasename}.exe`); - try { - if (statSync(siblingExe).isFile()) { - return siblingExe; - } - } catch { - // may not exist on disk - } - - return undefined; -} diff --git a/src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts b/src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts deleted file mode 100644 index 6dfea9bc15a96f..00000000000000 --- a/src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts +++ /dev/null @@ -1,315 +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 { execFile } from 'child_process'; -import { dirname } from '../../../base/common/path.js'; -import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -import { ILogService } from '../../log/common/log.js'; -import { IEncryptionMainService } from '../../encryption/common/encryptionService.js'; -import { IStorageMainService } from '../../storage/electron-main/storageMainService.js'; -import { CROSS_APP_SHARED_SECRET_KEYS, secretStorageKey, readEncryptedSecret, writeEncryptedSecret } from '../common/secrets.js'; -import { IStateService } from '../../state/node/state.js'; -import { INodeProcess, isMacintosh } from '../../../base/common/platform.js'; -import { IStorageMain } from '../../storage/electron-main/storageMain.js'; -import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; -import { ILaunchMainService } from '../../launch/electron-main/launchMainService.js'; -import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; -import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; - -const MIGRATION_STATE_KEY = 'crossAppSecretSharing.migrationDone'; - -/** - * Message types exchanged between apps over crossAppIPC for secret sharing. - */ -const enum CrossAppSecretMessageType { - /** Agents → Host: Request secrets */ - SecretRequest = 'secrets/request', - /** Host → Agents: Response with secrets */ - SecretResponse = 'secrets/response', - /** Agents → Host: Confirms secrets were stored, both sides mark migration done */ - SecretAck = 'secrets/ack', -} - -interface CrossAppSecretMessage { - type: CrossAppSecretMessageType; - data?: Record; -} - -/** - * Coordinates one-time secret migration between the VS Code app and the - * agents app using Electron's crossAppIPC (macOS only). - * - * **Demand-driven**: Only the agents app initiates migration. If it - * detects that migration hasn't been done yet, it: - * 1. Waits for the crossAppIPC connection (managed by ICrossAppIPCService). - * 2. Spawns Code.app with `--share-secrets-with-agents-app`, which - * either starts Code.app fresh or (if already running) forwards - * the arg to the existing instance via the node IPC socket. - * 3. Code.app creates its own crossAppIPC connection when it sees - * the arg, and the two connect. - * 4. Agents app sends `SecretRequest` → Code.app responds with - * `SecretResponse` → Agents app sends `SecretAck`. - * 5. Both sides mark migration as done. Code.app quits if it was - * launched solely for this purpose. - * - * Security: crossAppIPC uses code-signature verification (Mach ports - * on macOS) — the kernel authenticates both endpoints. No secrets are - * ever in process args, files, or network. - */ -export class MacOSCrossAppSecretSharing extends Disposable { - - private readonly isEmbeddedApp: boolean; - private readonly applicationStorage: IStorageMain; - private _onHostMigrationComplete: (() => void) | undefined; - private readonly hostHandshakeListeners = this._register(new DisposableStore()); - - constructor( - storageMainService: IStorageMainService, - private readonly encryptionMainService: IEncryptionMainService, - private readonly stateService: IStateService, - private readonly logService: ILogService, - environmentMainService: IEnvironmentMainService, - launchMainService: ILaunchMainService, - lifecycleMainService: ILifecycleMainService, - private readonly crossAppIPCService: ICrossAppIPCService, - ) { - super(); - this.isEmbeddedApp = !!(process as INodeProcess).isEmbeddedApp; - this.applicationStorage = storageMainService.applicationStorage; - this.initialize(environmentMainService, launchMainService, lifecycleMainService); - } - - private initialize( - environmentMainService: IEnvironmentMainService, - launchMainService: ILaunchMainService, - lifecycleMainService: ILifecycleMainService, - ): void { - if (this.isEmbeddedApp) { - // Agents app: initiate migration if needed - this.initializeAsAgentsApp(); - } else if (environmentMainService.args['share-secrets-with-agents-app']) { - // Code.app launched fresh with --share-secrets-with-agents-app: - // respond to the agents app's request, then quit if no other reason to stay - const hasOtherArgs = environmentMainService.args._.length > 0 || environmentMainService.args['folder-uri'] || environmentMainService.args['file-uri']; - this.initializeAsHostApp(hasOtherArgs ? undefined : () => { - this.logService.info('[CrossAppSecretSharing] Host app was launched for migration only, quitting'); - lifecycleMainService.quit(); - }); - } else { - // Code.app already running: listen for --share-secrets-with-agents-app - // forwarded from a second instance via the launch service - this._register(launchMainService.onDidRequestShareSecrets(() => { - this.initializeAsHostApp(); - })); - } - } - - private async initializeAsAgentsApp(): Promise { - if (!isMacintosh || !this.isEmbeddedApp) { - return; - } - - if (this.isMigrationDone()) { - this.logService.trace('[CrossAppSecretSharing] Migration already done, skipping'); - return; - } - - // Wait for storage to be ready before we start — handleSecretResponse - // will write secrets into applicationStorage. - await this.applicationStorage.whenInit; - - if (!this.crossAppIPCService.initialized) { - this.logService.info('[CrossAppSecretSharing] crossAppIPC not initialized, skipping migration'); - return; - } - - this.logService.info('[CrossAppSecretSharing] Migration needed, starting...'); - - // Listen for connection — when connected, request secrets - this._register(this.crossAppIPCService.onDidConnect(isServer => { - this.logService.info(`[CrossAppSecretSharing] Connected (isServer=${isServer}), requesting secrets from host app`); - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretRequest }); - })); - - // Listen for messages - this._register(this.crossAppIPCService.onDidReceiveMessage(msg => { - const secretMsg = msg as CrossAppSecretMessage; - if (secretMsg?.type === CrossAppSecretMessageType.SecretResponse) { - this.handleSecretResponse(secretMsg.data ?? {}); - } - })); - - // If already connected (e.g. service was initialized before storage was ready), - // send the request immediately. - if (this.crossAppIPCService.connected) { - this.logService.info(`[CrossAppSecretSharing] Already connected (isServer=${this.crossAppIPCService.isServer}), requesting secrets from host app`); - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretRequest }); - } - - // Spawn Code.app with --share-secrets-with-agents-app - this.spawnHostApp(); - - // Timeout: if migration doesn't complete within 30s, give up - setTimeout(() => { - if (!this.isMigrationDone()) { - this.logService.warn('[CrossAppSecretSharing] Migration timed out'); - } - }, 30_000); - } - - private async initializeAsHostApp(onComplete?: () => void): Promise { - if (!isMacintosh || this.isEmbeddedApp) { - onComplete?.(); - return; - } - - if (this.isMigrationDone()) { - this.logService.trace('[CrossAppSecretSharing] Migration already done, skipping'); - onComplete?.(); - return; - } - - // Wait for application storage to be fully initialized before - // checking for secrets — storage may still be in-memory at this - // point during early startup. - await this.applicationStorage.whenInit; - - if (!this.hasAnySharedSecrets()) { - this.logService.trace('[CrossAppSecretSharing] No shared secrets to share, skipping'); - onComplete?.(); - return; - } - - if (!this.crossAppIPCService.initialized) { - this.logService.info('[CrossAppSecretSharing] crossAppIPC not initialized'); - onComplete?.(); - return; - } - - this._onHostMigrationComplete = onComplete; - - this.logService.info('[CrossAppSecretSharing] Host app responding to secret sharing request'); - - // Dispose previous listeners if initializeAsHostApp is called again - // (e.g. via repeated onDidRequestShareSecrets events). - this.hostHandshakeListeners.clear(); - - // Listen for messages from the agents app - this.hostHandshakeListeners.add(this.crossAppIPCService.onDidReceiveMessage(msg => { - const secretMsg = msg as CrossAppSecretMessage; - if (secretMsg?.type === CrossAppSecretMessageType.SecretRequest) { - this.handleSecretRequest(); - } else if (secretMsg?.type === CrossAppSecretMessageType.SecretAck) { - this.handleSecretAck(); - } - })); - - // If disconnected before ack, still allow the host to quit - this.hostHandshakeListeners.add(this.crossAppIPCService.onDidDisconnect(() => { - this._onHostMigrationComplete?.(); - this._onHostMigrationComplete = undefined; - })); - } - - private isMigrationDone(): boolean { - return this.stateService.getItem(MIGRATION_STATE_KEY, false); - } - - private hasAnySharedSecrets(): boolean { - for (const key of CROSS_APP_SHARED_SECRET_KEYS) { - if (this.applicationStorage.get(secretStorageKey(key)) !== undefined) { - return true; - } - } - return false; - } - - private spawnHostApp(): void { - // Agents app's process.execPath: - // /Contents/Applications//Contents/MacOS/Electron - // Code.app bundle is 6 directories up: - // MacOS → Contents → → Applications → Contents → - const codeAppBundle = dirname(dirname(dirname(dirname(dirname(dirname(process.execPath)))))); - - this.logService.info('[CrossAppSecretSharing] Spawning host app:', codeAppBundle); - - const child = execFile('open', [ - '-a', codeAppBundle, - '-n', // new instance (so args are passed even if already running) - '-g', // don't bring to front - '--args', '--share-secrets-with-agents-app', - ], (error) => { - if (error) { - this.logService.error('[CrossAppSecretSharing] Failed to spawn host app:', error.message); - } - }); - child.unref(); - } - - private async handleSecretRequest(): Promise { - this.logService.info('[CrossAppSecretSharing] Host app handling secret request'); - - const secrets: Record = {}; - - for (const key of CROSS_APP_SHARED_SECRET_KEYS) { - try { - const decrypted = await readEncryptedSecret( - key, - (fullKey) => this.applicationStorage.get(fullKey), - (value) => this.encryptionMainService.decrypt(value), - this.logService, - ); - if (decrypted !== undefined) { - secrets[key] = decrypted; - } - } catch (err) { - this.logService.error('[CrossAppSecretSharing] Failed to read secret for key:', key, err); - } - } - - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretResponse, data: secrets }); - this.logService.info('[CrossAppSecretSharing] Sent secrets response with', Object.keys(secrets).length, 'keys'); - } - - private async handleSecretResponse(secrets: Record): Promise { - this.logService.info('[CrossAppSecretSharing] Agents app received', Object.keys(secrets).length, 'secrets'); - - for (const [key, value] of Object.entries(secrets)) { - if (!CROSS_APP_SHARED_SECRET_KEYS.includes(key)) { - this.logService.warn('[CrossAppSecretSharing] Ignoring unexpected key:', key); - continue; - } - - try { - await writeEncryptedSecret( - key, - value, - (fullKey, encrypted) => this.applicationStorage.set(fullKey, encrypted), - (v) => this.encryptionMainService.encrypt(v), - this.logService, - ); - } catch (err) { - this.logService.error('[CrossAppSecretSharing] Failed to store secret for key:', key, err); - } - } - - this.stateService.setItem(MIGRATION_STATE_KEY, true); - this.logService.info('[CrossAppSecretSharing] Migration complete'); - - // Tell the host app migration is done so it can also record it. - // Don't close here — let the host close first after receiving the ack. - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretAck }); - } - - private handleSecretAck(): void { - this.stateService.setItem(MIGRATION_STATE_KEY, true); - this.logService.info('[CrossAppSecretSharing] Host app received ack, migration complete on both sides'); - - const onComplete = this._onHostMigrationComplete; - this._onHostMigrationComplete = undefined; - - onComplete?.(); - } -} diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 90c8db613ddd25..a56091d0ff79a7 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -12,8 +12,8 @@ import { join } from '../../../base/common/path.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; import { URI } from '../../../base/common/uri.js'; import { Promises } from '../../../base/node/pfs.js'; -import { InMemoryStorageDatabase, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage, StorageHint, StorageState, MigratingStorage } from '../../../base/parts/storage/common/storage.js'; -import { ISQLiteStorageDatabaseLoggingOptions, ISQLiteStorageDatabaseOptions, SQLiteStorageDatabase } from '../../../base/parts/storage/node/storage.js'; +import { InMemoryStorageDatabase, IStorage, Storage, StorageHint, StorageState, MigratingStorage } from '../../../base/parts/storage/common/storage.js'; +import { ISQLiteStorageDatabaseLoggingOptions, SQLiteStorageDatabase } from '../../../base/parts/storage/node/storage.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IFileService } from '../../files/common/files.js'; import { ILogService, LogLevel } from '../../log/common/log.js'; @@ -22,7 +22,6 @@ import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfil import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from '../../telemetry/common/telemetry.js'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { Schemas } from '../../../base/common/network.js'; -import { ICrossAppIPCMessage, ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; export interface IStorageMainOptions { @@ -361,15 +360,12 @@ export class ApplicationSharedStorageMain extends BaseStorageMain { return undefined; } - private sharedDatabase: SharedSQLiteStorageDatabase | undefined; - constructor( private readonly options: IStorageMainOptions, private readonly storageFolderPath: string, private readonly applicationStorage: IStorageMain, logService: ILogService, fileService: IFileService, - private readonly crossAppIPCService: ICrossAppIPCService, ) { super(logService, fileService); } @@ -379,32 +375,19 @@ export class ApplicationSharedStorageMain extends BaseStorageMain { this.logService.info(`[shared storage] Creating shared storage database at '${storageFilePath}' (wasCreated: ${wasCreated})`); - this.sharedDatabase = new SharedSQLiteStorageDatabase(storageFilePath, { - logging: this.createLoggingOptions(), - useWAL: true, - busyTimeout: 2000 - }, this.crossAppIPCService, this.logService); - this._register(this.sharedDatabase); + const database = new SQLiteStorageDatabase(storageFilePath, { + logging: this.createLoggingOptions() + }); - this.logService.info(`[shared storage] Initializing fallback application storage (type: ${this.applicationStorage instanceof HostApplicationStorageMain ? 'host' : 'local'}, path: ${this.applicationStorage.path ?? 'in-memory'})`); + this.logService.info(`[shared storage] Initializing fallback application storage (path: ${this.applicationStorage.path ?? 'in-memory'})`); await this.applicationStorage.init(); this.logService.info(`[shared storage] Fallback application storage initialized with ${this.applicationStorage.items.size} items`); - const migratingStorage = this._register(new MigratingStorage(this.sharedDatabase, { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined })); - migratingStorage.setFallbackStorage(this.applicationStorage.storage, this.applicationStorage instanceof HostApplicationStorageMain); + const migratingStorage = this._register(new MigratingStorage(database, { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined })); + migratingStorage.setFallbackStorage(this.applicationStorage.storage, false); return migratingStorage; } - protected override async doInit(storage: IStorage): Promise { - await super.doInit(storage); - - // Mark the shared database as initialized so that - // cross-app IPC messages are processed from now on. - // This must happen after Storage.init() completes to - // avoid processing stale queued messages. - this.sharedDatabase?.setInitialized(); - } - get applicationStorageItems(): Map { return this.applicationStorage.items; } @@ -427,29 +410,6 @@ export class ApplicationSharedStorageMain extends BaseStorageMain { } } -export class HostApplicationStorageMain extends BaseStorageMain { - - constructor( - readonly path: string, - logService: ILogService, - fileService: IFileService - ) { - super(logService, fileService); - } - - protected async doCreate(): Promise { - this.logService.info(`[shared storage] Opening host application storage at '${this.path}'`); - try { - const storage = new Storage(new SQLiteStorageDatabase(this.path, { logging: this.createLoggingOptions() })); - return storage; - } catch (error) { - this.logService.error(`[shared storage] Failed to open host application storage at '${this.path}': ${error}`); - throw error; - } - } - -} - export class WorkspaceStorageMain extends BaseStorageMain { private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; @@ -528,102 +488,6 @@ export class WorkspaceStorageMain extends BaseStorageMain { } } -const enum SharedStorageMessageType { - Changed = 'sharedStorage:changed' -} - -interface ISharedStorageChangedMessage extends ICrossAppIPCMessage { - readonly type: SharedStorageMessageType.Changed; - readonly data: { - readonly changed?: [string, string][]; - readonly deleted?: string[]; - }; -} - -/** - * A SQLite storage database wrapper that detects external changes - * via CrossAppIPC. When the sibling app (VS Code or Sessions app) - * writes to the shared storage, it sends an IPC message with the - * changed keys for instant notification. - */ -class SharedSQLiteStorageDatabase extends Disposable implements IStorageDatabase { - - private readonly _onDidChangeItemsExternal = this._register(new Emitter()); - readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; - - private readonly database: SQLiteStorageDatabase; - private initialized = false; - - constructor( - path: string, - options: ISQLiteStorageDatabaseOptions | undefined, - private readonly crossAppIPCService: ICrossAppIPCService, - private readonly logService: ILogService - ) { - super(); - - this.database = new SQLiteStorageDatabase(path, options); - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.crossAppIPCService.onDidReceiveMessage(msg => { - if (msg.type !== SharedStorageMessageType.Changed) { - return; - } - - if (!this.initialized) { - this.logService.trace('[shared storage] Ignoring cross-app IPC message received before initialization'); - return; - } - - const { changed, deleted } = (msg as ISharedStorageChangedMessage).data; - this.logService.trace(`[shared storage] Received cross-app IPC change: ${changed?.length ?? 0} changed, ${deleted?.length ?? 0} deleted`); - - this._onDidChangeItemsExternal.fire({ - changed: changed ? new Map(changed) : undefined, - deleted: deleted ? new Set(deleted) : undefined - }); - })); - } - - setInitialized(): void { - this.initialized = true; - } - - async getItems(): Promise> { - const items = await this.database.getItems(); - this.logService.trace(`[shared storage] Initialized with ${items.size} items`); - return items; - } - - async updateItems(request: IUpdateRequest): Promise { - await this.database.updateItems(request); - - const changedCount = request.insert?.size ?? 0; - const deletedCount = request.delete?.size ?? 0; - this.logService.trace(`[shared storage] Sending cross-app IPC change: ${changedCount} changed, ${deletedCount} deleted`); - - // Notify the sibling app via IPC - this.crossAppIPCService.sendMessage({ - type: SharedStorageMessageType.Changed, - data: { - changed: request.insert ? Array.from(request.insert.entries()) : undefined, - deleted: request.delete ? Array.from(request.delete.values()) : undefined - } - }); - } - - async optimize(): Promise { - return this.database.optimize(); - } - - async close(recovery?: () => Map): Promise { - return this.database.close(recovery); - } -} - export class InMemoryStorageMain extends BaseStorageMain { get path(): string | undefined { diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 35eb51cc274d57..d31880cc9b1d9a 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -14,13 +14,12 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; import { AbstractStorageService, isProfileUsingDefaultStorage, IStorageService, StorageScope, StorageTarget } from '../common/storage.js'; -import { ApplicationStorageMain, ApplicationSharedStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain, IStorageChangeEvent, HostApplicationStorageMain } from './storageMain.js'; +import { ApplicationStorageMain, ApplicationSharedStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain, IStorageChangeEvent } from './storageMain.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesMainService } from '../../userDataProfile/electron-main/userDataProfile.js'; import { IAnyWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { Schemas } from '../../../base/common/network.js'; -import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; //#region Storage Main Service (intent: make application, profile and workspace storage accessible to windows from main process) @@ -96,7 +95,6 @@ export class StorageMainService extends Disposable implements IStorageMainServic @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IFileService private readonly fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @ICrossAppIPCService private readonly crossAppIPCService: ICrossAppIPCService, ) { super(); @@ -204,29 +202,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic const sharedStorageFolderPath = join(this.environmentService.appSharedDataHome.with({ scheme: Schemas.file }).fsPath, 'sharedStorage'); - // Determine the fallback storage for transparent migration of keys - // from APPLICATION to APPLICATION_SHARED scope: - // In VS Code: reuse the own application storage (keys are local) - let fallbackStorage: IStorageMain = this.applicationStorage; - const hostUserRoamingDataHome = this.environmentService.parentAppUserRoamingDataHome; - if (hostUserRoamingDataHome) { - // - In the Agents App: create a storage backed by the host (VS Code) - // app's application DB so keys are found even if VS Code hasn't - // migrated them to the shared DB yet. - // We use ProfileStorageMain (not ApplicationStorageMain) to avoid - // writing telemetry state into the host app's DB — this is read-only. - const hostApplicationStoragePath = join(hostUserRoamingDataHome.with({ scheme: Schemas.file }).fsPath, 'globalStorage', 'state.vscdb'); - this.logService.info(`StorageMainService: creating application shared storage with host app fallback at '${hostApplicationStoragePath}'`); - fallbackStorage = this._register(new HostApplicationStorageMain( - hostApplicationStoragePath, - this.logService, - this.fileService - )); - } else { - this.logService.info(`StorageMainService: creating application shared storage with local application storage fallback`); - } - - const applicationSharedStorage = new ApplicationSharedStorageMain(this.getStorageOptions(), sharedStorageFolderPath, fallbackStorage, this.logService, this.fileService, this.crossAppIPCService); + // Use the local application storage as fallback for transparent migration + // of keys from APPLICATION to APPLICATION_SHARED scope. The agents window is + // now part of the same VS Code app, so there is no separate "host" app DB to + // fall back to. + const applicationSharedStorage = new ApplicationSharedStorageMain(this.getStorageOptions(), sharedStorageFolderPath, this.applicationStorage, this.logService, this.fileService); this._register(Event.once(applicationSharedStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed application shared storage`); diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index eef84a5d3517a4..1ed6be2376c157 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { notStrictEqual, ok, strictEqual } from 'assert'; +import { notStrictEqual, strictEqual } from 'assert'; import { Schemas } from '../../../../base/common/network.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; @@ -26,8 +26,6 @@ import { UserDataProfilesMainService } from '../../../userDataProfile/electron-m import { TestLifecycleMainService } from '../../../test/electron-main/workbenchTestServices.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { ICrossAppIPCMessage, ICrossAppIPCService } from '../../../crossAppIpc/electron-main/crossAppIpcService.js'; suite('StorageMainService', function () { @@ -35,19 +33,6 @@ suite('StorageMainService', function () { const productService: IProductService = { _serviceBrand: undefined, ...product }; - const nullCrossAppIPCService: ICrossAppIPCService = { - _serviceBrand: undefined, - isSupported: false, - initialized: false, - connected: false, - isServer: false, - onDidConnect: Event.None, - onDidDisconnect: Event.None, - onDidReceiveMessage: Event.None, - sendMessage: () => { }, - initialize: () => { } - }; - const inMemoryProfileRoot = URI.file('/location').with({ scheme: Schemas.inMemory }); const inMemoryProfile: IUserDataProfile = { id: 'id', @@ -132,7 +117,7 @@ suite('StorageMainService', function () { const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService); const fileService = disposables.add(new FileService(new NullLogService())); const uriIdentityService = disposables.add(new UriIdentityService(fileService)); - const testStorageService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService(), productService)), lifecycleMainService, fileService, uriIdentityService, nullCrossAppIPCService)); + const testStorageService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService(), productService)), lifecycleMainService, fileService, uriIdentityService)); disposables.add(testStorageService.applicationStorage); @@ -282,90 +267,6 @@ suite('StorageMainService', function () { strictEqual(didCloseWorkspaceStorage, true); }); - test('application shared storage receives cross-app IPC changes', async function () { - const onDidReceiveMessage = disposables.add(new Emitter()); - const sentMessages: ICrossAppIPCMessage[] = []; - const crossAppIPCService: ICrossAppIPCService = { - _serviceBrand: undefined, - isSupported: true, - initialized: true, - connected: true, - isServer: false, - onDidConnect: Event.None, - onDidDisconnect: Event.None, - onDidReceiveMessage: onDidReceiveMessage.event, - sendMessage: (msg) => sentMessages.push(msg), - initialize: () => { } - }; - - const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService); - const fileService = disposables.add(new FileService(new NullLogService())); - const uriIdentityService = disposables.add(new UriIdentityService(fileService)); - const storageMainService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService(), productService)), new TestLifecycleMainService(), fileService, uriIdentityService, crossAppIPCService)); - - const storage = storageMainService.applicationSharedStorage; - disposables.add(storage); - await storage.init(); - - // Verify that receiving a cross-app IPC message triggers a change event - let changeEvent: IStorageChangeEvent | undefined; - disposables.add(storage.onDidChangeStorage(e => { changeEvent = e; })); - - onDidReceiveMessage.fire({ - type: 'sharedStorage:changed', - data: { - changed: [['externalKey', 'externalValue']], - deleted: undefined - } - }); - - strictEqual(changeEvent?.key, 'externalKey'); - strictEqual(storage.get('externalKey'), 'externalValue'); - - // Verify that storing a value sends a cross-app IPC message - // (close flushes pending writes which triggers sendMessage) - const messagesBefore = sentMessages.length; - storage.set('testKey', 'testValue'); - await storage.close(); - ok(sentMessages.length > messagesBefore); - strictEqual(sentMessages[sentMessages.length - 1].type, 'sharedStorage:changed'); - - // Verify that messages received before init are ignored - const onDidReceiveMessage2 = disposables.add(new Emitter()); - const crossAppIPCService2: ICrossAppIPCService = { - ...crossAppIPCService, - onDidReceiveMessage: onDidReceiveMessage2.event, - }; - - const storageMainService2 = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(new UriIdentityService(fileService)), environmentService, fileService, new NullLogService(), productService)), new TestLifecycleMainService(), fileService, disposables.add(new UriIdentityService(fileService)), crossAppIPCService2)); - - const storage2 = storageMainService2.applicationSharedStorage; - disposables.add(storage2); - - let preInitChangeReceived = false; - disposables.add(storage2.onDidChangeStorage(() => { preInitChangeReceived = true; })); - - // Fire message before init - onDidReceiveMessage2.fire({ - type: 'sharedStorage:changed', - data: { changed: [['preInitKey', 'preInitValue']] } - }); - - strictEqual(preInitChangeReceived, false); - - // Now init and verify subsequent messages work - await storage2.init(); - - onDidReceiveMessage2.fire({ - type: 'sharedStorage:changed', - data: { changed: [['postInitKey', 'postInitValue']] } - }); - - strictEqual(storage2.get('postInitKey'), 'postInitValue'); - - await storage2.close(); - }); - test('application shared storage closed onWillShutdown', async function () { const lifecycleMainService = new TestLifecycleMainService(); const storageMainService = createStorageService(lifecycleMainService); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 1652c62e690c98..8cf5dceb0635d8 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -86,7 +86,6 @@ export abstract class AbstractUpdateService implements IUpdateService { private _hasCheckedForOverwriteOnQuit: boolean = false; private readonly overwriteUpdatesCheckInterval = new IntervalTimer(); private _internalOrg: string | undefined = undefined; - protected _suspended = false; private readonly _onStateChange = new Emitter(); readonly onStateChange: Event = this._onStateChange.event; @@ -289,11 +288,6 @@ export abstract class AbstractUpdateService implements IUpdateService { async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); - if (this._suspended) { - this.logService.trace('update#checkForUpdates - suspended, skipping'); - return; - } - if (this.state.type !== StateType.Idle) { return; } @@ -301,19 +295,6 @@ export abstract class AbstractUpdateService implements IUpdateService { this.doCheckForUpdates(explicit); } - /** - * Prevents all update checks (automatic and manual) from running. - * Used by the cross-app update coordinator when another app owns - * the update client. - */ - suspend(): void { - this._suspended = true; - } - - resume(): void { - this._suspended = false; - } - async downloadUpdate(explicit: boolean): Promise { this.logService.trace('update#downloadUpdate, state = ', this.state.type); diff --git a/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts b/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts deleted file mode 100644 index 37582c9610ba50..00000000000000 --- a/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts +++ /dev/null @@ -1,310 +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 { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; -import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; -import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; -import { ILogService } from '../../log/common/log.js'; -import { IUpdateService, State } from '../common/update.js'; -import { AbstractUpdateService } from './abstractUpdateService.js'; - -/** - * Message types exchanged between apps over crossAppIPC. - */ -const enum CrossAppUpdateMessageType { - /** Server → Client: Update state changed */ - StateChange = 'update/stateChange', - /** Client → Server: Request to check for updates */ - CheckForUpdates = 'update/checkForUpdates', - /** Client → Server: Request to download an available update */ - DownloadUpdate = 'update/downloadUpdate', - /** Client → Server: Request to apply a downloaded update */ - ApplyUpdate = 'update/applyUpdate', - /** Client → Server: Request to quit and install */ - QuitAndInstall = 'update/quitAndInstall', - /** Server → Client: Initial state sync after connection */ - InitialState = 'update/initialState', - /** Client → Server: Request initial state */ - RequestInitialState = 'update/requestInitialState', - /** Server → Client: Ask client to quit for an upcoming update */ - PrepareForQuit = 'update/prepareForQuit', - /** Client → Server: Client confirms it will quit */ - QuitConfirmed = 'update/quitConfirmed', - /** Client → Server: Client's quit was vetoed by the user */ - QuitVetoed = 'update/quitVetoed', -} - -interface CrossAppUpdateMessage { - type: CrossAppUpdateMessageType; - data?: State | boolean; -} - -/** - * Coordinates update ownership between host and embedded Electron apps - * using crossAppIPC. Whichever app starts first becomes the IPC server - * and owns the update client. The second app becomes the client and - * proxies update operations to the server. - * - * When only one app is running, it uses its local update service directly. - * When both apps are running, the IPC server owns the update client and - * the IPC client's local service is suspended to prevent duplicate - * checks and downloads. - * - * This class implements {@link IUpdateService} so it can be used directly - * as the update channel source for renderer processes while transparently - * handling the coordination. - */ -export class CrossAppUpdateCoordinator extends Disposable implements IUpdateService { - - declare readonly _serviceBrand: undefined; - - private mode: 'standalone' | 'server' | 'client' = 'standalone'; - - private _state: State; - - private readonly _onStateChange = this._register(new Emitter()); - readonly onStateChange: Event = this._onStateChange.event; - - /** Disposed when entering client mode, re-registered on disconnect. */ - private localStateListener: IDisposable | undefined; - - /** True when the server has sent PrepareForQuit and is waiting for a response. */ - private pendingQuitAndInstall = false; - - get state(): State { return this._state; } - - constructor( - private readonly localUpdateService: AbstractUpdateService, - private readonly logService: ILogService, - private readonly lifecycleMainService: ILifecycleMainService, - private readonly crossAppIPCService: ICrossAppIPCService, - ) { - super(); - - // Start with the local service's current state - this._state = this.localUpdateService.state; - - // Track local service state changes (used in standalone/server mode) - this.registerLocalStateListener(); - - // Subscribe to cross-app IPC events - this._register(this.crossAppIPCService.onDidConnect(isServer => { - this.handleConnect(isServer); - })); - - // If the service is already connected (e.g. another consumer initialized - // it earlier), run the connect logic immediately. - if (this.crossAppIPCService.connected) { - this.handleConnect(this.crossAppIPCService.isServer); - } - - this._register(this.crossAppIPCService.onDidReceiveMessage(msg => { - this.handleMessage(msg as CrossAppUpdateMessage); - })); - - this._register(this.crossAppIPCService.onDidDisconnect(reason => { - this.logService.info(`CrossAppUpdateCoordinator: disconnected (${reason}), was ${this.mode}`); - - if (this.mode === 'client') { - this.localUpdateService.resume(); - this.registerLocalStateListener(); - this.updateState(this.localUpdateService.state); - } - - if (this.mode === 'server' && this.pendingQuitAndInstall) { - this.logService.info('CrossAppUpdateCoordinator: client disconnected during pending quit, treating as confirmed'); - this.pendingQuitAndInstall = false; - this.mode = 'standalone'; - this.localUpdateService.quitAndInstall(); - return; - } - - this.mode = 'standalone'; - })); - } - - private handleConnect(isServer: boolean): void { - this.logService.info(`CrossAppUpdateCoordinator: connected (isServer=${isServer})`); - - if (isServer) { - this.mode = 'server'; - this.broadcastState(this.localUpdateService.state); - } else { - this.mode = 'client'; - this.localUpdateService.suspend(); - this.localStateListener?.dispose(); - this.localStateListener = undefined; - this.sendMessage({ type: CrossAppUpdateMessageType.RequestInitialState }); - } - } - - private registerLocalStateListener(): void { - this.localStateListener = this.localUpdateService.onStateChange(state => { - this.updateState(state); - this.broadcastState(state); - }); - } - - private handleMessage(msg: CrossAppUpdateMessage): void { - this.logService.trace(`CrossAppUpdateCoordinator: received ${msg.type} (mode=${this.mode})`); - - switch (msg.type) { - // --- Messages handled by the client --- - case CrossAppUpdateMessageType.StateChange: - case CrossAppUpdateMessageType.InitialState: - if (this.mode === 'client') { - this.updateState(msg.data as State); - } - break; - - case CrossAppUpdateMessageType.PrepareForQuit: - if (this.mode === 'client') { - this.logService.info('CrossAppUpdateCoordinator: server requested quit for update'); - this.lifecycleMainService.quit().then(veto => { - if (veto) { - this.logService.info('CrossAppUpdateCoordinator: client quit was vetoed'); - this.sendMessage({ type: CrossAppUpdateMessageType.QuitVetoed }); - } else { - this.sendMessage({ type: CrossAppUpdateMessageType.QuitConfirmed }); - } - }); - } - break; - - // --- Messages handled by the server --- - case CrossAppUpdateMessageType.RequestInitialState: - if (this.mode === 'server') { - this.sendMessage({ type: CrossAppUpdateMessageType.InitialState, data: this.localUpdateService.state }); - } - break; - - case CrossAppUpdateMessageType.CheckForUpdates: - if (this.mode === 'server') { - this.localUpdateService.checkForUpdates(typeof msg.data === 'boolean' ? msg.data : true); - } - break; - - case CrossAppUpdateMessageType.DownloadUpdate: - if (this.mode === 'server') { - this.localUpdateService.downloadUpdate(typeof msg.data === 'boolean' ? msg.data : true); - } - break; - - case CrossAppUpdateMessageType.ApplyUpdate: - if (this.mode === 'server') { - this.localUpdateService.applyUpdate(); - } - break; - - case CrossAppUpdateMessageType.QuitAndInstall: - if (this.mode === 'server') { - this.doCoordinatedQuitAndInstall(); - } - break; - - case CrossAppUpdateMessageType.QuitConfirmed: - if (this.mode === 'server') { - this.logService.info('CrossAppUpdateCoordinator: client confirmed quit, proceeding with quitAndInstall'); - this.pendingQuitAndInstall = false; - this.localUpdateService.quitAndInstall(); - } - break; - - case CrossAppUpdateMessageType.QuitVetoed: - if (this.mode === 'server') { - this.logService.info('CrossAppUpdateCoordinator: client vetoed quit, aborting quitAndInstall'); - this.pendingQuitAndInstall = false; - } - break; - } - } - - private updateState(state: State): void { - this._state = state; - this._onStateChange.fire(state); - } - - private broadcastState(state: State): void { - if (this.mode === 'server') { - this.sendMessage({ type: CrossAppUpdateMessageType.StateChange, data: state }); - } - } - - private sendMessage(msg: CrossAppUpdateMessage): void { - this.crossAppIPCService.sendMessage(msg); - } - - // --- IUpdateService implementation --- - - async checkForUpdates(explicit: boolean): Promise { - if (this.mode === 'client') { - this.sendMessage({ type: CrossAppUpdateMessageType.CheckForUpdates, data: explicit }); - } else { - await this.localUpdateService.checkForUpdates(explicit); - } - } - - async downloadUpdate(explicit: boolean): Promise { - if (this.mode === 'client') { - this.sendMessage({ type: CrossAppUpdateMessageType.DownloadUpdate, data: explicit }); - } else { - await this.localUpdateService.downloadUpdate(explicit); - } - } - - async applyUpdate(): Promise { - if (this.mode === 'client') { - this.sendMessage({ type: CrossAppUpdateMessageType.ApplyUpdate }); - } else { - await this.localUpdateService.applyUpdate(); - } - } - - /** - * Coordinates quit-and-install when a peer is connected. - * Asks the client to quit first; only proceeds with the server's - * quitAndInstall if the client confirms. If the client's quit is - * vetoed (e.g. unsaved editors), the whole operation is aborted. - * - * If no peer is connected (standalone), proceeds directly. - */ - private doCoordinatedQuitAndInstall(): void { - if (this.crossAppIPCService.connected) { - // Ask the client to quit; it will respond with QuitConfirmed/QuitVetoed, - // or disconnect (treated as implicit confirmation). - this.pendingQuitAndInstall = true; - this.sendMessage({ type: CrossAppUpdateMessageType.PrepareForQuit }); - } else { - this.localUpdateService.quitAndInstall(); - } - } - - async quitAndInstall(): Promise { - if (this.mode === 'client') { - // Ask the server to start the coordinated quit flow - this.sendMessage({ type: CrossAppUpdateMessageType.QuitAndInstall }); - } else { - this.doCoordinatedQuitAndInstall(); - } - } - - async isLatestVersion(): Promise { - return this.localUpdateService.isLatestVersion(); - } - - async _applySpecificUpdate(packagePath: string): Promise { - return this.localUpdateService._applySpecificUpdate(packagePath); - } - - async setInternalOrg(internalOrg: string | undefined): Promise { - return this.localUpdateService.setInternalOrg(internalOrg); - } - - override dispose(): void { - this.localStateListener?.dispose(); - super.dispose(); - } -} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 0c5efaf80b8178..e92c54e2fe8c2b 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -165,11 +165,6 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau private onUpdateAvailable(): void { this.logService.trace('update#onUpdateAvailable - Electron autoUpdater reported update available'); - if (this._suspended) { - this.logService.trace('update#onUpdateAvailable - suspended, ignoring'); - return; - } - if (this.state.type !== StateType.CheckingForUpdates && this.state.type !== StateType.Overwriting) { return; } @@ -178,7 +173,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } private onUpdateDownloaded(update: IUpdate): void { - if (this._suspended || this.state.type !== StateType.Downloading) { + if (this.state.type !== StateType.Downloading) { return; } @@ -191,11 +186,6 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau private onUpdateNotAvailable(): void { this.logService.trace('update#onUpdateNotAvailable - Electron autoUpdater reported no update available'); - if (this._suspended) { - this.logService.trace('update#onUpdateNotAvailable - suspended, ignoring'); - return; - } - if (this.state.type !== StateType.CheckingForUpdates) { return; } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 6b97e209fb7be3..a7851933a4b681 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -15,7 +15,6 @@ import { memoize } from '../../../base/common/decorators.js'; import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; import { basename } from '../../../base/common/path.js'; -import { INodeProcess } from '../../../base/common/platform.js'; import { transform } from '../../../base/common/stream.js'; import { URI } from '../../../base/common/uri.js'; import { checksum } from '../../../base/node/crypto.js'; @@ -163,12 +162,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const versionedResourcesFolder = this.productService.commit.substring(0, 10); const innoUpdater = path.join(exeDir, versionedResourcesFolder, 'tools', 'inno_updater.exe'); const exeName = basename(exePath); - const siblingExeName = this.productService.win32SiblingExeBasename ? `${this.productService.win32SiblingExeBasename}.exe` : ''; // Unblock inno_updater --gc when our context-menu COM surrogate keeps a // handle on the orphan commit folder. See https://github.com/microsoft/vscode/issues/294546. await this.killContextMenuComSurrogate(); await new Promise(resolve => { - const child = spawn(innoUpdater, ['--gc', exePath, versionedResourcesFolder, exeName, siblingExeName], { + const child = spawn(innoUpdater, ['--gc', exePath, versionedResourcesFolder, exeName], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true, timeout: 2 * 60 * 1000 @@ -421,13 +419,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.setState(State.Idle(getUpdateType())); }); - // The InnoSetup installer creates the -ready mutex using the host app's - // mutex name ({#AppMutex}). When running as the embedded app, use - // win32SetupMutexName (the host's mutex) to find the correct signal. - const setupMutexName = (process as INodeProcess).isEmbeddedApp - ? this.productService.win32SetupMutexName - : this.productService.win32MutexName; - const readyMutexName = `${setupMutexName}-ready`; + const readyMutexName = `${this.productService.win32MutexName}-ready`; const mutex = await import('@vscode/windows-mutex'); this.updateCancellationTokenSource?.dispose(true); diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index fe9ce0b757d5fb..77ac5fa75c7042 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -7,7 +7,7 @@ import { app, Event as ElectronEvent } from 'electron'; import { disposableTimeout } from '../../../base/common/async.js'; import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { INodeProcess, isWindows } from '../../../base/common/platform.js'; +import { isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { ILogService } from '../../log/common/log.js'; @@ -51,8 +51,7 @@ export class ElectronURLListener extends Disposable { // Windows: install as protocol handler // Skip in portable mode: the registered command wouldn't preserve // portable mode settings, causing issues with OAuth flows. - // Skip for embedded apps: protocol handler is registered at install time. - if (isWindows && !environmentMainService.isPortable && !(process as INodeProcess).isEmbeddedApp) { + if (isWindows && !environmentMainService.isPortable) { const windowsParameters = environmentMainService.isBuilt ? [] : [`"${environmentMainService.appRoot}"`]; windowsParameters.push('--open-url', '--'); app.setAsDefaultProtocolClient(productService.urlProtocol, process.execPath, windowsParameters); diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index db523ea45d1c5c..433fc3c46e0716 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../base/common/event.js'; -import { INodeProcess } from '../../../base/common/platform.js'; import { joinPath } from '../../../base/common/resources.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { IFileService } from '../../files/common/files.js'; @@ -48,25 +47,10 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } protected override createDefaultProfile(): IUserDataProfile { - const defaultProfile = { + return { ...super.createDefaultProfile(), agentPluginsHome: this.agentPluginsHome }; - if (!(process as INodeProcess).isEmbeddedApp) { - return defaultProfile; - } - const hostUserRoamingDataHome = this.environmentService.parentAppUserRoamingDataHome; - if (!hostUserRoamingDataHome) { - return defaultProfile; - } - const hostAgentPluginsHome = getParentAppAgentPluginsPath(this.nativeEnvironmentService); - return { - ...defaultProfile, - keybindingsResource: joinPath(hostUserRoamingDataHome, 'keybindings.json'), - promptsHome: joinPath(hostUserRoamingDataHome, 'prompts'), - mcpResource: joinPath(hostUserRoamingDataHome, 'mcp.json'), - agentPluginsHome: hostAgentPluginsHome ? URI.file(hostAgentPluginsHome) : this.agentPluginsHome - }; } async createAgentsWindowProfile(): Promise { @@ -87,14 +71,6 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } } -function getParentAppAgentPluginsPath(environmentService: INativeEnvironmentService): string | undefined { - const hostUserHome = environmentService.parentAppUserHome; - if (!hostUserHome) { - return undefined; - } - return getAgentPluginsPath(environmentService.args, hostUserHome); -} - function getAgentPluginsPath(args: NativeParsedArgs, userHome: URI): string { const cliAgentPluginsDir = args['agent-plugins-dir']; if (cliAgentPluginsDir) { diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 319ff8fb6c4580..37cd505a38bf83 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -26,8 +26,6 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { userDataDir, homeDir: userDataDir, tmpDir: userDataDir, - parentAppUserDataDir: undefined, - parentAppUserHomeDir: undefined }; super(Object.create(null), paths, { _serviceBrand: undefined, ...product }); } diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 53399df186a0bf..c8ec2ae6af101e 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -27,8 +27,6 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { userDataDir, homeDir: userDataDir, tmpDir: userDataDir, - parentAppUserDataDir: undefined, - parentAppUserHomeDir: undefined }; super(Object.create(null), paths, { _serviceBrand: undefined, ...product }); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 36511b63be60e3..291648bca96e32 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -442,9 +442,6 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native homeDir: string; tmpDir: string; userDataDir: string; - isEmbeddedApp?: boolean; - parentAppUserDataDir?: string; - parentAppUserHomeDir?: string; partsSplash?: IPartsSplash; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 07551319546ce7..7aeeb120987f9d 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, INodeProcess } from '../../../base/common/platform.js'; +import { isTahoeOrNewer, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { release } from 'os'; @@ -52,6 +52,7 @@ export interface IWindowCreationOptions { readonly state: IWindowState; readonly extensionDevelopmentPath?: string[]; readonly isExtensionTestHost?: boolean; + readonly isSessionsWindow?: boolean; } interface ITouchBarSegment extends electron.SegmentedControlSegment { @@ -707,8 +708,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none' }; - if ((process as INodeProcess).isEmbeddedApp) { - webPreferences.backgroundThrottling = false; // disable for sub-app + if (config.isSessionsWindow) { + webPreferences.backgroundThrottling = false; // keep agents window responsive when in background } const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, undefined, webPreferences); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 3f6af0feb38f73..3899541ec3b7c9 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -7,7 +7,7 @@ import electron, { Display, Rectangle } from 'electron'; import { Color } from '../../../base/common/color.js'; import { Event } from '../../../base/common/event.js'; import { join } from '../../../base/common/path.js'; -import { INodeProcess, IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -180,11 +180,6 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt } else if (isWindows) { if (!environmentMainService.isBuilt) { options.icon = join(environmentMainService.appRoot, 'resources/win32/code_150x150.png'); // only when running out of sources on Windows - } else if ((process as INodeProcess).isEmbeddedApp) { - // For sub app the proxy executable acts as a launcher to the main executable whose - // icon will be used when creating windows if the following override is not set. - // This avoids sharing icon with the main application. - options.icon = join(environmentMainService.appRoot, 'resources/win32/sessions.ico'); } } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index e9072892638d7e..8f56afc29ec233 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -17,7 +17,7 @@ import { Disposable, DisposableStore, IDisposable } from '../../../base/common/l import { Schemas } from '../../../base/common/network.js'; import { basename, join, normalize, posix } from '../../../base/common/path.js'; import { getMarks, mark } from '../../../base/common/performance.js'; -import { INodeProcess, IProcessEnvironment, isMacintosh, isWindows, OS } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isMacintosh, isWindows, OS } from '../../../base/common/platform.js'; import { cwd } from '../../../base/common/process.js'; import { extUriBiasedIgnorePathCase, isEqual, isEqualAuthority, normalizePath, originalFSPath, removeTrailingPathSeparator } from '../../../base/common/resources.js'; import { assertReturnsDefined } from '../../../base/common/types.js'; @@ -334,12 +334,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic async open(openConfig: IOpenConfiguration): Promise { this.logService.trace('windowsManager#open'); - // Take care of agents app specially - const isAgentsApp = (process as INodeProcess).isEmbeddedApp; - if (isAgentsApp) { - openConfig = await this.ensureAgentsWindow(openConfig); - } - // Make sure addMode/removeMode is only enabled if we have an active window if ((openConfig.addMode || openConfig.removeMode) && (openConfig.initialStartup || !this.getLastActiveWindow())) { openConfig.addMode = false; @@ -408,7 +402,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) - if (openConfig.initialStartup && !isAgentsApp /* skipped for agents app */) { + if (openConfig.initialStartup) { // Untitled workspaces are always restored untitledWorkspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspaces()); @@ -498,9 +492,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Handle ` chat` this.handleChatRequest(openConfig, usedWindows); - // Handle ` --open-chat-session` - this.handleOpenChatSession(openConfig, usedWindows); - return usedWindows; } @@ -544,17 +535,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - private handleOpenChatSession(openConfig: IOpenConfiguration, usedWindows: ICodeWindow[]): void { - const sessionUri = openConfig.cli['open-chat-session']; - if (!sessionUri || usedWindows.length === 0) { - return; - } - - const window = usedWindows[0]; - window.sendWhenReady('vscode:openChatSession', CancellationToken.None, sessionUri); - window.focus(); - } - private async doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], @@ -1570,9 +1550,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic homeDir: this.environmentMainService.userHome.with({ scheme: Schemas.file }).fsPath, tmpDir: this.environmentMainService.tmpDir.with({ scheme: Schemas.file }).fsPath, userDataDir: this.environmentMainService.userDataPath, - isEmbeddedApp: this.environmentMainService.isEmbeddedApp, - parentAppUserDataDir: this.environmentMainService.parentAppUserRoamingDataHome?.with({ scheme: Schemas.file }).fsPath, - parentAppUserHomeDir: this.environmentMainService.parentAppUserHome?.with({ scheme: Schemas.file }).fsPath, remoteAuthority: options.remoteAuthority, workspace: options.workspace, @@ -1618,7 +1595,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const createdWindow = window = this.instantiationService.createInstance(CodeWindow, { state, extensionDevelopmentPath: configuration.extensionDevelopmentPath, - isExtensionTestHost: !!configuration.extensionTestsPath + isExtensionTestHost: !!configuration.extensionTestsPath, + isSessionsWindow: configuration.isSessionsWindow }); mark('code/didCreateCodeWindow'); diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 68f71f1cf2f4d5..560f5ca44af8bc 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -10,7 +10,7 @@ import { Emitter, Event as CommonEvent } from '../../../base/common/event.js'; import { normalizeDriveLetter, splitRecentLabel } from '../../../base/common/labels.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { Schemas } from '../../../base/common/network.js'; -import { isMacintosh, INodeProcess, isWindows } from '../../../base/common/platform.js'; +import { isMacintosh, isWindows } from '../../../base/common/platform.js'; import { basename, extUriBiasedIgnorePathCase, originalFSPath } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; import { Promises } from '../../../base/node/pfs.js'; @@ -108,7 +108,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add to recent documents (Windows only, macOS later) // Skip in portable mode to avoid leaving traces on the machine // Skip in the sessions app to avoid polluting the jump list - if (isWindows && recent.fileUri.scheme === Schemas.file && !this.environmentMainService.isPortable && !(process as INodeProcess).isEmbeddedApp) { + if (isWindows && recent.fileUri.scheme === Schemas.file && !this.environmentMainService.isPortable) { app.addRecentDocument(recent.fileUri.fsPath); } } @@ -326,11 +326,6 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa return; } - // Skip in the sessions app to avoid polluting the jump list - if ((process as INodeProcess).isEmbeddedApp) { - return; - } - await this.updateWindowsJumpList(); this._register(this.onDidChangeRecentlyOpened(() => this.updateWindowsJumpList())); } @@ -461,11 +456,6 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa return; } - // Skip in the sessions app to avoid polluting the dock - if ((process as INodeProcess).isEmbeddedApp) { - return; - } - // We clear all documents first to ensure an up-to-date view on the set. Since entries // can get deleted on disk, this ensures that the list is always valid app.clearRecentDocuments(); diff --git a/src/vs/sessions/services/vscode/browser/themeImporterService.ts b/src/vs/sessions/services/vscode/browser/themeImporterService.ts deleted file mode 100644 index 63bf1f07b6e149..00000000000000 --- a/src/vs/sessions/services/vscode/browser/themeImporterService.ts +++ /dev/null @@ -1,27 +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 { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IThemeImporterService, IThemePreviewResult } from '../common/themeImporter.js'; - -/** - * Browser/web no-op implementation of {@link IThemeImporterService}. The web - * variant of the Agents app does not have access to a parent VS Code - * installation, so theme importing is unavailable. - */ -class BrowserThemeImporterService implements IThemeImporterService { - - declare readonly _serviceBrand: undefined; - - async getVSCodeTheme(): Promise { - return undefined; - } - - async previewVSCodeTheme(): Promise { - return undefined; - } -} - -registerSingleton(IThemeImporterService, BrowserThemeImporterService, InstantiationType.Delayed); diff --git a/src/vs/sessions/services/vscode/common/themeImporter.ts b/src/vs/sessions/services/vscode/common/themeImporter.ts deleted file mode 100644 index 9b1df8a9fe22f3..00000000000000 --- a/src/vs/sessions/services/vscode/common/themeImporter.ts +++ /dev/null @@ -1,48 +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 { IDisposable } from '../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - -/** The VS Code configuration key for the active color theme. */ -export const COLOR_THEME_SETTINGS_ID = 'workbench.colorTheme'; - -export const IThemeImporterService = createDecorator('IThemeImporterService'); - -/** - * Result of previewing the parent VS Code's color theme. - */ -export interface IThemePreviewResult extends IDisposable { - /** - * Permanently imports the previewed theme into the Agents app by - * copying the providing extension and installing it from there. - */ - apply(): Promise; -} - -/** - * Service that reads the parent VS Code installation's active color theme - * and can import it into the Agents app — using the providing extension - * from the parent VS Code installation if necessary. - */ -export interface IThemeImporterService { - - readonly _serviceBrand: undefined; - - /** - * Resolves the parent VS Code's active color theme. Returns `undefined` - * when the parent settings cannot be read or the theme is already one of - * the onboarding themes displayed in the theme picker. - */ - getVSCodeTheme(): Promise; - - /** - * Temporarily installs the providing extension from the host's extensions - * directory and applies the VS Code theme. Returns a cached - * {@link IThemePreviewResult} to apply or dispose the preview. Returns - * `undefined` if the theme is already available or cannot be resolved. - */ - previewVSCodeTheme(): Promise; -} diff --git a/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts b/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts deleted file mode 100644 index 92342b992cbcc2..00000000000000 --- a/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts +++ /dev/null @@ -1,257 +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 { parse as parseJSONC } from '../../../../base/common/jsonc.js'; -import { getErrorMessage } from '../../../../base/common/errors.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { joinPath } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; -import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; -import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; -import { IThemeImporterService, IThemePreviewResult, COLOR_THEME_SETTINGS_ID } from '../common/themeImporter.js'; -import { INativeWorkbenchEnvironmentService } from '../../../../workbench/services/environment/electron-browser/environmentService.js'; - -/** - * Describes a color theme from the parent VS Code installation. - */ -interface IParentThemeInfo { - /** The settingsId of the theme (e.g. "Dark Modern", "Monokai"). */ - readonly settingsId: string; - /** - * The location of the extension that provides this theme. - * `undefined` when the theme is already available (built-in or installed). - */ - readonly extensionLocation: URI | undefined; -} - -class ThemeImporterService extends Disposable implements IThemeImporterService { - - declare readonly _serviceBrand: undefined; - - private _parentThemePromise: Promise | undefined; - private _previewPromise: Promise | undefined; - - constructor( - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, - ) { - super(); - } - - async getVSCodeTheme(): Promise { - if (!this._parentThemePromise) { - this._parentThemePromise = this._resolveVSCodeTheme(); - } - const themeInfo = await this._parentThemePromise; - return themeInfo?.settingsId; - } - - async previewVSCodeTheme(): Promise { - if (!this._previewPromise) { - this._previewPromise = this._doPreview(); - // Clear cache if preview resolved to undefined so callers can retry - this._previewPromise.then(result => { - if (!result) { - this._previewPromise = undefined; - } - }); - } - return this._previewPromise; - } - - private async _doPreview(): Promise { - try { - const theme = await this._getVSCodeTheme(); - if (!theme) { - return undefined; - } - - const installed = await this._installFromHostLocation(theme); - await this._setTheme(theme.settingsId); - let applied = false; - - return { - apply: async () => { - applied = true; - this._previewPromise = undefined; - await this._apply(theme); - }, - dispose: () => { - this._previewPromise = undefined; - if (applied) { - return; - } - void this._disposePreview(installed); - }, - }; - } catch (err) { - this.logService.error('[VSCodeThemeImporter] Failed to preview theme:', err); - return undefined; - } - } - - private async _apply(theme: IParentThemeInfo): Promise { - try { - if (!theme.extensionLocation) { - return; - } - - // Copy extension to Agents app's own extensions directory - const extensionsHome = URI.file(this.environmentService.extensionsPath); - const folderName = theme.extensionLocation.path.split('/').pop()!; - const targetLocation = joinPath(extensionsHome, folderName); - - this.logService.info(`[VSCodeThemeImporter] Copying extension to ${targetLocation.toString()}`); - await this.fileService.copy(theme.extensionLocation, targetLocation, true); - - // Replace install from the copied location - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - await this.extensionManagementService.installFromLocation(targetLocation, profileLocation); - } catch (err) { - this.logService.error('[VSCodeThemeImporter] Failed to apply theme:', err); - } - } - - private async _disposePreview(installed: ILocalExtension | undefined): Promise { - if (!installed) { - return; - } - try { - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - await this.extensionManagementService.uninstall(installed, { profileLocation }); - } catch (err) { - this.logService.warn('[VSCodeThemeImporter] Failed to uninstall preview extension:', err); - } - } - - private async _getVSCodeTheme(): Promise { - if (!this._parentThemePromise) { - this._parentThemePromise = this._resolveVSCodeTheme(); - } - return this._parentThemePromise; - } - - /** - * Installs the extension from the host's extensions directory if needed. - * Returns the installed extension, or `undefined` if no install was needed. - */ - private async _installFromHostLocation(theme: IParentThemeInfo): Promise { - if (!theme.extensionLocation) { - return undefined; - } - this.logService.info(`[VSCodeThemeImporter] Installing extension from ${theme.extensionLocation.toString()}`); - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - return this.extensionManagementService.installFromLocation(theme.extensionLocation, profileLocation); - } - - private async _setTheme(themeSettingsId: string): Promise { - const allThemes = await this.themeService.getColorThemes(); - const match = allThemes.find(t => t.settingsId === themeSettingsId); - if (match) { - await this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); - } else { - this.logService.warn(`[VSCodeThemeImporter] Theme ${themeSettingsId} not found after install`); - } - } - - private async _resolveVSCodeTheme(): Promise { - try { - const settingsId = await this._readVSCodeThemeId(); - if (!settingsId) { - return undefined; - } - - // Find the extension providing this theme by scanning the host's extensions - const extensionLocation = await this._findThemeExtension(settingsId); - - return { settingsId, extensionLocation }; - } catch (err) { - this.logService.warn('[VSCodeThemeImporter] Failed to resolve VS Code theme:', err); - return undefined; - } - } - - /** - * Scans the host VS Code's extensions directory to find which extension - * provides the given theme. Returns the extension location URI, or - * `undefined` if the theme is already available (built-in or installed). - */ - private async _findThemeExtension(themeSettingsId: string): Promise { - const allThemes = await this.themeService.getColorThemes(); - if (allThemes.find(t => t.settingsId === themeSettingsId)) { - return undefined; - } - - const hostExtensionsHome = this.environmentService.parentAppExtensionsHome; - if (!hostExtensionsHome) { - return undefined; - } - - try { - const scanned = await this.extensionsScannerService.scanOneOrMultipleExtensions( - hostExtensionsHome, - ExtensionType.User, - {}, - ); - for (const ext of scanned) { - if (this._extensionProvidesTheme(ext.manifest, themeSettingsId)) { - return ext.location; - } - } - } catch (err) { - this.logService.warn('[VSCodeThemeImporter] Failed to scan host extensions:', err); - } - - return undefined; - } - - private _extensionProvidesTheme(manifest: IExtensionManifest, themeSettingsId: string): boolean { - const themes = manifest.contributes?.themes; - if (!Array.isArray(themes)) { - return false; - } - return themes.some(t => { - const theme = t as { id?: string; label?: string }; - return theme.id === themeSettingsId || theme.label === themeSettingsId; - }); - } - - private async _readVSCodeThemeId(): Promise { - const hostDataHome = this.environmentService.parentAppUserRoamingDataHome; - if (!hostDataHome) { - this.logService.warn('[VSCodeThemeImporter] Host user data home is not available'); - return undefined; - } - - try { - const settingsUri = joinPath(hostDataHome, 'settings.json'); - const content = await this.fileService.readFile(settingsUri); - const settings = parseJSONC>(content.value.toString()); - const themeId = settings[COLOR_THEME_SETTINGS_ID]; - if (typeof themeId === 'string') { - return themeId; - } - this.logService.warn('[VSCodeThemeImporter] workbench.colorTheme is not set in host settings.json', themeId, settingsUri.toString()); - return undefined; - } catch (e) { - this.logService.warn('[VSCodeThemeImporter] Failed to read host settings.json, falling back to default theme', getErrorMessage(e)); - return undefined; - } - } -} - -registerSingleton(IThemeImporterService, ThemeImporterService, InstantiationType.Delayed); diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 9c88bb5abcaa27..8c28636d6b61a8 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -200,7 +200,6 @@ import '../workbench/contrib/policyExport/electron-browser/policyExport.contribu //#region --- sessions contributions import './electron-browser/sessions.desktop.contribution.js'; -import './services/vscode/electron-browser/themeImporterService.js'; // Remote Agent Host import '../platform/agentHost/electron-browser/agentHostService.js'; diff --git a/src/vs/sessions/sessions.web.main.ts b/src/vs/sessions/sessions.web.main.ts index cf54fb54dc4a8d..7d1421489769b2 100644 --- a/src/vs/sessions/sessions.web.main.ts +++ b/src/vs/sessions/sessions.web.main.ts @@ -71,7 +71,6 @@ import '../platform/extensionResourceLoader/browser/extensionResourceLoaderServi import '../workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import '../workbench/services/power/browser/powerService.js'; import '../platform/sandbox/browser/sandboxHelperService.js'; -import './services/vscode/browser/themeImporterService.js'; import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js'; diff --git a/src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts b/src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts new file mode 100644 index 00000000000000..99601910656e12 --- /dev/null +++ b/src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js'; +import { IBannerService } from '../../../services/banner/browser/bannerService.js'; +import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; + +/** + * Tracks whether we have already shown the one-time banner explaining that + * the formerly separate Agents application is now a window inside VS Code. + * Stored in `StorageScope.APPLICATION` so the banner is shown at most once + * per installation, regardless of profile. + */ +const AGENTS_APP_MERGED_BANNER_SHOWN_KEY = 'workbench.banner.agentsAppMerged.shown'; + +class AgentsAppMergedBannerContribution { + + constructor( + @IBannerService bannerService: IBannerService, + @IStorageService storageService: IStorageService, + @IProductService productService: IProductService + ) { + if (productService.quality === 'stable') { + return; + } + + if (storageService.getBoolean(AGENTS_APP_MERGED_BANNER_SHOWN_KEY, StorageScope.APPLICATION, false)) { + return; + } + + storageService.store(AGENTS_APP_MERGED_BANNER_SHOWN_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); + + const message = localize('agentsAppMerged.message', "The Agents app is deprecated. Starting with this version, it is integrated into {0}.", productService.nameLong); + const openAction = { + href: 'command:workbench.action.openAgentsWindow', + label: localize('agentsAppMerged.open', "Open Agents Window") + }; + + bannerService.show({ + id: 'workbench.banner.agentsAppMerged', + icon: ThemeIcon.fromId('info'), + message, + ariaLabel: message, + actions: [openAction] + }); + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(AgentsAppMergedBannerContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 1d831a025a592f..64b8586af2959f 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -63,17 +63,10 @@ function getSessionsWindowTrustNote(environmentService: IWorkbenchEnvironmentSer if (!environmentService.isSessionsWindow) { return undefined; } - const parentAppName = productService.quality === 'stable' - ? 'Visual Studio Code' - : productService.quality === 'insider' - ? 'Visual Studio Code Insiders' - : productService.quality === 'exploration' - ? 'Visual Studio Code Exploration' - : productService.nameLong; if (isWorkspace) { - return localize('sessionsWindowWorkspaceTrustNote', "Trusting this workspace will also mark it as trusted in {0}.", parentAppName); + return localize('sessionsWindowWorkspaceTrustNote', "Trusting this workspace will also mark it as trusted in {0}.", productService.nameLong); } - return localize('sessionsWindowFolderTrustNote', "Trusting this folder will also mark it as trusted in {0}.", parentAppName); + return localize('sessionsWindowFolderTrustNote', "Trusting this folder will also mark it as trusted in {0}.", productService.nameLong); } export class WorkspaceTrustContextKeys extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index b267fefcbea14c..1abd21a9d7ef76 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -164,10 +164,7 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment homeDir: configuration.homeDir, tmpDir: configuration.tmpDir, userDataDir: configuration.userDataDir, - parentAppUserDataDir: configuration.parentAppUserDataDir, - parentAppUserHomeDir: configuration.parentAppUserHomeDir }, - productService, - !!configuration.isEmbeddedApp); + productService); } } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index f1c685b03794f9..c56e45f72c8640 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -105,8 +105,6 @@ export class TestNativeHostService implements INativeHostService { async openAgentsWindow(_options?: { folderUri?: UriComponents }): Promise { } - async launchSiblingApp(_args?: string[]): Promise { } - async toggleFullScreen(): Promise { } async isMaximized(): Promise { return true; } async isFullScreen(): Promise { return true; } diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 4277093b103893..4530bb0ec7a9b2 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -187,6 +187,9 @@ import './contrib/encryption/electron-browser/encryption.contribution.js'; // Emergency Alert import './contrib/emergencyAlert/electron-browser/emergencyAlert.contribution.js'; +// Agents App Merged Banner +import './contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.js'; + // MCP import './contrib/mcp/electron-browser/mcp.contribution.js'; From 9ea52c61046589fbafb6565f8ccb0d9e1dc39c49 Mon Sep 17 00:00:00 2001 From: Isidor Date: Thu, 7 May 2026 10:42:00 +0200 Subject: [PATCH 14/28] feat: add Get Changed Files tool configuration and enablement in Copilot Chat --- extensions/copilot/package.json | 11 +++++++++++ extensions/copilot/package.nls.json | 1 + .../copilot/src/extension/intents/node/agentIntent.ts | 3 +++ .../configuration/common/configurationService.ts | 2 ++ 4 files changed, 17 insertions(+) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 17f42237d7a7e1..779f04f3d6938c 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -543,6 +543,7 @@ "icon": "$(diff)", "userDescription": "%copilot.tools.changes.description%", "modelDescription": "Get git diffs of current file changes in a git repository. Don't forget that you can use run_in_terminal to run git commands in a terminal as well.", + "when": "config.github.copilot.chat.getChangedFilesTool.enabled", "tags": [ "vscode_codesearch" ], @@ -4445,6 +4446,16 @@ "experimental" ] }, + "github.copilot.chat.getChangedFilesTool.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%github.copilot.config.getChangedFilesTool.enabled%", + "tags": [ + "advanced", + "experimental", + "onExp" + ] + }, "github.copilot.chat.executionSubagent.enabled": { "type": "boolean", "default": false, diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index bccc10f19cfbc3..d1a98742c666e9 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -491,6 +491,7 @@ "copilot.tools.skill.name": "Skill", "copilot.tools.skill.description": "Execute a skill by name. Skills provide specialized capabilities, domain knowledge, and refined workflows.", "github.copilot.config.skill.enabled": "Enable the skill tool in Copilot Chat. When enabled, skills are invoked via a dedicated skill tool instead of readFile.", + "github.copilot.config.getChangedFilesTool.enabled": "Enable the Get Changed Files tool in Copilot Chat. When enabled, the agent can retrieve git diffs of current changes via a dedicated tool.", "github.copilot.config.searchSubagent.enabled": "Enable the search subagent tool for iterative code exploration in the workspace.", "github.copilot.config.searchSubagent.useAgenticProxy": "Use the agentic proxy for the search subagent tool.", "github.copilot.config.searchSubagent.model": "Model to use for the search subagent. When useAgenticProxy is enabled, defaults to 'vscode-agentic-search-router-a'. Otherwise defaults to the main agent model.", diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 013d28faead7f0..32fdf1f74aba0c 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -154,6 +154,9 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. const skillToolEnabled = configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SkillToolEnabled, experimentationService); allowTools[ToolName.Skill] = skillToolEnabled; + const getSCMChangesEnabled = configurationService.getExperimentBasedConfig(ConfigKey.Advanced.GetChangedFilesToolEnabled, experimentationService); + allowTools[ToolName.GetScmChanges] = getSCMChangesEnabled; + allowTools[ToolName.SessionStoreSql] = true; allowTools[CUSTOM_TOOL_SEARCH_NAME] = !!model.supportsToolSearch; diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 2b4ba498383250..96b58a71eb9173 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -662,6 +662,8 @@ export namespace ConfigKey { export const ExecutionSubagentToolEnabled = defineSetting('chat.executionSubagent.enabled', ConfigType.ExperimentBased, false); export const SkillToolEnabled = defineSetting('chat.skillTool.enabled', ConfigType.ExperimentBased, false); + /** When enabled, the get_changed_files tool is available to the agent. */ + export const GetChangedFilesToolEnabled = defineSetting('chat.getChangedFilesTool.enabled', ConfigType.ExperimentBased, false); /** Model to use for the execution subagent */ /** Use the agentic proxy for the execution subagent */ export const ExecutionSubagentUseAgenticProxy = defineSetting('chat.executionSubagent.useAgenticProxy', ConfigType.ExperimentBased, false); From 0a2f1f308f467c0f95cbd1e6480e56c374dd6a40 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 7 May 2026 01:55:50 -0700 Subject: [PATCH 15/28] BYOK: Add provideId static property to all providers --- .../byok/vscode-node/anthropicProvider.ts | 5 +++-- .../extension/byok/vscode-node/azureProvider.ts | 5 +++-- .../byok/vscode-node/byokContribution.ts | 16 ++++++++-------- .../byok/vscode-node/customOAIProvider.ts | 10 +++++----- .../byok/vscode-node/geminiNativeProvider.ts | 3 ++- .../extension/byok/vscode-node/ollamaProvider.ts | 7 +++++-- .../extension/byok/vscode-node/openAIProvider.ts | 4 +++- .../byok/vscode-node/openRouterProvider.ts | 5 ++++- .../extension/byok/vscode-node/xAIProvider.ts | 3 ++- 9 files changed, 35 insertions(+), 23 deletions(-) diff --git a/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts index 5a24520c0ce339..ca7d3b9e802a12 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts @@ -34,6 +34,7 @@ import { IBYOKStorageService } from './byokStorageService'; export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { public static readonly providerName = 'Anthropic'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( knownModels: BYOKKnownModels | undefined, @@ -46,7 +47,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { @IOTelService private readonly _otelService: IOTelService, @IToolDeferralService private readonly _toolDeferralService: IToolDeferralService, ) { - super(AnthropicLMProvider.providerName.toLowerCase(), AnthropicLMProvider.providerName, knownModels, byokStorageService, logService); + super(AnthropicLMProvider.providerId, AnthropicLMProvider.providerName, knownModels, byokStorageService, logService); } @@ -356,7 +357,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { // Record OTel metrics for this Anthropic LLM call if (result.usage) { const durationSec = (Date.now() - issuedTime) / 1000; - const metricAttrs = { operationName: GenAiOperationName.CHAT, providerName: 'anthropic', requestModel: model.id, responseModel: model.id }; + const metricAttrs = { operationName: GenAiOperationName.CHAT, providerName: AnthropicLMProvider.providerId, requestModel: model.id, responseModel: model.id }; GenAiMetrics.recordOperationDuration(this._otelService, durationSec, metricAttrs); if (result.usage.prompt_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.prompt_tokens, 'input', metricAttrs); } if (result.usage.completion_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.completion_tokens, 'output', metricAttrs); } diff --git a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts index 56f5bb2d617d6c..6e32e34a46050c 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts @@ -47,7 +47,8 @@ export function resolveAzureUrl(modelId: string, url: string): string { export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { - static readonly providerName = 'Azure'; + public static readonly providerName = 'Azure'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( byokStorageService: IBYOKStorageService, @@ -59,7 +60,7 @@ export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext ) { super( - AzureBYOKModelProvider.providerName.toLowerCase(), + AzureBYOKModelProvider.providerId, AzureBYOKModelProvider.providerName, byokStorageService, logService, diff --git a/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts b/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts index 9ccea9caa4aa29..3cd08f201c2756 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts @@ -65,14 +65,14 @@ export class BYOKContrib extends Disposable implements IExtensionContribution { if (this._store.isDisposed) { return; } - this._providers.set(OllamaLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OllamaLMProvider, this._byokStorageService)); - this._providers.set(AnthropicLMProvider.providerName.toLowerCase(), instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService)); - this._providers.set(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService)); - this._providers.set(XAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService)); - this._providers.set(OAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService)); - this._providers.set(OpenRouterLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService)); - this._providers.set(AzureBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService)); - this._providers.set(CustomOAIBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService)); + this._providers.set(OllamaLMProvider.providerId, instantiationService.createInstance(OllamaLMProvider, this._byokStorageService)); + this._providers.set(AnthropicLMProvider.providerId, instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService)); + this._providers.set(GeminiNativeBYOKLMProvider.providerId, instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService)); + this._providers.set(XAIBYOKLMProvider.providerId, instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService)); + this._providers.set(OAIBYOKLMProvider.providerId, instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService)); + this._providers.set(OpenRouterLMProvider.providerId, instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService)); + this._providers.set(AzureBYOKModelProvider.providerId, instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService)); + this._providers.set(CustomOAIBYOKModelProvider.providerId, instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService)); for (const [providerName, provider] of this._providers) { this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerName, provider)); diff --git a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts index 772e2cf4460696..1c6e8c52589f4c 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts @@ -163,8 +163,8 @@ export abstract class AbstractCustomOAIBYOKModelProvider extends AbstractOpenAIC export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { - static readonly providerName: string = 'CustomOAI'; - private providerName: string = CustomOAIBYOKModelProvider.providerName; + public static readonly providerName = 'CustomOAI'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( _byokStorageService: IBYOKStorageService, @@ -175,16 +175,16 @@ export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvid @IExperimentationService expService: IExperimentationService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext ) { - super(CustomOAIBYOKModelProvider.providerName.toLowerCase(), CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext); + super(CustomOAIBYOKModelProvider.providerId, CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext); this.migrateExistingConfigs(); } // TODO: Remove this after 6 months private async migrateExistingConfigs(): Promise { - await this.migrateConfig(ConfigKey.Deprecated.CustomOAIModels, this.providerName, this.providerName); + await this.migrateConfig(ConfigKey.Deprecated.CustomOAIModels, CustomOAIBYOKModelProvider.providerName, CustomOAIBYOKModelProvider.providerName); } protected resolveUrl(modelId: string, url: string): string { return resolveCustomOAIUrl(modelId, url); } -} \ No newline at end of file +} diff --git a/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts index c8fb3efb9e2619..fee2a987026922 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts @@ -26,6 +26,7 @@ import { IBYOKStorageService } from './byokStorageService'; export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvider { public static readonly providerName = 'Gemini'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( knownModels: BYOKKnownModels | undefined, @@ -35,7 +36,7 @@ export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvide @ITelemetryService private readonly _telemetryService: ITelemetryService, @IOTelService private readonly _otelService: IOTelService, ) { - super(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService); + super(GeminiNativeBYOKLMProvider.providerId, GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService); } protected async getAllModels(silent: boolean, apiKey: string | undefined): Promise[]> { diff --git a/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts index 43598527d80cb2..de48e3f775a604 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts @@ -38,7 +38,10 @@ export interface OllamaConfig extends LanguageModelChatConfiguration { } export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider { + public static readonly providerName = 'Ollama'; + public static readonly providerId = this.providerName.toLowerCase(); + private _modelCache = new Map(); constructor( @@ -50,7 +53,7 @@ export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider Date: Thu, 7 May 2026 02:20:31 -0700 Subject: [PATCH 16/28] BYOK: Avoid auth token retrieval for diagnostics purposes only --- .../vscode-node/languageModelAccess.ts | 5 ++- .../inlineChat2/node/inlineChatIntent.ts | 2 +- .../node/defaultIntentRequestHandler.ts | 34 ++++++++----------- .../prompts/node/codeMapper/codeMapper.ts | 2 +- .../src/platform/chat/common/commonTypes.ts | 4 +-- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index cecd1b8eb400d8..55e99b723369e7 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -439,6 +439,9 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } private async _getToken(): Promise { + if (!this._authenticationService.anyGitHubSession) { + return undefined; + } try { const copilotToken = await this._authenticationService.getCopilotToken(); return copilotToken; @@ -654,7 +657,7 @@ export class CopilotLanguageModelWrapper extends Disposable { throw vscode.LanguageModelError.Blocked(blockedExtensionMessage); } else if (result.type === ChatFetchResponseType.QuotaExceeded) { const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const details = getErrorDetailsFromChatFetchError(result, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const details = getErrorDetailsFromChatFetchError(result, this._authenticationService.copilotToken?.copilotPlan, outageStatus); const err = new vscode.LanguageModelError(details.message); err.name = 'ChatQuotaExceeded'; throw err; diff --git a/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts index 56c1ec62522de8..caa0530a80746e 100644 --- a/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts +++ b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts @@ -171,7 +171,7 @@ export class InlineChatIntent implements IIntent { if (result.lastResponse.type !== ChatFetchResponseType.Success) { const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const details = getErrorDetailsFromChatFetchError(result.lastResponse, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const details = getErrorDetailsFromChatFetchError(result.lastResponse, this._authenticationService.copilotToken?.copilotPlan, outageStatus); return { errorDetails: { message: details.message, diff --git a/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts b/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts index 4c5660ec76ecaf..6915d619b9bc4a 100644 --- a/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts +++ b/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts @@ -9,7 +9,7 @@ import type { ChatRequest, ChatResponseReferencePart, ChatResponseStream, ChatRe import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade'; import { IChatHookService, UserPromptSubmitHookInput, UserPromptSubmitHookOutput } from '../../../platform/chat/common/chatHookService'; -import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes'; +import { CanceledResult, ChatFetchError, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes'; import { IConversationOptions } from '../../../platform/chat/common/conversationOptions'; import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; @@ -484,6 +484,11 @@ export class DefaultIntentRequestHandler { return {}; } + private async getErrorDetails(error: ChatFetchError) { + const status = await this._octoKitService.getGitHubOutageStatus(); + return getErrorDetailsFromChatFetchError(error, this._authenticationService.copilotToken?.copilotPlan, status); + } + private async processResult(fetchResult: ChatResponse, responseMessage: string, chatResult: ChatResult | void, metadataFragment: Partial, baseModelTelemetry: ConversationalBaseTelemetryData, rounds: IToolCallRound[]): Promise { switch (fetchResult.type) { case ChatFetchResponseType.Success: @@ -491,16 +496,14 @@ export class DefaultIntentRequestHandler { case ChatFetchResponseType.OffTopic: return this.processOffTopicFetchResult(baseModelTelemetry); case ChatFetchResponseType.Canceled: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Cancelled, { message: errorDetails.message, type: 'user' }, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.QuotaExceeded: case ChatFetchResponseType.RateLimited: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); if (fetchResult.type === ChatFetchResponseType.RateLimited && fetchResult.capiError?.code?.startsWith('user_model_rate_limited') && !fetchResult.isAuto) { @@ -520,22 +523,19 @@ export class DefaultIntentRequestHandler { case ChatFetchResponseType.BadRequest: case ChatFetchResponseType.NetworkError: case ChatFetchResponseType.Failed: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, { message: errorDetails.message, type: 'server' }, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.Filtered: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: fetchResult.category } }; this.turn.setResponse(TurnStatus.Filtered, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.PromptFiltered: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: FilterReason.Prompt } }; this.turn.setResponse(TurnStatus.PromptFiltered, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; @@ -546,30 +546,26 @@ export class DefaultIntentRequestHandler { return chatResult; } case ChatFetchResponseType.AgentFailedDependency: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.Length: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.NotFound: // before we had `NotFound`, it would fall into Unknown, so behavior should be consistent case ChatFetchResponseType.Unknown: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.ExtensionBlocked: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; // This shouldn't happen, only 3rd party extensions should be blocked this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); diff --git a/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts b/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts index 10e110c361d61d..a104bd39250523 100644 --- a/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts +++ b/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts @@ -391,7 +391,7 @@ export class CodeMapper { return undefined; } const outageStatus = await this.octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this.authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, this.authenticationService.copilotToken?.copilotPlan, outageStatus); result = createOutcome([{ label: errorDetails.message, message: `request ${fetchResult.type}`, severity: 'error' }], errorDetails); } if (result.annotations.length || result.errorDetails) { diff --git a/extensions/copilot/src/platform/chat/common/commonTypes.ts b/extensions/copilot/src/platform/chat/common/commonTypes.ts index 6598662913436a..37984eb637dfdd 100644 --- a/extensions/copilot/src/platform/chat/common/commonTypes.ts +++ b/extensions/copilot/src/platform/chat/common/commonTypes.ts @@ -358,11 +358,11 @@ function getQuotaHitMessage(fetchResult: ChatFetchError, copilotPlan: string | u } } -export function getErrorDetailsFromChatFetchError(fetchResult: ChatFetchError, copilotPlan: string, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { +export function getErrorDetailsFromChatFetchError(fetchResult: ChatFetchError, copilotPlan: string | undefined, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { return { code: fetchResult.type, ...getErrorDetailsFromChatFetchErrorInner(fetchResult, copilotPlan, gitHubOutageStatus) }; } -function getErrorDetailsFromChatFetchErrorInner(fetchResult: ChatFetchError, copilotPlan: string, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { +function getErrorDetailsFromChatFetchErrorInner(fetchResult: ChatFetchError, copilotPlan: string | undefined, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { let details: ChatErrorDetails; switch (fetchResult.type) { case ChatFetchResponseType.OffTopic: From cca167843243a0a45890df486dc3f1171820bb4a Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 7 May 2026 02:24:09 -0700 Subject: [PATCH 17/28] PR feedback --- .../src/extension/byok/vscode-node/byokContribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts b/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts index 3cd08f201c2756..3bb2b4407f2a62 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts @@ -74,8 +74,8 @@ export class BYOKContrib extends Disposable implements IExtensionContribution { this._providers.set(AzureBYOKModelProvider.providerId, instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService)); this._providers.set(CustomOAIBYOKModelProvider.providerId, instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService)); - for (const [providerName, provider] of this._providers) { - this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerName, provider)); + for (const [providerId, provider] of this._providers) { + this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerId, provider)); } this._logService.info(`BYOK: registered ${this._providers.size} provider(s): ${Array.from(this._providers.keys()).join(', ')}`); } else if (!this._byokProvidersRegistered) { From 031b769bd9e798f4cce13f35871f2559a04b34e6 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 7 May 2026 11:05:51 +0100 Subject: [PATCH 18/28] Enhance shimmer effect for in-progress session titles (#314956) style: enhance shimmer effect for in-progress session titles with support checks Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../sessions/browser/media/sessionsList.css | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index 17899477bde53c..91396a31fc18c9 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -370,44 +370,41 @@ } } -/* Shimmer effect for in-progress session titles when not selected */ - -.monaco-list-row:not(.selected) .session-item.in-progress .session-title { - background: linear-gradient( - 90deg, - var(--vscode-strongForeground) 0%, - var(--vscode-strongForeground) 30%, - var(--vscode-chat-thinkingShimmer) 50%, - var(--vscode-strongForeground) 70%, - var(--vscode-strongForeground) 100% - ); - background-size: 400% 100%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: session-title-shimmer 3s linear infinite; -} - -.vs-dark .monaco-list-row:not(.selected) .session-item.in-progress .session-title, -.hc-black .monaco-list-row:not(.selected) .session-item.in-progress .session-title { - background-image: linear-gradient( - 90deg, - var(--vscode-strongForeground) 0%, - var(--vscode-strongForeground) 30%, - var(--vscode-descriptionForeground) 50%, - var(--vscode-strongForeground) 70%, - var(--vscode-strongForeground) 100% - ); -} +/* Shimmer effect for in-progress session titles when not selected. */ +/* Gated behind @supports because some environments apply */ +/* `-webkit-text-fill-color: transparent` without honoring */ +/* `background-clip: text`, which renders gradient-filled blocks */ +/* instead of clipped text. Also disabled under reduced motion. */ + +@supports ((background-clip: text) or (-webkit-background-clip: text)) { + @media not (prefers-reduced-motion: reduce) { + .monaco-list-row:not(.selected) .session-item.in-progress .session-title { + background: linear-gradient( + 90deg, + var(--vscode-strongForeground) 0%, + var(--vscode-strongForeground) 30%, + var(--vscode-chat-thinkingShimmer) 50%, + var(--vscode-strongForeground) 70%, + var(--vscode-strongForeground) 100% + ); + background-size: 400% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: session-title-shimmer 3s linear infinite; + } -@media (prefers-reduced-motion: reduce) { - .monaco-list-row:not(.selected) .session-item.in-progress .session-title { - animation: none; - background: none; - background-clip: border-box; - -webkit-background-clip: border-box; - color: var(--vscode-strongForeground); - -webkit-text-fill-color: var(--vscode-strongForeground); + .vs-dark .monaco-list-row:not(.selected) .session-item.in-progress .session-title, + .hc-black .monaco-list-row:not(.selected) .session-item.in-progress .session-title { + background-image: linear-gradient( + 90deg, + var(--vscode-strongForeground) 0%, + var(--vscode-strongForeground) 30%, + var(--vscode-descriptionForeground) 50%, + var(--vscode-strongForeground) 70%, + var(--vscode-strongForeground) 100% + ); + } } } From 576a7ee19ccfd9058820e0adf70dab868b2cbe99 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 7 May 2026 12:28:01 +0200 Subject: [PATCH 19/28] Disable builtin extensions contributing unsupported features in sessions window --- .../browser/extensionEnablementService.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 86cb30c390819a..ccff0f3760ed41 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -638,8 +638,17 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } - // Built-in extensions are always enabled in the sessions window. + // Built-in extensions are enabled in sessions window except the chat extension and extensions that contribute not supported features. if (extension.isBuiltin) { + if (extension.identifier.id.toLowerCase() === this._chatExtensionId) { + return false; + } + + const contributes = extension.manifest.contributes; + if (contributes?.debuggers || contributes?.views || contributes?.viewsContainers || contributes?.walkthroughs) { + return true; + } + return false; } From 4bfb680c409b3678b5ecd946add9747426e9946c Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 7 May 2026 11:41:30 +0100 Subject: [PATCH 20/28] Add borders for agents new session button and chat input in light theme (#314960) feat: add borders for agents new session button and chat input in light theme Co-authored-by: mrleemurray Co-authored-by: Copilot --- extensions/theme-defaults/themes/light_vs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 3fdbbead3d0d66..8941e8588c6662 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -34,7 +34,9 @@ "terminal.inactiveSelectionBackground": "#E5EBF1", "widget.border": "#d4d4d4", "actionBar.toggledBackground": "#dddddd", - "diffEditor.unchangedRegionBackground": "#f8f8f8" + "diffEditor.unchangedRegionBackground": "#f8f8f8", + "agentsNewSessionButton.border": "#D8D8D8", + "agentsChatInput.border": "#D8D8D8" }, "tokenColors": [ { From d3276862bdf8239170abad1fa2785df2ef74fcfc Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Thu, 7 May 2026 15:42:46 +0500 Subject: [PATCH 21/28] guarantee that return of TreeDataProvider.getChildren() is not mutated by vscode (#306955) allow return readonly T[] for TreeDataProvider.getChildren Co-authored-by: Alex Ross <38270282+alexr00@users.noreply.github.com> --- .../api/browser/mainThreadTreeViews.ts | 12 ++++++------ .../workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostTreeViews.ts | 4 ++-- .../api/test/browser/extHostTreeViews.test.ts | 4 ++-- .../workbench/browser/parts/views/treeView.ts | 18 +++++++++--------- src/vs/workbench/common/views.ts | 6 +++--- .../browser/userDataSyncConflictsView.ts | 2 +- src/vscode-dts/vscode.d.ts | 2 ++ 8 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index f641d14eac2765..4601f067dc70a1 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -292,12 +292,12 @@ class TreeViewDataProvider implements ITreeViewDataProvider { this.hasResolve = this._proxy.$hasResolve(this.treeViewId); } - async getChildren(treeItem?: ITreeItem): Promise { + async getChildren(treeItem?: ITreeItem): Promise { const batches = await this.getChildrenBatch(treeItem ? [treeItem] : undefined); return batches?.[0]; } - getChildrenBatch(treeItems?: ITreeItem[]): Promise { + getChildrenBatch(treeItems?: ITreeItem[]): Promise<(readonly ITreeItem[])[] | undefined> { if (!treeItems) { this.itemsMap.clear(); } @@ -317,12 +317,12 @@ class TreeViewDataProvider implements ITreeViewDataProvider { }); } - private convertTransferChildren(parents: ITreeItem[], children: (number | ITreeItem)[][] | undefined) { - const convertedChildren: (ITreeItem[] | undefined)[] = Array(parents.length); + private convertTransferChildren(parents: ITreeItem[], children: (readonly (number | ITreeItem)[])[] | undefined) { + const convertedChildren: (readonly ITreeItem[] | undefined)[] = Array(parents.length); if (children) { for (const childGroup of children) { const childGroupIndex = childGroup[0] as number; - convertedChildren[childGroupIndex] = childGroup.slice(1) as ITreeItem[]; + convertedChildren[childGroupIndex] = childGroup.slice(1) as readonly ITreeItem[]; } } return convertedChildren; @@ -366,7 +366,7 @@ class TreeViewDataProvider implements ITreeViewDataProvider { return this.itemsMap.size === 0; } - private async postGetChildren(elementGroups: (ITreeItem[] | undefined)[] | undefined): Promise { + private async postGetChildren(elementGroups: (readonly ITreeItem[] | undefined)[] | undefined): Promise { if (elementGroups === undefined) { return undefined; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7c94812d301e4c..e6dcc340860477 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2421,7 +2421,7 @@ export interface ExtHostTreeViewsShape { * for [x,y] returns * [[1,z]], where the inner array is [original index, ...children] */ - $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(number | ITreeItem)[][] | undefined>; + $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(readonly (number | ITreeItem)[])[] | undefined>; $handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index c0d05cbb486335..3424a8f62b2b81 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -164,7 +164,7 @@ export class ExtHostTreeViews extends Disposable implements ExtHostTreeViewsShap return view as vscode.TreeView; } - async $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(number | ITreeItem)[][] | undefined> { + async $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(readonly (number | ITreeItem)[])[] | undefined> { const treeView = this._treeViews.get(treeViewId); if (!treeView) { return Promise.reject(new NoTreeViewError(treeViewId)); @@ -488,7 +488,7 @@ class ExtHostTreeView extends Disposable { } } - async getChildren(parentHandle: TreeItemHandle | Root): Promise { + async getChildren(parentHandle: TreeItemHandle | Root): Promise { const parentElement = parentHandle ? this.getExtensionElement(parentHandle) : undefined; if (parentHandle && !parentElement) { this._logService.error(`No tree item with id \'${parentHandle}\' found.`); diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 2ca97fac8a80a3..59187e1d3c2a0e 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -20,14 +20,14 @@ import { runWithFakedTimers } from '../../../../base/test/common/timeTravelSched import { IExtHostTelemetry } from '../../common/extHostTelemetry.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -function unBatchChildren(result: (number | ITreeItem)[][] | undefined): ITreeItem[] | undefined { +function unBatchChildren(result: (readonly (number | ITreeItem)[])[] | undefined): readonly ITreeItem[] | undefined { if (!result || result.length === 0) { return undefined; } if (result.length > 1) { throw new Error('Unexpected result length, all tests are unbatched.'); } - return result[0].slice(1) as ITreeItem[]; + return result[0].slice(1) as readonly ITreeItem[]; } suite('ExtHostTreeView', function () { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 1c9305bd780ba8..487cf07936e3f8 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -366,12 +366,12 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return this._isEmpty; } - async getChildren(element?: ITreeItem): Promise { + async getChildren(element?: ITreeItem): Promise { const batches = await this.getChildrenBatch(element ? [element] : undefined); return batches?.[0]; } - private updateEmptyState(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): void { + private updateEmptyState(nodes: ITreeItem[], childrenGroups: (readonly ITreeItem[])[]): void { if ((nodes.length === 1) && (nodes[0] instanceof Root)) { const oldEmpty = this._isEmpty; this._isEmpty = (childrenGroups.length === 0) || (childrenGroups[0].length === 0); @@ -381,7 +381,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } } - private findCheckboxesUpdated(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): ITreeItem[] { + private findCheckboxesUpdated(nodes: ITreeItem[], childrenGroups: (readonly ITreeItem[])[]): ITreeItem[] { if (childrenGroups.length === 0) { return []; } @@ -401,8 +401,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return checkboxesUpdated; } - async getChildrenBatch(nodes?: ITreeItem[]): Promise { - let childrenGroups: ITreeItem[][]; + async getChildrenBatch(nodes?: ITreeItem[]): Promise<(readonly ITreeItem[])[]> { + let childrenGroups: (readonly ITreeItem[])[]; let checkboxesUpdated: ITreeItem[] = []; if (nodes?.every((node): node is Required => !!node.children)) { childrenGroups = nodes.map(node => node.children); @@ -1172,7 +1172,7 @@ class TreeViewDelegate implements IListVirtualDelegate { } } -async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise { +async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise<(readonly ITreeItem[])[] | undefined> { if (dataProvider.getChildrenBatch) { return dataProvider.getChildrenBatch(nodes); } else { @@ -1197,8 +1197,8 @@ class TreeDataSource implements IAsyncDataSource { } private batch: ITreeItem[] | undefined; - private batchPromise: Promise | undefined; - async getChildren(element: ITreeItem): Promise { + private batchPromise: Promise<(readonly ITreeItem[])[] | undefined> | undefined; + async getChildren(element: ITreeItem): Promise { const dataProvider = this.treeView.dataProvider; if (!dataProvider) { return []; @@ -1210,7 +1210,7 @@ class TreeDataSource implements IAsyncDataSource { this.batch.push(element); } const indexInBatch = this.batch.length - 1; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { setTimeout(async () => { const batch = this.batch; this.batch = undefined; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 29b8e0445288ac..da69831713055b 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -793,7 +793,7 @@ export interface ITreeItem { command?: TreeCommand; - children?: ITreeItem[]; + children?: readonly ITreeItem[]; parent?: ITreeItem; @@ -876,8 +876,8 @@ export class NoTreeViewError extends Error { export interface ITreeViewDataProvider { readonly isTreeEmpty?: boolean; readonly onDidChangeEmpty?: Event; - getChildren(element?: ITreeItem): Promise; - getChildrenBatch?(element?: ITreeItem[]): Promise; + getChildren(element?: ITreeItem): Promise; + getChildrenBatch?(element?: ITreeItem[]): Promise<(readonly ITreeItem[])[] | undefined>; } export interface ITreeViewDragAndDropController { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts index b26d97bf66a648..5543fa53d6fe36 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts @@ -64,7 +64,7 @@ export class UserDataSyncConflictsViewPane extends TreeViewPane implements IUser this.treeView.dataProvider = { getChildren() { return that.getTreeItems(); } }; } - private async getTreeItems(): Promise { + private async getTreeItems(): Promise { const roots: ITreeItem[] = []; const conflictResources: UserDataSyncConflictResource[] = this.userDataSyncService.conflicts diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index df78e006e8e0f0..e5cff6649f79b0 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -12249,6 +12249,8 @@ declare module 'vscode' { /** * Get the children of `element` or root if no element is passed. * + * *Note:* The result is not mutated by the API consumer; readonly arrays may be cast to `T[]`. + * * @param element The element from which the provider gets children. Can be `undefined`. * @returns Children of `element` or root if no element is passed. */ From 3c7a4b8121660d7a254f4f8c60a4ccdbaa00109f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 7 May 2026 20:43:16 +1000 Subject: [PATCH 22/28] fix: disable auto model feature in CLI configuration (#314936) * fix: disable auto model feature in CLI configuration * fix: update tests to explicitly enable CLIAutoModelEnabled after default changed to false Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/4384e16e-ab3e-4ad5-aa27-6e4d4439ed89 Co-authored-by: DonJayamanne <1948812+DonJayamanne@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- extensions/copilot/package.json | 2 +- .../node/test/copilotCliModels.spec.ts | 30 ++++++++++++++----- .../common/configurationService.ts | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 779f04f3d6938c..a9d929015b7197 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4690,7 +4690,7 @@ }, "github.copilot.chat.cli.autoModel.enabled": { "type": "boolean", - "default": true, + "default": false, "markdownDescription": "%github.copilot.config.cli.autoModel.enabled%", "tags": [ "advanced" diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts index b8d9ccb56912a8..894345e501e48c 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliModels.spec.ts @@ -191,7 +191,9 @@ describe('CopilotCLIModels', () => { }); it('resolves "auto" without querying SDK models', async () => { - const { models } = createModels({ hasSession: false }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: false, configService }); // Even without a session, 'auto' resolves to itself expect(await models.resolveModel('auto')).toBe('auto'); @@ -366,7 +368,9 @@ describe('CopilotCLIModels', () => { } it('always includes auto model in results', async () => { - const { models } = createModels({ hasSession: true }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -380,7 +384,9 @@ describe('CopilotCLIModels', () => { }); it('returns only auto when not authenticated', async () => { - const { models } = createModels({ hasSession: false }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: false, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -403,7 +409,9 @@ describe('CopilotCLIModels', () => { getRequestId: vi.fn(() => undefined), } as unknown as ICopilotCLISDK; - const { models } = createModels({ hasSession: true, sdk }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, sdk, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -430,7 +438,9 @@ describe('CopilotCLIModels', () => { }); it('returns full model list with auto prepended after fetch completes', async () => { - const { models } = createModels({ hasSession: true }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -444,7 +454,9 @@ describe('CopilotCLIModels', () => { }); it('resets to auto-only after auth change, then recovers', async () => { - const { models, auth } = createModels({ hasSession: true }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models, auth } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -559,8 +571,10 @@ describe('CopilotCLIModels', () => { expect(await models.resolveModel('auto')).toBeUndefined(); }); - it('includes auto model when setting is enabled (default)', async () => { - const { models } = createModels({ hasSession: true }); + it('includes auto model when setting is enabled', async () => { + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 96b58a71eb9173..be27557cc1112d 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -611,7 +611,7 @@ export namespace ConfigKey { export const OmitBaseAgentInstructions = defineAndMigrateSetting('chat.advanced.omitBaseAgentInstructions', 'chat.omitBaseAgentInstructions', false); export const CLIShowExternalSessions = defineSetting('chat.cli.showExternalSessions', ConfigType.Simple, true); export const CLIPlanExitModeEnabled = defineSetting('chat.cli.planExitMode.enabled', ConfigType.Simple, true); - export const CLIAutoModelEnabled = defineSetting('chat.cli.autoModel.enabled', ConfigType.Simple, true); + export const CLIAutoModelEnabled = defineSetting('chat.cli.autoModel.enabled', ConfigType.Simple, false); export const CLIModelDetailsEnabled = defineSetting('chat.agent.modelDetails.enabled', ConfigType.Simple, true); export const CLIPlanCommandEnabled = defineSetting('chat.cli.planCommand.enabled', ConfigType.Simple, true); export const CLIChatLazyLoadSessionItem = defineSetting('chat.cli.lazyLoadSessionItem.enabled', ConfigType.Simple, true); From 90ba4f25d5c48ac4641f0d53eca7546a34f4b16b Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 7 May 2026 11:43:35 +0100 Subject: [PATCH 23/28] Fix gap between radio buttons in tabbed action list (#314973) fix: reduce gap between radio buttons in tabbed action list Co-authored-by: mrleemurray --- src/vs/platform/actionWidget/browser/tabbedActionListWidget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css b/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css index 7dfc41c2384b51..2c0141dd6394c6 100644 --- a/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css +++ b/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css @@ -15,7 +15,7 @@ .action-widget .tabbed-action-list-tabbar .monaco-custom-radio { width: 100%; - gap: 8px; + gap: 4px; } .action-widget .tabbed-action-list-tabbar .monaco-custom-radio > .monaco-button { From 784f7e1c507216b40a4fc8cd749d260821350cba Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 May 2026 10:47:07 +0200 Subject: [PATCH 24/28] Ensures that component fixtures dont leak disposables --- .../componentFixtures/baseUI.fixture.ts | 24 +-- .../chat/chatToolRiskBadge.fixture.ts | 36 ++++ .../chat/chatWidget.fixture.ts | 87 +++++++--- .../chat/promptFilePickers.fixture.ts | 15 ++ .../editor/inlineChatZoneWidget.fixture.ts | 2 +- .../editor/inlineCompletions/other.fixture.ts | 12 +- .../editor/inlineCompletions/views.fixture.ts | 6 +- .../editor/multiDiffEditor.fixture.ts | 12 +- .../editor/peekReference.fixture.ts | 8 +- .../editor/renameWidget.fixture.ts | 2 + .../browser/componentFixtures/fixtureUtils.ts | 157 +++++++++++++----- .../imageCarousel.fixture.ts | 4 +- .../sessions/agentSessionsViewer.fixture.ts | 15 +- ...aiCustomizationManagementEditor.fixture.ts | 6 +- 14 files changed, 282 insertions(+), 104 deletions(-) diff --git a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts index 2bc08dd758784e..bd8b75e607b33f 100644 --- a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts @@ -330,7 +330,7 @@ function renderInputBoxes({ container, disposableStore }: ComponentFixtureContex // Count Badges // ============================================================================ -function renderCountBadges({ container }: ComponentFixtureContext): void { +function renderCountBadges({ container, disposableStore }: ComponentFixtureContext): void { container.style.padding = '16px'; container.style.display = 'flex'; container.style.gap = '12px'; @@ -350,7 +350,7 @@ function renderCountBadges({ container }: ComponentFixtureContext): void { label.style.color = 'var(--vscode-foreground)'; badgeContainer.appendChild(label); - new CountBadge(badgeContainer, { count }, themedBadgeStyles); + disposableStore.add(new CountBadge(badgeContainer, { count }, themedBadgeStyles)); container.appendChild(badgeContainer); } } @@ -381,12 +381,12 @@ function renderActionBar({ container, disposableStore }: ComponentFixtureContext })); horizontalBar.push([ - new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save')), - new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo')), - new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo')), + disposableStore.add(new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save'))), + disposableStore.add(new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo'))), + disposableStore.add(new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo'))), new Separator(), - new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find')), - new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace')), + disposableStore.add(new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find'))), + disposableStore.add(new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace'))), ]); // Action bar with disabled items @@ -404,9 +404,9 @@ function renderActionBar({ container, disposableStore }: ComponentFixtureContext })); mixedBar.push([ - new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { }), - new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }), - new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }), + disposableStore.add(new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { })), + disposableStore.add(new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { })), + disposableStore.add(new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { })), ]); } @@ -473,7 +473,7 @@ function renderProgressBars({ container, disposableStore }: ComponentFixtureCont // Highlighted Label // ============================================================================ -function renderHighlightedLabels({ container }: ComponentFixtureContext): void { +function renderHighlightedLabels({ container, disposableStore }: ComponentFixtureContext): void { container.style.padding = '16px'; container.style.display = 'flex'; container.style.flexDirection = 'column'; @@ -487,7 +487,7 @@ function renderHighlightedLabels({ container }: ComponentFixtureContext): void { row.style.gap = '8px'; const labelContainer = $('div'); - const label = new HighlightedLabel(labelContainer); + const label = disposableStore.add(new HighlightedLabel(labelContainer)); label.set(text, highlights); row.appendChild(labelContainer); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts index ecaa2e213b5ae0..d3e042d3c2cc42 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { ToolRiskBadgeWidget } from '../../../../contrib/chat/browser/widget/chatContentParts/toolInvocationParts/toolRiskBadgeWidget.js'; import { IToolRiskAssessment, ToolRiskLevel } from '../../../../contrib/chat/browser/tools/chatToolRiskAssessmentService.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { IFixtureMessage, renderChatWidget } from './chatWidget.fixture.js'; import '../../../../contrib/chat/browser/widget/media/chat.css'; @@ -42,6 +43,21 @@ function renderBadge(context: ComponentFixtureContext, state: RenderState): void container.appendChild(itemContainer); } +function makeInContextMessage(assessment?: IToolRiskAssessment): IFixtureMessage[] { + return [{ + user: '', + assistant: [{ + kind: 'terminalConfirmation', + command: 'git init', + riskAssessment: assessment, + riskLoading: !assessment, + }], + responseComplete: false, + }]; +} + +const inContextOptions = { width: 720, height: 400 }; + const greenAssessment: IToolRiskAssessment = { risk: ToolRiskLevel.Green, explanation: 'Reads workspace files and returns matches; no side effects.', @@ -77,4 +93,24 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { labels: { kind: 'screenshot' }, render: (ctx) => renderBadge(ctx, { kind: 'assessment', assessment: redAssessment }), }), + + GreenInContext: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(greenAssessment), ...inContextOptions }), + }), + + OrangeInContext: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(orangeAssessment), ...inContextOptions }), + }), + + RedInContext: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(redAssessment), ...inContextOptions }), + }), + + LoadingInContext: defineComponentFixture({ + labels: { kind: 'animated' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(), ...inContextOptions }), + }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts index 8a1200ae8f5543..f9bb34e7f5956e 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts @@ -20,7 +20,8 @@ import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../.. import { IChatWidget, IChatWidgetService } from '../../../../contrib/chat/browser/chat.js'; import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js'; import { ChatToolInvocation } from '../../../../contrib/chat/common/model/chatProgressTypes/chatToolInvocation.js'; -import { IToolData, ToolDataSource } from '../../../../contrib/chat/common/tools/languageModelToolsService.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../contrib/chat/common/tools/languageModelToolsService.js'; +import { IChatToolRiskAssessmentService, IToolRiskAssessment, ToolRiskLevel } from '../../../../contrib/chat/browser/tools/chatToolRiskAssessmentService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../../contrib/chat/common/constants.js'; @@ -30,18 +31,20 @@ import { FixtureMenuService, registerChatFixtureServices } from './chatFixtureUt import '../../../../contrib/chat/browser/widget/media/chat.css'; -interface IFixtureMessage { +export interface IFixtureMessage { readonly user: string; // user prompt text readonly assistant?: ReadonlyArray< | { kind: 'markdown'; text: string } | { kind: 'progress'; text: string } - | { kind: 'terminalConfirmation'; command: string; title?: string } + | { kind: 'terminalConfirmation'; command: string; title?: string; riskAssessment?: { risk: ToolRiskLevel; explanation: string }; riskLoading?: boolean } >; readonly responseComplete?: boolean; } -interface IChatWidgetFixtureOptions { +export interface IChatWidgetFixtureOptions { readonly messages: ReadonlyArray; + readonly width?: number; + readonly height?: number; } function makeUserMessage(text: string) { @@ -51,11 +54,24 @@ function makeUserMessage(text: string) { }; } -async function renderChatWidget(context: ComponentFixtureContext, options: IChatWidgetFixtureOptions): Promise { +export async function renderChatWidget(context: ComponentFixtureContext, options: IChatWidgetFixtureOptions): Promise { const { container, disposableStore } = context; const widgetHolder: { current: IChatWidget | undefined } = { current: undefined }; + const fixtureToolData: IToolData = { + id: 'fixture.terminalTool', + displayName: 'Terminal', + modelDescription: 'Run a command in the terminal', + source: ToolDataSource.Internal, + }; + + // Collect risk assessments from messages so the risk badge service can + // return them synchronously via getCached(). + const hasRiskAssessment = options.messages.some(m => m.assistant?.some(p => p.kind === 'terminalConfirmation' && p.riskAssessment)); + const hasRiskLoading = options.messages.some(m => m.assistant?.some(p => p.kind === 'terminalConfirmation' && p.riskLoading)); + const needsRiskService = hasRiskAssessment || hasRiskLoading; + const instantiationService = createEditorServices(disposableStore, { colorTheme: context.theme, additionalServices: (reg) => { @@ -74,14 +90,39 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat override getWidgetsByLocations() { return []; } override register() { return { dispose() { } }; } }()); + + if (needsRiskService) { + reg.defineInstance(ILanguageModelToolsService, new class extends mock() { + override onDidChangeTools = Event.None; + override onDidPrepareToolCallBecomeUnresponsive = Event.None; + override getTools() { return [fixtureToolData]; } + override getTool(id: string) { return id === fixtureToolData.id ? fixtureToolData : undefined; } + }()); + reg.defineInstance(IChatToolRiskAssessmentService, new class extends mock() { + override isEnabled() { return true; } + override getCached() { + // Return the first risk assessment found in the fixture messages. + for (const m of options.messages) { + for (const p of m.assistant ?? []) { + if (p.kind === 'terminalConfirmation' && p.riskAssessment) { + return p.riskAssessment; + } + } + } + return undefined; + } + // For riskLoading: assess() never resolves, keeping the badge in loading state. + override async assess(): Promise { return new Promise(() => { }); } + }()); + } }, }); const configService = instantiationService.get(IConfigurationService) as TestConfigurationService; - await configService.setUserConfiguration('chat', { + configService.setUserConfiguration('chat', { editor: { fontSize: 13, fontFamily: 'default', fontWeight: 'default', lineHeight: 0, wordWrap: 'off' }, }); - await configService.setUserConfiguration('editor', { fontFamily: 'monospace', fontLigatures: false }); + configService.setUserConfiguration('editor', { fontFamily: 'monospace', fontLigatures: false }); configService.setUserConfiguration(ChatConfiguration.ToolConfirmationCarousel, true); // Build a real ChatModel populated with hand-crafted requests/responses, then drive a @@ -94,13 +135,6 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat )); chatService.addSession(model); - const fixtureToolData: IToolData = { - id: 'fixture.terminalTool', - displayName: 'Terminal', - modelDescription: 'Run a command in the terminal', - source: ToolDataSource.Internal, - }; - for (const message of options.messages) { const request = model.addRequest(makeUserMessage(message.user), { variables: [] }, 0); const response = request.response!; @@ -137,8 +171,10 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat const viewModel = disposableStore.add(instantiationService.createInstance(ChatViewModel, model, undefined)); - container.style.width = '720px'; - container.style.height = '600px'; + const width = options.width ?? 720; + const height = options.height ?? 600; + container.style.width = `${width}px`; + container.style.height = `${height}px`; container.style.backgroundColor = 'var(--vscode-sideBar-background, var(--vscode-editor-background))'; container.classList.add('monaco-workbench'); @@ -198,9 +234,7 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat widgetHolder.current = fixtureWidget; inputPart.render(session, '', fixtureWidget); - inputPart.layout(720); - await new Promise(r => setTimeout(r, 50)); - inputPart.layout(720); + inputPart.layout(width); const listContainer = dom.$('.interactive-list'); listContainer.style.flex = '1 1 auto'; @@ -231,11 +265,7 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat listWidget.refresh(); const listHeight = 420; - listWidget.layout(listHeight, 720); - - // Allow the renderer to flush its async progressive rendering pass. - await new Promise(r => setTimeout(r, 100)); - listWidget.layout(listHeight, 720); + listWidget.layout(listHeight, width); listWidget.scrollTop = 0; } @@ -252,7 +282,14 @@ const PENDING_TOOL_APPROVAL: IFixtureMessage[] = [ { user: 'run git init', assistant: [ - { kind: 'terminalConfirmation', command: 'git init' }, + { + kind: 'terminalConfirmation', + command: 'git init', + riskAssessment: { + risk: ToolRiskLevel.Orange, + explanation: 'Initializes a new Git repository in the current directory. Reversible by removing the .git folder.', + }, + }, ], responseComplete: false, }, diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts index 8df42a30c25d1b..83ea3499dafe14 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts @@ -41,13 +41,28 @@ interface RenderPromptPickerOptions extends ComponentFixtureContext { } class FixtureQuickInputService extends QuickInputService { + private readonly _activePicks = new Set>(); + override createQuickPick(options: { useSeparators: true }): IQuickPick; override createQuickPick(options?: { useSeparators: boolean }): IQuickPick; override createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { const quickPick = super.createQuickPick(options) as IQuickPick; quickPick.ignoreFocusOut = true; + this._activePicks.add(quickPick); return quickPick; } + + override dispose(): void { + // Force-hide any open picks so PromptFilePickers' onDidHide handler + // disposes its internal DisposableStore (it skips disposal while + // `ignoreFocusOut` is true). + for (const pick of this._activePicks) { + pick.ignoreFocusOut = false; + pick.hide(); + } + this._activePicks.clear(); + super.dispose(); + } } export default defineThemedFixtureGroup({ path: 'chat/' }, { diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts index 1a8478b3dd54f5..eb4b9db4dd97f4 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts @@ -397,7 +397,7 @@ function renderInlineChatZoneWidget({ container, disposableStore, theme }: Compo zoneWidget.show(new Position(10, 1)); - const dummyModel = instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.EditorInline, canUseTools: false }); + const dummyModel = disposableStore.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.EditorInline, canUseTools: false })); zoneWidget.widget.chatWidget.setModel(dummyModel); zoneWidget.widget.chatWidget.setInputPlaceholder('Ask Copilot...'); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts index 55c97a33069ca1..55f7a785480fb4 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts @@ -231,7 +231,7 @@ function createLongDistanceEditor(options: { clearSuggestWidgetInlineCompletions: () => { }, dispose: () => { }, fetch: async () => true, - inlineCompletions: constObservable(new InlineCompletionsState([ + inlineCompletions: constObservable(disposableStore.add(new InlineCompletionsState([ InlineEditItem.createForTest( TextModelValueReference.snapshot(textModel), new Range( @@ -242,11 +242,11 @@ function createLongDistanceEditor(options: { ), options.newText ) - ], undefined)), + ], undefined))), loading: constObservable(false), seedInlineCompletionsWithSuggestWidget: () => { }, seedWithCompletion: () => { }, - suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + suggestWidgetInlineCompletions: constObservable(disposableStore.add(InlineCompletionsState.createEmpty())), }); const editorWidgetOptions: ICodeEditorWidgetOptions = { @@ -317,17 +317,17 @@ export function createApp(config: Config) { clearSuggestWidgetInlineCompletions: () => { }, dispose: () => { }, fetch: async () => true, - inlineCompletions: constObservable(new InlineCompletionsState([ + inlineCompletions: constObservable(disposableStore.add(new InlineCompletionsState([ InlineEditItem.createForTest( TextModelValueReference.snapshot(targetModel), new Range(1, 1, 3, 100), `export interface Config {\n\tport: number;\n\thost: string;\n\tdebug: boolean;\n}` ) - ], undefined)), + ], undefined))), loading: constObservable(false), seedInlineCompletionsWithSuggestWidget: () => { }, seedWithCompletion: () => { }, - suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + suggestWidgetInlineCompletions: constObservable(disposableStore.add(InlineCompletionsState.createEmpty())), }); const editor = disposableStore.add(instantiationService.createInstance( diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts index 484cb369dbacf5..cef9ca0e48bd77 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts @@ -56,7 +56,7 @@ function renderInlineEdit(options: InlineEditOptions): void { clearSuggestWidgetInlineCompletions: () => { }, dispose: () => { }, fetch: async () => true, - inlineCompletions: constObservable(new InlineCompletionsState([ + inlineCompletions: constObservable(disposableStore.add(new InlineCompletionsState([ InlineEditItem.createForTest( TextModelValueReference.snapshot(textModel), new Range( @@ -67,11 +67,11 @@ function renderInlineEdit(options: InlineEditOptions): void { ), options.newText ) - ], undefined)), + ], undefined))), loading: constObservable(false), seedInlineCompletionsWithSuggestWidget: () => { }, seedWithCompletion: () => { }, - suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + suggestWidgetInlineCompletions: constObservable(disposableStore.add(InlineCompletionsState.createEmpty())), }); const editorWidgetOptions: ICodeEditorWidgetOptions = { diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts index cf8c005f0ac703..3d2758eb3275a2 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts @@ -124,7 +124,7 @@ function renderMultiDiffEditor({ container, disposableStore, theme }: ComponentF documents: ValueWithChangeEvent.const([doc1, doc2, doc3]), }; - const viewModel = widget.createViewModel(model); + const viewModel = disposableStore.add(widget.createViewModel(model)); widget.setViewModel(viewModel); widget.layout(new Dimension(800, 600)); } @@ -141,9 +141,9 @@ class DelayedDocumentDiffProvider implements IDocumentDiffProvider { readonly onDidChange: Event = () => toDisposable(() => { }); constructor(private readonly _delayMs: number) { } - async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, _cancellationToken: CancellationToken): Promise { - await timeout(this._delayMs); - if (_cancellationToken.isCancellationRequested || original.isDisposed() || modified.isDisposed()) { + async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, cancellationToken: CancellationToken): Promise { + await timeout(this._delayMs, cancellationToken); + if (cancellationToken.isCancellationRequested || original.isDisposed() || modified.isDisposed()) { return ({ changes: [], quitEarly: true, @@ -223,7 +223,7 @@ function renderMultiDiffEditorIncrementalUpdate() { // Start with only doc1 — its diff resolves immediately (800ms virtual) const documents = new ValueWithChangeEvent[]>([doc1]); const model: IMultiDiffEditorModel = { documents }; - const viewModel = widget.createViewModel(model); + const viewModel = disposableStore.add(widget.createViewModel(model)); widget.setViewModel(viewModel); widget.layout(new Dimension(800, 600)); @@ -266,7 +266,7 @@ function renderMultiDiffEditorDocumentSwap() { // Start with A and B const documents = new ValueWithChangeEvent[]>([docA, docB]); const model: IMultiDiffEditorModel = { documents }; - const viewModel = widget.createViewModel(model); + const viewModel = disposableStore.add(widget.createViewModel(model)); widget.setViewModel(viewModel); widget.layout(new Dimension(800, 600)); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts index 938e24504ba589..a66087769e6864 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts @@ -106,7 +106,7 @@ function renderPeekReference({ container, disposableStore, theme }: ComponentFix contributions: [] }; - const editor = disposableStore.add(instantiationService.createInstance( + const editor = instantiationService.createInstance( CodeEditorWidget, container, { @@ -118,7 +118,7 @@ function renderPeekReference({ container, disposableStore, theme }: ComponentFix cursorBlinking: 'solid', }, editorWidgetOptions - )); + ); editor.setModel(textModel); editor.focus(); @@ -131,7 +131,11 @@ function renderPeekReference({ container, disposableStore, theme }: ComponentFix true, layoutData, ); + // Register widget BEFORE editor so widget.dispose() runs first; otherwise + // `ReferenceWidget.dispose()` calls `observableCodeEditor(disposed editor)` + // which creates a fresh untracked ObservableCodeEditor. disposableStore.add(referenceWidget); + disposableStore.add(editor); const range = { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 21 }; referenceWidget.setTitle('processFile'); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts index 37e16a7b6a5f8a..364cea7621c902 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; @@ -91,6 +92,7 @@ function renderRenameWidget(options: RenameFixtureOptions): void { undefined, cts ); + disposableStore.add(toDisposable(() => renameWidget.cancelInput(false, 'fixture-teardown'))); } export default defineThemedFixtureGroup({ path: 'editor/' }, { diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index 9595e4c8fa8a5c..113f198fcd0bf5 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -8,6 +8,7 @@ import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer'; import { DisposableStore, DisposableTracker, IDisposable, IReference, setDisposableTracker, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; +import { ModifierKeyEmitter } from '../../../../base/browser/dom.js'; // eslint-disable-next-line local/code-import-patterns import '../../../../../../build/vite/style.css'; import '../../../browser/media/style.css'; @@ -444,6 +445,23 @@ export class FixtureLogService extends NullLogService { } } +/** + * `ModelService` for fixtures that disposes all owned text models when the + * service itself is disposed. This is safe because `TestInstantiationService` + * is the first item added to the fixture's `DisposableStore`, so it disposes + * last (LIFO) — after all widgets have already torn down. + */ +export class FixtureModelService extends ModelService { + override dispose(): void { + for (const model of this.getModels()) { + if (!model.isDisposed()) { + model.dispose(); + } + } + super.dispose(); + } +} + /** * `ITextModelService` for fixtures that resolves URIs against `IModelService`. * Models created via `createTextModel` (which uses `IModelService.createModel`) @@ -524,7 +542,7 @@ export function createEditorServices(disposables: DisposableStore, options?: Cre define(IThemeService, TestThemeService); } define(ILogService, FixtureLogService); - define(IModelService, ModelService); + define(IModelService, FixtureModelService); define(ICodeEditorService, TestCodeEditorService); define(IContextKeyService, MockContextKeyService); define(ICommandService, TestCommandService); @@ -661,7 +679,16 @@ export function createEditorServices(disposables: DisposableStore, options?: Cre }, }); - const instantiationService = disposables.add(new TestInstantiationService(services, true)); + // Pass `_properDispose: true` so the underlying `InstantiationService`'s + // dispose runs, which disposes services it instantiated lazily from + // `SyncDescriptor`s (e.g. MenuService, ContextKeyService). Without this, + // production services with internal Disposables leak past the fixture. + // + // Don't add TestInstantiationService to disposables immediately — it must + // dispose runs, which disposes services it instantiated lazily from + // `SyncDescriptor`s (e.g. MenuService, ContextKeyService). Without this, + // production services with internal Disposables leak past the fixture. + const instantiationService = disposables.add(new TestInstantiationService(services, true, undefined, true)); disposables.add(toDisposable(() => { for (const id of serviceIdentifiers) { @@ -781,7 +808,7 @@ export interface ComponentFixtureContext { export interface ComponentFixtureOptions { render: (context: ComponentFixtureContext) => void | Promise; labels?: ThemedFixtureGroupLabels; - virtualTime?: { enabled?: boolean; durationMs?: number }; + virtualTime?: { enabled?: boolean; durationMs?: number; teardownDrainMs?: number }; } type ThemedFixtures = ReturnType; @@ -800,10 +827,6 @@ if (logOutsideTime) { let fixtureRenderCounter = 0; -// See TODO in defineComponentFixture: leak errors detected during teardown are -// stashed here and rethrown from the next fixture render. -let pendingLeakErrorToThrow: Error | undefined; - /** * Creates Dark and Light fixture variants from a single render function. * The render function receives a context with container and disposableStore. @@ -818,60 +841,98 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed displayMode: { type: 'component' }, background: theme === darkTheme ? 'dark' : 'light', render: async (container: HTMLElement, context) => { - // TODO: component-explorer currently ignores errors thrown from the - // teardown disposable (where leak detection runs, after the screenshot). - // Until it surfaces those, we stash the leak error and rethrow it from - // the next fixture render so the failure still becomes visible. - const pendingLeakError = pendingLeakErrorToThrow; - pendingLeakErrorToThrow = undefined; - if (pendingLeakError) { - throw pendingLeakError; - } - const disposableStore = new DisposableStore(); // Do not enable virtual time in explorer ui, as multiple fixtures are rendered in parallel. const virtualTimeEnabled = (options.virtualTime?.enabled ?? true) && context.host.kind !== 'explorer-ui'; - // Detect disposable leaks the same way unit tests do (`ensureNoDisposablesAreLeakedInTestSuite`). // The tracker is global and therefore unsafe when fixtures render in parallel, // so it is only enabled outside the explorer UI (e.g. in screenshot/CI mode). - const leakDetectionEnabled = false && context.host.kind !== 'explorer-ui'; + const leakDetectionEnabled = true && context.host.kind !== 'explorer-ui'; + // Warm up the `ModifierKeyEmitter` singleton before the leak tracker + // starts so its long-lived `DisposableStore` (created on first + // `MenuEntryActionViewItem.render`) doesn't show up as a leak in + // the first fixture that uses a menu toolbar. + if (leakDetectionEnabled) { + ModifierKeyEmitter.getInstance(); + } const tracker = leakDetectionEnabled ? new DisposableTracker() : undefined; if (tracker) { setDisposableTracker(tracker); } - const leakLabel = `${(options.labels ? resolveLabels(options.labels).join('/') : '')}/${theme === darkTheme ? 'Dark' : 'Light'} (render#${fixtureRenderCounter + 1})`; + // Virtual time infrastructure lives across the whole fixture + // lifetime (render + dispose). This lets us advance virtual time + // during dispose to drain async cleanup work (e.g. `Promise.race` + // guards behind `timeout(1000)` that hold references until they + // settle) before the leak tracker checks for undisposed objects. + const clock = new VirtualClock(Date.now()); + const p = new VirtualTimeProcessor( + clock, + drainMicrotasksEmbedding(realTimeApi), + realTimeApi, + { defaultMaxEvents: 100 }, + ); + const virtualTimeApi = createVirtualTimeApi(clock, { fakeRequestAnimationFrame: true }); + const teardownDrainMs = options.virtualTime?.teardownDrainMs ?? 1100; + + // Single async dispose orchestrates teardown order: + // 1. dispose user disposables (synchronous part) + // 2. drain virtual time (so timers scheduled during dispose + // — like `Promise.race([..., timeout(1000)])` — settle and + // release their captured references) + // 3. tear down virtual time (uninstall global API, dispose `p`) + // 4. stop tracker and check for leaks + // All on one disposable so the steps run in order. + context.addDisposable({ + dispose: async () => { + // Re-push virtual time so any `setTimeout`/`setInterval` + // calls made by `dispose()` of fixture-owned objects + // land in `p` and can be drained below. Render unpushes + // virtual time when it completes (so screenshot capture + // etc. can use real timers), so we have to push again. + let teardownTimeApi: IDisposable | undefined; + if (virtualTimeEnabled) { + teardownTimeApi = pushGlobalTimeApi(virtualTimeApi); + } - context.addDisposable(toDisposable(() => { - disposableStore.dispose(); - if (tracker) { - setDisposableTracker(null); - const result = tracker.computeLeakingDisposables(); - if (result) { - console.error(result.details); - pendingLeakErrorToThrow = new Error(`[leak detected in previous fixture: ${leakLabel}] There are ${result.leaks.length} undisposed disposables!${result.details}`); + try { + disposableStore.dispose(); + } catch (e) { + console.error(`[ComponentFixture] error disposing fixture: ${e instanceof Error ? e.stack : e}`); } - } - })); - async function actualRender() { - const schedulerStore = disposableStore.add(new DisposableStore()); - const clock = new VirtualClock(Date.now()); - const p = schedulerStore.add(new VirtualTimeProcessor( - clock, - drainMicrotasksEmbedding(realTimeApi), - realTimeApi, - { defaultMaxEvents: 100 }, - )); + if (virtualTimeEnabled) { + try { + await p.run({ + until: untilTime(clock.now + teardownDrainMs), + maxEvents: 1000, + maxTraceDepth: 5, + }); + } catch (e) { + console.error(`[ComponentFixture] error draining virtual time during teardown: ${e instanceof Error ? e.stack : e}`); + } + } - await setupTheme(container, theme); + teardownTimeApi?.dispose(); + p.dispose(); + + if (tracker) { + setDisposableTracker(null); + const result = tracker.computeLeakingDisposables(); + if (result) { + throw new Error(`There are ${result.leaks.length} undisposed disposables!${result.details}`); + } + } + }, + }); - const virtualTimeApi = createVirtualTimeApi(clock, { fakeRequestAnimationFrame: true }); + async function actualRender() { + await setupTheme(container, theme); + let renderTimeApi: IDisposable | undefined; if (virtualTimeEnabled) { - schedulerStore.add(pushGlobalTimeApi(virtualTimeApi)); + renderTimeApi = pushGlobalTimeApi(virtualTimeApi); disposableStore.add(installFakeRunWhenIdle((_targetWindow, callback, _timeout?) => { const stackTrace = new Error().stack; @@ -917,7 +978,9 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed } throw e; } finally { - schedulerStore.dispose(); + // Unpush virtual time so the post-render flow (screenshot + // capture, stability checks, …) runs with real timers. + renderTimeApi?.dispose(); } } @@ -932,6 +995,14 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed // Trace-reset escapes virtual time so it actually fires. afterMicrotaskClosure: cb => nextMacrotask(realTimeApi, cb), }); + + const wantsTimeTrace = !!context.input && typeof context.input === 'object' && !!(context.input as Record).outputTimeTrace; + if (wantsTimeTrace && virtualTimeEnabled && p.history.length > 0) { + const startTime = p.history[0].time; + const history = buildHistoryFromTasks(p.history, startTime); + return { output: renderSwimlanes(history) }; + } + return undefined; }, }); diff --git a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts index 88dc3ac98f9c0b..89a853231a5b65 100644 --- a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts @@ -60,7 +60,7 @@ async function renderCarousel(context: ComponentFixtureContext, collection: IIma colorTheme: theme, additionalServices: ({ defineInstance }) => { const fileService = new FileService(new NullLogService()); - fileService.registerProvider(Schemas.file, new NullFileSystemProvider()); + disposableStore.add(fileService.registerProvider(Schemas.file, new NullFileSystemProvider())); disposableStore.add(fileService); defineInstance(IFileService, fileService); defineInstance(IWebviewService, new class extends mock() { }()); @@ -73,7 +73,7 @@ async function renderCarousel(context: ComponentFixtureContext, collection: IIma editor.create(container); editor.layout(new Dimension(600, 500)); - const input = new ImageCarouselEditorInput(collection, startIndex); + const input = disposableStore.add(new ImageCarouselEditorInput(collection, startIndex)); await editor.setInput(input, undefined, {}, CancellationToken.None); } diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts index 1c06a3540bc7de..45c4a252e56588 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts @@ -11,6 +11,7 @@ import { FuzzyScore } from '../../../../../base/common/filters.js'; import { ITreeNode } from '../../../../../base/browser/ui/tree/tree.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { Event } from '../../../../../base/common/event.js'; +import { toDisposable } from '../../../../../base/common/lifecycle.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'; @@ -134,7 +135,12 @@ function renderSessionItem(ctx: ComponentFixtureContext, session: IAgentSession, container.appendChild(listRow); const template = renderer.renderTemplate(listRow); - renderer.renderElement(wrapAsTreeNode(session), 0, template); + const treeNode = wrapAsTreeNode(session); + renderer.renderElement(treeNode, 0, template); + disposableStore.add(toDisposable(() => { + renderer.disposeElement(treeNode, 0, template); + renderer.disposeTemplate(template); + })); } function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionSection): void { @@ -160,7 +166,12 @@ function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionS container.appendChild(listRow); const template = renderer.renderTemplate(listRow); - renderer.renderElement(wrapAsTreeNode(section), 0, template); + const treeNode = wrapAsTreeNode(section); + renderer.renderElement(treeNode, 0, template); + disposableStore.add(toDisposable(() => { + renderer.disposeElement(treeNode, 0, template); + renderer.disposeTemplate(template); + })); } // ============================================================================ diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts index b55ed9c1a8a9d6..a1ec2496ce54b0 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts @@ -703,7 +703,8 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor languageServiceRef.value = instantiationService.get(ILanguageService); for (const [uri, content] of fileContents) { if (!modelServiceRef.value.getModel(uri)) { - modelServiceRef.value.createModel(content, null, uri, false); + const model = modelServiceRef.value.createModel(content, null, uri, false); + ctx.disposableStore.add({ dispose: () => model.dispose() }); } } @@ -713,7 +714,8 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor editor.create(ctx.container); editor.layout(new Dimension(width, height)); - await editor.setInput(AICustomizationManagementEditorInput.getOrCreate(), undefined, {}, CancellationToken.None); + const editorInput = ctx.disposableStore.add(AICustomizationManagementEditorInput.getOrCreate()); + await editor.setInput(editorInput, undefined, {}, CancellationToken.None); if (options.selectedSection) { editor.selectSectionById(options.selectedSection); From 817db05a56463c0369815b0b374218bfa9438896 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 May 2026 10:51:59 +0200 Subject: [PATCH 25/28] Fix disposable leaks and cancellation handling across editor, chat, and menu components --- src/vs/base/common/async.ts | 9 ++++- .../base/common/observableInternal/index.ts | 2 +- .../observableInternal/reactions/autorun.ts | 36 +++++++++++++++++++ .../widget/diffEditor/diffEditorViewModel.ts | 13 +++---- .../multiDiffEditor/diffEditorItemTemplate.ts | 2 +- .../multiDiffEditorViewModel.ts | 7 ++-- src/vs/platform/actions/common/menuService.ts | 9 ++--- .../chatReferencesContentPart.ts | 4 +-- .../browser/widget/input/chatInputPart.ts | 6 ++-- .../contrib/chat/common/model/chatModel.ts | 11 +++--- 10 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 80da7321ed2fd4..0b10da5ce44d3a 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from './cancellation.js'; -import { BugIndicatingError, CancellationError } from './errors.js'; +import { BugIndicatingError, CancellationError, isCancellationError } from './errors.js'; import { Emitter, Event } from './event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, MutableDisposable, toDisposable } from './lifecycle.js'; import { extUri as defaultExtUri, IExtUri } from './resources.js'; @@ -116,6 +116,13 @@ export function raceCancellationError(promise: Promise, token: Cancellatio }); } +export function rejectIfNotCanceled(err: unknown): undefined { + if (isCancellationError(err)) { + return undefined; + } + return Promise.reject(err) as never; +} + /** * Wraps a cancellable promise such that it is no cancellable. Can be used to * avoid issues with shared promises that would normally be returned as diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index a924e98352669e..0115e59d4180ea 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -6,7 +6,7 @@ // This is a facade for the observable implementation. Only import from here! export { observableValueOpts } from './observables/observableValueOpts.js'; -export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta, autorunPerKeyedItem, autorunSelfDisposable } from './reactions/autorun.js'; +export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta, autorunPerKeyedItem, autorunSelfDisposable, registerAutorunSelfDisposable } from './reactions/autorun.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'; diff --git a/src/vs/base/common/observableInternal/reactions/autorun.ts b/src/vs/base/common/observableInternal/reactions/autorun.ts index dad3cbdd203292..4df3da54541f7a 100644 --- a/src/vs/base/common/observableInternal/reactions/autorun.ts +++ b/src/vs/base/common/observableInternal/reactions/autorun.ts @@ -232,6 +232,7 @@ export interface IReaderWithDispose extends IReaderWithStore, IDisposable { } /** * An autorun with a `dispose()` method on its `reader` which cancels the autorun. * It it safe to call `dispose()` synchronously. + * @deprecated Use autorunSelfDisposable2 */ export function autorunSelfDisposable(fn: (reader: IReaderWithDispose) => void, debugLocation = DebugLocation.ofCaller()): IDisposable { let ar: IDisposable | undefined; @@ -256,3 +257,38 @@ export function autorunSelfDisposable(fn: (reader: IReaderWithDispose) => void, return ar; } + + +/** + * An autorun with a `dispose()` method on its `reader` which cancels the autorun. + * It it safe to call `dispose()` synchronously. + * TODO@hediet/copilot: rename to delete autorunSelfDisposable, and rename autorunSelfDisposable2 to autorunSelfDisposable. + */ +export function registerAutorunSelfDisposable(store: DisposableStore, fn: (reader: IReaderWithDispose) => void, debugLocation = DebugLocation.ofCaller()): void { + let ar: IDisposable | undefined; + let disposeSync = false; + + // eslint-disable-next-line prefer-const + ar = autorun(reader => { + fn({ + delayedStore: reader.delayedStore, + store: reader.store, + readObservable: reader.readObservable.bind(reader), + dispose: () => { + if (!ar) { + // dispose on first run, ar is not initialized yet. + disposeSync = true; + } else { + // dispose on reaction, ar is already registered. + store.delete(ar); + } + } + }); + }, debugLocation); + + if (disposeSync) { + ar.dispose(); + } else { + store.add(ar); + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index 2b233fc81c3358..9d9dfc2e7c12c6 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { rejectIfNotCanceled, RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ISettableObservable, ITransaction, autorun, autorunWithStore, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; +import { IObservable, IReader, ISettableObservable, ITransaction, autorun, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; import { IDiffProviderFactoryService } from './diffProviderFactoryService.js'; import { filterWithPrevious } from './utils.js'; import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; @@ -261,8 +261,9 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo debouncer.schedule(); })); - this._register(autorunWithStore(async (reader, store) => { + this._register(autorun(async (reader) => { /** @description compute diff */ + const store = reader.store; // So that they get recomputed when these settings change this._options.hideUnchangedRegionsMinimumLineCount.read(reader); @@ -294,9 +295,9 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo ignoreTrimWhitespace: this._options.ignoreTrimWhitespace.read(reader), maxComputationTimeMs: this._options.maxComputationTimeMs.read(reader), computeMoves: this._options.showMoves.read(reader), - }, this._cancellationTokenSource.token); + }, this._cancellationTokenSource.token).catch(rejectIfNotCanceled); - if (this._cancellationTokenSource.token.isCancellationRequested) { + if (!result || this._cancellationTokenSource.token.isCancellationRequested) { return; } if (model.original.isDisposed() || model.modified.isDisposed()) { @@ -348,7 +349,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo } public async waitForDiff(): Promise { - await waitForState(this.isDiffUpToDate, s => s); + await waitForState(this.isDiffUpToDate, s => s, undefined, this._cancellationTokenSource.token).catch(rejectIfNotCanceled); } public serializeState(): SerializedState { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index d7192151b3460a..bd6af43721efe6 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -129,7 +129,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< this._lastScrollTop = -1; this._isSettingScrollTop = false; - const btn = new Button(this._elements.collapseButton, {}); + const btn = this._register(new Button(this._elements.collapseButton, {})); this._register(autorun(reader => { btn.element.className = ''; diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 8d0acd345b8b15..01cf1197415024 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, ITransaction, ObservablePromise, ObservableResolvedPromise, constObservable, derived, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; -import { timeout } from '../../../../base/common/async.js'; +import { rejectIfNotCanceled, timeout } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -17,6 +17,7 @@ import { DiffEditorOptions } from '../diffEditor/diffEditorOptions.js'; import { DiffEditorViewModel } from '../diffEditor/diffEditorViewModel.js'; import { RefCounted } from '../diffEditor/utils.js'; import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; +import { cancelOnDispose } from '../../../../base/common/cancellation.js'; export class MultiDiffEditorViewModel extends Disposable { private readonly _documents: IObservable[] | 'loading'>; @@ -186,8 +187,8 @@ export class DocumentDiffItemViewModel extends Disposable { this.waitForInitialDiffOr1s = new ObservablePromise( Promise.race([ - this.diffEditorViewModel.waitForDiff(), - timeout(1000), + this.diffEditorViewModel.waitForDiff().catch(rejectIfNotCanceled), + timeout(1000, cancelOnDispose(this._store)).catch(rejectIfNotCanceled), ]) ); } diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index f5b7046bbb9674..f62922ec710936 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -5,7 +5,7 @@ import { RunOnceScheduler } from '../../../base/common/async.js'; import { DebounceEmitter, Emitter, Event } from '../../../base/common/event.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { DisposableStore, Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import { IMenu, IMenuActionOptions, IMenuChangeEvent, IMenuCreateOptions, IMenuItem, IMenuItemHide, IMenuService, isIMenuItem, isISubmenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from './actions.js'; import { ICommandAction, ILocalizedString } from '../../action/common/action.js'; import { ICommandService } from '../../commands/common/commands.js'; @@ -16,7 +16,7 @@ import { removeFastWithoutKeepingOrder } from '../../../base/common/arrays.js'; import { localize } from '../../../nls.js'; import { IKeybindingService } from '../../keybinding/common/keybinding.js'; -export class MenuService implements IMenuService { +export class MenuService extends Disposable implements IMenuService { declare readonly _serviceBrand: undefined; @@ -27,7 +27,8 @@ export class MenuService implements IMenuService { @IKeybindingService private readonly _keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, ) { - this._hiddenStates = new PersistedMenuHideState(storageService); + super(); + this._hiddenStates = this._register(new PersistedMenuHideState(storageService)); } createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu { @@ -51,7 +52,7 @@ export class MenuService implements IMenuService { } } -class PersistedMenuHideState { +class PersistedMenuHideState implements IDisposable { private static readonly _key = 'menu.hiddenCommands'; 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 849393873dd5a8..6893c58f15c9d6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -215,7 +215,7 @@ export class CollapsibleListPool extends Disposable { const container = $('.chat-used-context-list'); store.add(createFileIconThemableTreeContainerScope(container, this.themeService)); - const list = this.instantiationService.createInstance( + const list = store.add(this.instantiationService.createInstance( WorkbenchList, 'ChatListRenderer', container, @@ -268,7 +268,7 @@ export class CollapsibleListPool extends Disposable { } }, }, - }); + })); return { list, 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 bf5b6c9bf31ea0..1bb83a8fd5019f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2644,7 +2644,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { - inputModel = this.modelService.createModel('', null, this.inputUri, true); + inputModel = this._register(this.modelService.createModel('', null, this.inputUri, true)); } this.textModelResolverService.createModelReference(this.inputUri).then(ref => { @@ -3164,13 +3164,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.updateToolConfirmationCarouselMaxHeight(); const capturedKey = key; - Event.once(part.onDidEmpty)(() => { + this._register(Event.once(part.onDidEmpty)(() => { this._chatToolConfirmationCarousels.deleteAndDispose(capturedKey); if (this._currentSessionKey === capturedKey) { dom.clearNode(this.chatToolConfirmationCarouselContainer); dom.hide(this.chatToolConfirmationCarouselContainer); } - }); + })); return part; } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 56a6e8feeb081c..aa624dfbe52862 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -9,12 +9,12 @@ import { VSBuffer, decodeHex, encodeHex } from '../../../../../base/common/buffe import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { revive } from '../../../../../base/common/marshalling.js'; import { Schemas } from '../../../../../base/common/network.js'; import { equals } from '../../../../../base/common/objects.js'; -import { IObservable, autorun, autorunSelfDisposable, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js'; +import { IObservable, autorun, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts, registerAutorunSelfDisposable } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { hasKey, WithDefinedProps } from '../../../../../base/common/types.js'; import { URI, UriDto } from '../../../../../base/common/uri.js'; @@ -761,7 +761,8 @@ class ResponseView extends AbstractResponse { } export class Response extends AbstractResponse implements IDisposable { - private _onDidChangeValue = new Emitter(); + private readonly _store = new DisposableStore(); + private _onDidChangeValue = this._store.add(new Emitter()); public get onDidChangeValue() { return this._onDidChangeValue.event; } @@ -778,7 +779,7 @@ export class Response extends AbstractResponse implements IDisposable { } dispose(): void { - this._onDidChangeValue.dispose(); + this._store.dispose(); } @@ -902,7 +903,7 @@ export class Response extends AbstractResponse implements IDisposable { }); } else if (progress.kind === 'toolInvocation') { - autorunSelfDisposable(reader => { + registerAutorunSelfDisposable(this._store, reader => { progress.state.read(reader); // update repr when state changes this._contentChanged(false); From 8ff4538da0ea4651c37d70458843d140e74b3eac Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 May 2026 11:37:17 +0200 Subject: [PATCH 26/28] Fail CI if a component fixture throws & enable node_module caching --- .github/workflows/component-fixture-tests.yml | 25 +++++++++++++ .github/workflows/screenshot-test.yml | 35 ++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.github/workflows/component-fixture-tests.yml b/.github/workflows/component-fixture-tests.yml index cdd00061ead807..325b550ebda008 100644 --- a/.github/workflows/component-fixture-tests.yml +++ b/.github/workflows/component-fixture-tests.yml @@ -28,7 +28,22 @@ jobs: with: node-version-file: .nvmrc + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v5 + with: + path: .build/node_modules_cache + key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 @@ -36,13 +51,23 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build - name: Install rspack dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build/rspack + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + - name: Transpile source run: npm run transpile-client diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index 8c372e13428894..099a6397728f93 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -36,7 +36,22 @@ jobs: with: node-version-file: .nvmrc + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v5 + with: + path: .build/node_modules_cache + key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 @@ -44,13 +59,23 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build - name: Install rspack dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build/rspack + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + - name: Copy codicons run: cp node_modules/@vscode/codicons/dist/codicon.ttf src/vs/base/browser/ui/codicons/codicon/codicon.ttf @@ -328,11 +353,11 @@ jobs: diff -u test/componentFixtures/blocks-ci-screenshots.md /tmp/blocks-ci-updated.md || true exit 1 - # - name: Fail if fixtures had errors - # if: always() && steps.fixture_errors.outputs.has_errors == 'true' - # run: | - # echo "::error::One or more component fixtures failed to render. See the 'Check fixture errors' step for details." - # exit 1 + - name: Fail if fixtures had errors + if: always() && steps.fixture_errors.outputs.has_errors == 'true' + run: | + echo "::error::One or more component fixtures failed to render. See the 'Check fixture errors' step for details." + exit 1 # - name: Prepare explorer artifact From 6467d8382aa4725c0abfc37f805ddae3ce1b1f48 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 7 May 2026 11:44:25 +0200 Subject: [PATCH 27/28] Adds missing getVendors mock --- .../test/browser/componentFixtures/chat/chatFixtureUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts index cec1f50ae4bb75..5e29f3a87949ea 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts @@ -176,7 +176,7 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I reg.defineInstance(IChatSessionsService, new class extends mock() { override getAllChatSessionContributions() { return []; } override readonly onDidChangeSessionOptions = Event.None; override readonly onDidChangeOptionGroups = Event.None; override readonly onDidChangeAvailability = Event.None; override getCustomAgentTargetForSessionType() { return Target.Undefined; } override requiresCustomModelsForSessionType() { return false; } override getOptionGroupsForSessionType() { return []; } }()); reg.defineInstance(IChatEntitlementService, new class extends mock() { }()); reg.defineInstance(IChatModeService, new MockChatModeService()); - reg.defineInstance(ILanguageModelsService, new class extends mock() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } }()); + reg.defineInstance(ILanguageModelsService, new class extends mock() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } override getVendors() { return []; } override hasResolvedVendor() { return false; } }()); reg.defineInstance(ILanguageModelToolsService, new class extends mock() { override onDidChangeTools = Event.None; override onDidPrepareToolCallBecomeUnresponsive = Event.None; override getTools() { return []; } }()); reg.defineInstance(IChatToolRiskAssessmentService, new class extends mock() { override isEnabled() { return false; } From 5bb794de87fa319f04e986f41a31fd8ebaa42d92 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Thu, 7 May 2026 13:02:04 +0100 Subject: [PATCH 28/28] Refine action list style and add detailItemHeight option (#314980) * Add detailItemHeight option for action list and adjust styles * Adjust padding for action list row elements * Add detailItemHeight option to permission picker action item list options --------- Co-authored-by: mrleemurray --- src/vs/platform/actionWidget/browser/actionList.ts | 8 +++++++- src/vs/platform/actionWidget/browser/actionWidget.css | 7 +++---- src/vs/sessions/contrib/changes/browser/changesView.ts | 2 +- .../browser/widget/input/permissionPickerActionItem.ts | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 9a3dfa6590812f..8c645030f8d6a7 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -483,6 +483,12 @@ export interface IActionListOptions { */ readonly inlineDescription?: boolean; + /** + * Height (in px) used for action items that have a `detail` line. + * Defaults to 48. + */ + readonly detailItemHeight?: number; + /** * When true, the group title is shown on the first item of each group * in the description area (aligned to the right). @@ -999,7 +1005,7 @@ export class ActionListWidget extends Disposable { case ActionListItemKind.Separator: return this._separatorLineHeight; default: - return item.detail ? 48 : this._actionLineHeight; + return item.detail ? (this._options?.detailItemHeight ?? 48) : this._actionLineHeight; } } diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 7b79a90b337004..8f49370d8497fb 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -55,7 +55,7 @@ /** Styles for each row in the list element **/ .action-widget .monaco-list .monaco-list-row { - padding: 0 12px 0 8px; + padding: 0 12px 0 6px; white-space: nowrap; cursor: pointer; touch-action: none; @@ -151,7 +151,7 @@ .action-widget .monaco-list-row.action .detail { order: 99; width: 100%; - padding-left: 20px; + padding-left: 18px; font-size: 11px; line-height: 14px; color: var(--vscode-descriptionForeground); @@ -242,8 +242,7 @@ &:has(.detail:not([style*="display: none"])) { flex-wrap: wrap; align-content: center; - padding-top: 6px; - padding-right: 2px; + padding-right: 6px; .title { line-height: 14px; diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 8b8254034daaab..35f78a8ec5e055 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -1352,7 +1352,7 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem { }, }; - super(action, { actionProvider, listOptions: {} }, actionWidgetService, keybindingService, contextKeyService, telemetryService); + super(action, { actionProvider, listOptions: { detailItemHeight: 44 } }, actionWidgetService, keybindingService, contextKeyService, telemetryService); this._register(autorun(reader => { viewModel.versionModeObs.read(reader); 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 9a7c758271ecdf..07baa0272047b1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -190,7 +190,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { } }], reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, - listOptions: { minWidth: 255 }, + listOptions: { minWidth: 255, detailItemHeight: 44 }, }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); }