From 1a13b9ca22c82b81b9da95a120a33b7abeab17cc Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 13 Feb 2026 15:22:53 +0100 Subject: [PATCH 01/21] Add additional markdown extensions This marks the `.litcoffee` file extension as markdown. This is a markdown format, where indented code blocks can be interpreted as CoffeeScript. This also adds the `.mkdn` and `.ron` extensions, which are taken from https://github.com/sindresorhus/markdown-extensions/blob/main/index.js. --- extensions/markdown-basics/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index c77aad6a30149..b24c8594cc26e 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -18,14 +18,17 @@ "markdown" ], "extensions": [ + ".litcoffee", ".md", ".mkd", + ".mkdn", ".mdwn", ".mdown", ".markdown", ".markdn", ".mdtxt", ".mdtext", + ".ron", ".workbook" ], "filenamePatterns": [ From 278032fc752ce77b6cf3a47bd79ce49159d50445 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 4 Mar 2026 01:32:36 +0900 Subject: [PATCH 02/21] feat: support heap profile and snapshot capture for tsserver --- .../typescript-language-features/package.json | 52 +++++++++++++++++++ .../package.nls.json | 6 +++ .../src/configuration/configuration.ts | 48 +++++++++++++++++ .../src/tsServer/serverProcess.electron.ts | 18 +++++++ 4 files changed, 124 insertions(+) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index ed85772ade5fd..412986886aab4 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -2556,6 +2556,16 @@ "TypeScript" ] }, + "js/ts.tsserver.diagnosticDir": { + "type": "string", + "markdownDescription": "%configuration.tsserver.diagnosticDir%", + "scope": "window", + "keywords": [ + "TypeScript", + "diagnostic", + "memory" + ] + }, "typescript.tsserver.maxTsServerMemory": { "type": "number", "default": 3072, @@ -2563,6 +2573,48 @@ "markdownDeprecationMessage": "%configuration.tsserver.maxTsServerMemory.unifiedDeprecationMessage%", "scope": "window" }, + "js/ts.tsserver.heapSnapshot": { + "type": "number", + "default": 0, + "minimum": 0, + "markdownDescription": "%configuration.tsserver.heapSnapshot%", + "scope": "window", + "keywords": [ + "TypeScript", + "memory", + "diagnostics" + ] + }, + "js/ts.tsserver.heapProfile": { + "type": "object", + "default": { + "enabled": false + }, + "markdownDescription": "%configuration.tsserver.heapProfile%", + "scope": "window", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.heapProfile.enabled%" + }, + "dir": { + "type": "string", + "description": "%configuration.tsserver.heapProfile.dir%" + }, + "interval": { + "type": "number", + "minimum": 1, + "description": "%configuration.tsserver.heapProfile.interval%" + } + }, + "keywords": [ + "TypeScript", + "memory", + "heap", + "profile" + ] + }, "js/ts.tsserver.watchOptions": { "description": "%configuration.tsserver.watchOptions%", "scope": "window", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 8c28dd87ccdf2..40c4081de540b 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -126,6 +126,12 @@ "configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#js/ts.tsserver.node.path#` to run TS Server with a custom Node installation.", "configuration.tsserver.maxTsServerMemory.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.tsserver.maxMemory#` instead.", "configuration.tsserver.maxMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#js/ts.tsserver.node.path#` to run TS Server with a custom Node installation.", + "configuration.tsserver.diagnosticDir": "Directory where TypeScript server writes Node diagnostic output by passing `--diagnostic-dir`.", + "configuration.tsserver.heapSnapshot": "Controls how many near-heap-limit snapshots TypeScript server writes by passing `--heapsnapshot-near-heap-limit`. Set to `0` to disable.", + "configuration.tsserver.heapProfile": "Configures heap profiling for TypeScript server.", + "configuration.tsserver.heapProfile.enabled": "Enable heap profiling for TypeScript server by passing `--heap-prof`.", + "configuration.tsserver.heapProfile.dir": "Directory where TypeScript server writes heap profiles by passing `--heap-prof-dir`.", + "configuration.tsserver.heapProfile.interval": "Sampling interval in bytes for TypeScript server heap profiling by passing `--heap-prof-interval`.", "configuration.tsserver.experimental.enableProjectDiagnostics": "Enables project wide error reporting.", "configuration.tsserver.experimental.enableProjectDiagnostics.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.tsserver.experimental.enableProjectDiagnostics#` instead.", "typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Defaults to use VS Code's locale.", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index a557f08c0246b..ae43fda659eda 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -110,6 +110,12 @@ export class ImplicitProjectConfiguration { } } +export interface TsServerHeapProfileConfiguration { + readonly enabled: boolean; + readonly dir: string | undefined; + readonly interval: number | undefined; +} + export interface TypeScriptServiceConfiguration { readonly locale: string | null; readonly globalTsdk: string | null; @@ -126,6 +132,9 @@ export interface TypeScriptServiceConfiguration { readonly enableDiagnosticsTelemetry: boolean; readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; + readonly diagnosticDir: string | undefined; + readonly heapSnapshot: number; + readonly heapProfile: TsServerHeapProfileConfiguration; readonly enablePromptUseWorkspaceTsdk: boolean; readonly useVsCodeWatcher: boolean; readonly watchOptions: Proto.WatchOptions | undefined; @@ -168,6 +177,9 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu enableDiagnosticsTelemetry: this.readEnableDiagnosticsTelemetry(), enableProjectDiagnostics: this.readEnableProjectDiagnostics(), maxTsServerMemory: this.readMaxTsServerMemory(), + diagnosticDir: this.readDiagnosticDir(), + heapSnapshot: this.readHeapSnapshot(), + heapProfile: this.readHeapProfileConfiguration(), enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(), useVsCodeWatcher: this.readUseVsCodeWatcher(configuration), watchOptions: this.readWatchOptions(), @@ -288,6 +300,42 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return Math.max(memoryInMB, minimumMaxMemory); } + protected readDiagnosticDir(): string | undefined { + const diagnosticDir = readUnifiedConfig('tsserver.diagnosticDir', undefined, { fallbackSection: 'typescript' }); + return typeof diagnosticDir === 'string' && diagnosticDir.length > 0 ? diagnosticDir : undefined; + } + + protected readHeapSnapshot(): number { + const defaultNearHeapLimitSnapshotCount = 0; + const nearHeapLimitSnapshotCount = readUnifiedConfig('tsserver.heapSnapshot', defaultNearHeapLimitSnapshotCount, { fallbackSection: 'typescript' }); + if (!Number.isSafeInteger(nearHeapLimitSnapshotCount)) { + return defaultNearHeapLimitSnapshotCount; + } + return Math.max(nearHeapLimitSnapshotCount, 0); + } + + private readHeapProfileConfiguration(): TsServerHeapProfileConfiguration { + const defaultHeapProfileConfiguration: TsServerHeapProfileConfiguration = { + enabled: false, + dir: undefined, + interval: undefined, + }; + + const rawConfig = readUnifiedConfig<{ enabled?: unknown; dir?: unknown; interval?: unknown }>('tsserver.heapProfile', defaultHeapProfileConfiguration, { fallbackSection: 'typescript' }); + + const enabled = typeof rawConfig.enabled === 'boolean' ? rawConfig.enabled : false; + const dir = typeof rawConfig.dir === 'string' && rawConfig.dir.length > 0 ? rawConfig.dir : undefined; + const interval = typeof rawConfig.interval === 'number' && Number.isSafeInteger(rawConfig.interval) && rawConfig.interval > 0 + ? rawConfig.interval + : undefined; + + return { + enabled, + dir, + interval, + }; + } + protected readEnablePromptUseWorkspaceTsdk(): boolean { return readUnifiedConfig('tsdk.promptToUseWorkspaceVersion', false, { fallbackSection: 'typescript', fallbackSubSectionNameOverride: 'enablePromptUseWorkspaceTsdk' }); } diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index 7dbde90f7924c..992cae925dff7 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -162,6 +162,24 @@ function getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptService args.push(`--max-old-space-size=${configuration.maxTsServerMemory}`); } + if (configuration.diagnosticDir) { + args.push(`--diagnostic-dir=${configuration.diagnosticDir}`); + } + + if (configuration.heapSnapshot > 0) { + args.push(`--heapsnapshot-near-heap-limit=${configuration.heapSnapshot}`); + } + + if (configuration.heapProfile.enabled) { + args.push('--heap-prof'); + if (configuration.heapProfile.dir) { + args.push(`--heap-prof-dir=${configuration.heapProfile.dir}`); + } + if (configuration.heapProfile.interval) { + args.push(`--heap-prof-interval=${configuration.heapProfile.interval}`); + } + } + return args; } From 92de3b63d53e7574c9a7f7a0a85635b8bbdf6e34 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 4 Mar 2026 01:59:57 +0900 Subject: [PATCH 03/21] chore: apply feedback --- extensions/typescript-language-features/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 412986886aab4..fb58ac53c25f3 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -30,7 +30,9 @@ "typescript.npm", "js/ts.tsserver.npm.path", "typescript.tsserver.nodePath", - "js/ts.tsserver.node.path" + "js/ts.tsserver.node.path", + "js/ts.tsserver.diagnosticDir", + "js/ts.tsserver.heapProfile" ] } }, @@ -2559,7 +2561,7 @@ "js/ts.tsserver.diagnosticDir": { "type": "string", "markdownDescription": "%configuration.tsserver.diagnosticDir%", - "scope": "window", + "scope": "machine", "keywords": [ "TypeScript", "diagnostic", @@ -2591,7 +2593,7 @@ "enabled": false }, "markdownDescription": "%configuration.tsserver.heapProfile%", - "scope": "window", + "scope": "machine", "properties": { "enabled": { "type": "boolean", From f6da4b4e1a07823b5183b4e25416cceced47d99b Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Wed, 4 Mar 2026 13:08:01 +0900 Subject: [PATCH 04/21] fix: add graceful shutdown path when heapprofile is enabled --- .../src/tsServer/serverProcess.electron.ts | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index 992cae925dff7..a356fd7817bd4 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -24,6 +24,12 @@ const contentLengthSize: number = Buffer.byteLength(contentLength, 'utf8'); const blank: number = Buffer.from(' ', 'utf8')[0]; const backslashR: number = Buffer.from('\r', 'utf8')[0]; const backslashN: number = Buffer.from('\n', 'utf8')[0]; +const gracefulExitTimeout = 5000; +const tsServerExitRequest: Proto.Request = { + seq: 0, + type: 'request', + command: 'exit', +}; class ProtocolBuffer { @@ -207,10 +213,15 @@ function getTssDebugBrk(): string | undefined { } class IpcChildServerProcess extends Disposable implements TsServerProcess { + private _killTimeout: NodeJS.Timeout | undefined; + private _isShuttingDown = false; + constructor( private readonly _process: child_process.ChildProcess, + private readonly _useGracefulShutdown: boolean, ) { super(); + this._process.once('exit', () => this.clearKillTimeout()); } write(serverRequest: Proto.Request): void { @@ -230,18 +241,47 @@ class IpcChildServerProcess extends Disposable implements TsServerProcess { } kill(): void { - this._process.kill(); + if (!this._useGracefulShutdown) { + this._process.kill(); + return; + } + + if (this._isShuttingDown) { + return; + } + this._isShuttingDown = true; + + try { + this._process.send(tsServerExitRequest); + } catch { + this._process.kill(); + return; + } + + this._killTimeout = setTimeout(() => this._process.kill(), gracefulExitTimeout); + this._killTimeout.unref?.(); + } + + private clearKillTimeout(): void { + if (this._killTimeout) { + clearTimeout(this._killTimeout); + this._killTimeout = undefined; + } } } class StdioChildServerProcess extends Disposable implements TsServerProcess { private readonly _reader: Reader; + private _killTimeout: NodeJS.Timeout | undefined; + private _isShuttingDown = false; constructor( private readonly _process: child_process.ChildProcess, + private readonly _useGracefulShutdown: boolean, ) { super(); this._reader = this._register(new Reader(this._process.stdout!)); + this._process.once('exit', () => this.clearKillTimeout()); } write(serverRequest: Proto.Request): void { @@ -262,7 +302,39 @@ class StdioChildServerProcess extends Disposable implements TsServerProcess { } kill(): void { - this._process.kill(); + if (!this._useGracefulShutdown) { + this._process.kill(); + this._reader.dispose(); + return; + } + + if (this._isShuttingDown) { + return; + } + this._isShuttingDown = true; + + try { + this._process.stdin?.write(JSON.stringify(tsServerExitRequest) + '\r\n', 'utf8'); + this._process.stdin?.end(); + } catch { + this._process.kill(); + this._reader.dispose(); + return; + } + + this._killTimeout = setTimeout(() => { + this._process.kill(); + this._reader.dispose(); + }, gracefulExitTimeout); + this._killTimeout.unref?.(); + } + + private clearKillTimeout(): void { + if (this._killTimeout) { + clearTimeout(this._killTimeout); + this._killTimeout = undefined; + } + this._reader.dispose(); } } @@ -290,6 +362,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { const env = generatePatchedEnv(process.env, tsServerPath, !!execPath); const runtimeArgs = [...args]; const execArgv = getExecArgv(kind, configuration); + const useGracefulShutdown = configuration.heapProfile.enabled; const useIpc = !execPath && version.apiVersion?.gte(API.v460); if (useIpc) { runtimeArgs.push('--useNodeIpc'); @@ -309,6 +382,6 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { stdio: useIpc ? ['pipe', 'pipe', 'pipe', 'ipc'] : undefined, }); - return useIpc ? new IpcChildServerProcess(childProcess) : new StdioChildServerProcess(childProcess); + return useIpc ? new IpcChildServerProcess(childProcess, useGracefulShutdown) : new StdioChildServerProcess(childProcess, useGracefulShutdown); } } From 87cec2bf5b9c309aba8f3ac0882adfb2adb10d4b Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Wed, 4 Mar 2026 21:37:19 +0100 Subject: [PATCH 05/21] Add `.ronn` extension to markdown --- extensions/markdown-basics/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index b24c8594cc26e..434c7e7c6678a 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -29,6 +29,7 @@ ".mdtxt", ".mdtext", ".ron", + ".ronn", ".workbook" ], "filenamePatterns": [ From 8d491919b2cdac2fe69fd9ec9b62bc3593641d45 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 23:26:04 +0100 Subject: [PATCH 06/21] don't steal focus for readonly editors --- .../agentFeedbackEditorInputContribution.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts index 707a1090912f3..dc2434cfb45ad 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts @@ -342,15 +342,29 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return; } + // Don't capture Escape at this level - let it fall through to the input handler if focused + if (e.keyCode === KeyCode.Escape) { + this._hide(); + this._editor.focus(); + return; + } + + // Ctrl+I / Cmd+I explicitly focuses the feedback input + if ((e.ctrlKey || e.metaKey) && e.keyCode === KeyCode.KeyI) { + e.preventDefault(); + e.stopPropagation(); + widget.inputElement.focus(); + return; + } + // Don't focus if any modifier is held (keyboard shortcuts) if (e.ctrlKey || e.altKey || e.metaKey) { return; } - // Don't capture Escape at this level - let it fall through to the input handler if focused - if (e.keyCode === KeyCode.Escape) { - this._hide(); - this._editor.focus(); + // Only auto-focus the input on typing when the document is readonly; + // when editable the user must click or use Ctrl+I to focus. + if (!this._editor.getOption(EditorOption.readOnly)) { return; } @@ -413,6 +427,12 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements })); } + focusInput(): void { + if (this._visible && this._widget) { + this._widget.inputElement.focus(); + } + } + private _addFeedback(): boolean { if (!this._widget) { return false; From 0cb758d145d60ee942c342bdad9756d746143985 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 23:27:32 +0100 Subject: [PATCH 07/21] maximize editor group on double click --- .../contrib/configuration/browser/configuration.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index e94d1317988a8..44fb59656b333 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -40,6 +40,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'terminal.integrated.initialHint': false, + 'workbench.editor.doubleClickTabToToggleEditorGroupSizes': 'maximize', 'workbench.editor.restoreEditors': false, 'workbench.startupEditor': 'none', 'workbench.tips.enabled': false, From f48d290224c936a6aefa06bfb22ca1b9574d112a Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 4 Mar 2026 23:37:42 +0100 Subject: [PATCH 08/21] better sessions terminal tracking --- .../browser/sessionsTerminalContribution.ts | 108 +++++++++++++++++- .../sessionsTerminalContribution.test.ts | 76 +++++++++++- 2 files changed, 176 insertions(+), 8 deletions(-) diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index ff375376d0057..290fa8b309c38 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { isEqualOrParent } from '../../../../base/common/extpath.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; @@ -14,7 +15,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkbenchContribution, getWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalInstance, ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { Menus } from '../../../browser/menus.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; @@ -83,16 +84,17 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben } })); - // When terminals are restored on startup, ensure visibility matches active session + // When terminals are created externally, try to relate them to the active session this._register(this._terminalService.onDidCreateInstance(instance => { if (this._isCreatingTerminal || this._activeKey === undefined) { return; } - // If this instance is not tracked by us, hide it + // If this instance is already tracked by us, nothing to do const activeIds = this._pathToInstanceIds.get(this._activeKey); - if (!activeIds?.has(instance.instanceId)) { - this._terminalService.moveToBackground(instance); + if (activeIds?.has(instance.instanceId)) { + return; } + this._tryAdoptTerminal(instance); })); } @@ -160,6 +162,58 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben ids.add(instanceId); } + /** + * Attempts to associate an externally-created terminal with the active + * session by checking whether its initial cwd falls within the active + * session's worktree or repository. Hides the terminal if it cannot be + * related. + */ + private async _tryAdoptTerminal(instance: ITerminalInstance): Promise { + let cwd: string | undefined; + try { + cwd = await instance.getInitialCwd(); + } catch { + return; + } + + if (instance.isDisposed) { + return; + } + + const activeKey = this._activeKey; + if (!activeKey) { + return; + } + + // Re-check tracking — the terminal may have been adopted while awaiting + const activeIds = this._pathToInstanceIds.get(activeKey); + if (activeIds?.has(instance.instanceId)) { + return; + } + + const session = this._sessionsManagementService.activeSession.get(); + if (cwd && this._isRelatedToSession(cwd, session, activeKey)) { + this._addInstanceToPath(activeKey, instance.instanceId); + this._logService.trace(`[SessionsTerminal] Adopted terminal ${instance.instanceId} with cwd ${cwd}`); + } else { + this._terminalService.moveToBackground(instance); + } + } + + /** + * Returns whether the given cwd falls within the active session's + * worktree, repository, or the current active key (home dir fallback). + */ + private _isRelatedToSession(cwd: string, session: IActiveSessionItem | undefined, activeKey: string): boolean { + if (isEqualOrParent(cwd, activeKey, true)) { + return true; + } + if (session?.providerType === AgentSessionProviders.Background && session.repository) { + return isEqualOrParent(cwd, session.repository.fsPath, true); + } + return false; + } + /** * Hides all foreground terminals that do not belong to the given active key * and shows all background terminals that do belong to it. @@ -199,6 +253,32 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben this._pathToInstanceIds.delete(key); } } + + async dumpTracking(): Promise { + const trackedInstanceIds = new Set(); + + console.log('[SessionsTerminal] === Tracked Terminals ==='); + for (const [key, ids] of this._pathToInstanceIds) { + for (const instanceId of ids) { + trackedInstanceIds.add(instanceId); + const instance = this._terminalService.getInstanceFromId(instanceId); + let cwd = ''; + if (instance) { + try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } + } + console.log(` ${instanceId} - ${cwd} - ${key}`); + } + } + + console.log('[SessionsTerminal] === Untracked Terminals ==='); + for (const instance of this._terminalService.instances) { + if (!trackedInstanceIds.has(instance.instanceId)) { + let cwd = ''; + try { cwd = await instance.getInitialCwd(); } catch { /* ignored */ } + console.log(` ${instance.instanceId} - ${cwd}`); + } + } + } } registerWorkbenchContribution2(SessionsTerminalContribution.ID, SessionsTerminalContribution, WorkbenchPhase.AfterRestored); @@ -231,3 +311,21 @@ class OpenSessionInTerminalAction extends Action2 { } registerAction2(OpenSessionInTerminalAction); + +class DumpTerminalTrackingAction extends Action2 { + + constructor() { + super({ + id: 'agentSession.dumpTerminalTracking', + title: localize2('dumpTerminalTracking', "Dump Terminal Tracking"), + f1: true, + }); + } + + override async run(): Promise { + const contribution = getWorkbenchContribution(SessionsTerminalContribution.ID); + await contribution.dumpTracking(); + } +} + +registerAction2(DumpTerminalTrackingAction); diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index c2108711f8013..7305d4591b558 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -51,6 +51,14 @@ function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerT } as IActiveSessionItem; } +function makeTerminalInstance(id: number, cwd: string): ITerminalInstance { + return { + instanceId: id, + isDisposed: false, + getInitialCwd: () => Promise.resolve(cwd), + } as unknown as ITerminalInstance; +} + suite('SessionsTerminalContribution', () => { const store = new DisposableStore(); @@ -102,7 +110,9 @@ suite('SessionsTerminalContribution', () => { } override async createTerminal(opts?: any): Promise { const id = nextInstanceId++; - const instance = { instanceId: id } as ITerminalInstance; + const cwdUri: URI | undefined = opts?.config?.cwd; + const cwdStr = cwdUri?.fsPath ?? ''; + const instance = makeTerminalInstance(id, cwdStr); createdTerminals.push({ cwd: opts?.config?.cwd }); terminalInstances.set(id, instance); onDidCreateInstance.fire(instance); @@ -436,9 +446,10 @@ suite('SessionsTerminalContribution', () => { await tick(); // Simulate a terminal being restored (e.g. on startup) that is not tracked - const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + const restoredInstance = makeTerminalInstance(nextInstanceId++, '/some/other/path'); terminalInstances.set(restoredInstance.instanceId, restoredInstance); onDidCreateInstance.fire(restoredInstance); + await tick(); // The restored terminal should be moved to background assert.ok(moveToBackgroundCalls.includes(restoredInstance.instanceId), 'restored terminal should be backgrounded'); @@ -446,9 +457,10 @@ suite('SessionsTerminalContribution', () => { test('does not hide restored terminals before any session is active', async () => { // Simulate a terminal being restored before any session is active - const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + const restoredInstance = makeTerminalInstance(nextInstanceId++, '/some/path'); terminalInstances.set(restoredInstance.instanceId, restoredInstance); onDidCreateInstance.fire(restoredInstance); + await tick(); assert.strictEqual(moveToBackgroundCalls.length, 0, 'should not background before any session is active'); }); @@ -467,6 +479,64 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals.length, 1, 'should not create a new terminal'); assert.ok(showBackgroundCalls.includes(instanceId), 'should show the backgrounded terminal'); }); + + // --- Terminal adoption --- + + test('adopts externally-created terminal whose cwd matches the active worktree', async () => { + const worktree = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, worktree.fsPath); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'should not be hidden'); + // Verify it was adopted — ensureTerminal should reuse it + await contribution.ensureTerminal(worktree, false); + assert.strictEqual(createdTerminals.length, 1, 'should reuse adopted terminal, not create a second'); + }); + + test('adopts externally-created terminal whose cwd is a subdirectory of the active worktree', async () => { + const worktree = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, URI.file('/worktree/subdir').fsPath); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'subdirectory terminal should not be hidden'); + }); + + test('adopts externally-created terminal whose cwd matches the session repository', async () => { + const worktree = URI.file('/worktree'); + const repo = URI.file('/repo'); + activeSessionObs.set(makeAgentSession({ worktree, repository: repo, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, repo.fsPath); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(!moveToBackgroundCalls.includes(externalInstance.instanceId), 'terminal at repository path should not be hidden'); + }); + + test('hides externally-created terminal whose cwd does not match the active session', async () => { + const worktree = URI.file('/worktree'); + activeSessionObs.set(makeAgentSession({ worktree, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + const externalInstance = makeTerminalInstance(nextInstanceId++, '/unrelated/path'); + terminalInstances.set(externalInstance.instanceId, externalInstance); + onDidCreateInstance.fire(externalInstance); + await tick(); + + assert.ok(moveToBackgroundCalls.includes(externalInstance.instanceId), 'unrelated terminal should be hidden'); + }); }); function tick(): Promise { From d356c797a720444d88f27c9d37beb6f59e6eb57e Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:08:23 -0800 Subject: [PATCH 09/21] Browser: make Playwright per workspace (#299055) * Browser: make Playwright per workspace * Feedback, fixes --- .../sharedProcess/sharedProcessMain.ts | 10 +-- .../browserView/common/browserViewGroup.ts | 3 +- .../platform/browserView/common/cdp/types.ts | 2 +- .../electron-main/browserViewGroup.ts | 5 +- .../browserViewGroupMainService.ts | 4 +- .../electron-main/browserViewMainService.ts | 10 ++- .../node/browserViewGroupRemoteService.ts | 16 ++-- .../browserView/node/playwrightChannel.ts | 82 +++++++++++++++++++ .../browserView/node/playwrightService.ts | 7 +- .../playwrightWorkbenchService.ts | 21 ++++- 10 files changed, 129 insertions(+), 31 deletions(-) create mode 100644 src/vs/platform/browserView/node/playwrightChannel.ts diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index e112b958d730e..7f5cd7c26e9b4 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -134,9 +134,7 @@ import { IMcpGalleryManifestService } from '../../../platform/mcp/common/mcpGall import { McpGalleryManifestIPCService } from '../../../platform/mcp/common/mcpGalleryManifestServiceIpc.js'; import { IMeteredConnectionService } from '../../../platform/meteredConnection/common/meteredConnection.js'; import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js'; -import { IPlaywrightService } from '../../../platform/browserView/common/playwrightService.js'; -import { PlaywrightService } from '../../../platform/browserView/node/playwrightService.js'; -import { IBrowserViewGroupRemoteService, BrowserViewGroupRemoteService } from '../../../platform/browserView/node/browserViewGroupRemoteService.js'; +import { PlaywrightChannel } from '../../../platform/browserView/node/playwrightChannel.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -404,10 +402,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { // Web Content Extractor services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService)); - // Playwright - services.set(IBrowserViewGroupRemoteService, new SyncDescriptor(BrowserViewGroupRemoteService)); - services.set(IPlaywrightService, new SyncDescriptor(PlaywrightService)); - return new InstantiationService(services); } @@ -476,7 +470,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { this.server.registerChannel('sharedWebContentExtractor', webContentExtractorChannel); // Playwright - const playwrightChannel = ProxyChannel.fromService(accessor.get(IPlaywrightService), this._store); + const playwrightChannel = this._register(new PlaywrightChannel(this.server, accessor.get(IMainProcessService), accessor.get(ILogService))); this.server.registerChannel('playwright', playwrightChannel); } diff --git a/src/vs/platform/browserView/common/browserViewGroup.ts b/src/vs/platform/browserView/common/browserViewGroup.ts index 0f43b98c8b080..0c414d9df7b58 100644 --- a/src/vs/platform/browserView/common/browserViewGroup.ts +++ b/src/vs/platform/browserView/common/browserViewGroup.ts @@ -51,9 +51,10 @@ export interface IBrowserViewGroupService { /** * Create a new browser view group. + * @param windowId The ID of the primary window the group should be associated with. * @returns The id of the newly created group. */ - createGroup(): Promise; + createGroup(windowId: number): Promise; /** * Destroy a browser view group. diff --git a/src/vs/platform/browserView/common/cdp/types.ts b/src/vs/platform/browserView/common/cdp/types.ts index 603467e3ed2df..ca0256478c82e 100644 --- a/src/vs/platform/browserView/common/cdp/types.ts +++ b/src/vs/platform/browserView/common/cdp/types.ts @@ -151,7 +151,7 @@ export interface ICDPBrowserTarget extends ICDPTarget { /** Get all available targets */ getTargets(): IterableIterator; /** Create a new target in the specified browser context */ - createTarget(url: string, browserContextId?: string): Promise; + createTarget(url: string, browserContextId?: string, windowId?: number): Promise; /** Activate a target (bring to foreground) */ activateTarget(target: ICDPTarget): Promise; /** Close a target */ diff --git a/src/vs/platform/browserView/electron-main/browserViewGroup.ts b/src/vs/platform/browserView/electron-main/browserViewGroup.ts index d7d59c2701889..d68ba74efedf9 100644 --- a/src/vs/platform/browserView/electron-main/browserViewGroup.ts +++ b/src/vs/platform/browserView/electron-main/browserViewGroup.ts @@ -49,6 +49,7 @@ export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, I constructor( readonly id: string, + private readonly windowId: number, @IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService, @IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer, ) { @@ -127,12 +128,12 @@ export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, I return this.views.values(); } - async createTarget(url: string, browserContextId?: string): Promise { + async createTarget(url: string, browserContextId?: string, windowId = this.windowId): Promise { if (browserContextId && !this.knownContextIds.has(browserContextId)) { throw new Error(`Unknown browser context ${browserContextId}`); } - const target = await this.browserViewMainService.createTarget(url, browserContextId); + const target = await this.browserViewMainService.createTarget(url, browserContextId, windowId); if (target instanceof BrowserView) { await this.addView(target.id); } diff --git a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts index 20dd6331c0ea5..4cc7aadfe97c4 100644 --- a/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewGroupMainService.ts @@ -33,9 +33,9 @@ export class BrowserViewGroupMainService extends Disposable implements IBrowserV super(); } - async createGroup(): Promise { + async createGroup(windowId: number): Promise { const id = generateUuid(); - const group = this.instantiationService.createInstance(BrowserViewGroup, id); + const group = this.instantiationService.createInstance(BrowserViewGroup, id, windowId); this.groups.set(id, group); // Auto-cleanup when the group disposes itself diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index 3959717fb1d7b..c2a4e3aeefe64 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -19,6 +19,7 @@ import { IProductService } from '../../product/common/productService.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; import { logBrowserOpen } from '../common/browserViewTelemetry.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; export const IBrowserViewMainService = createDecorator('browserViewMainService'); @@ -158,7 +159,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this.browserViews.values(); } - async createTarget(url: string, browserContextId?: string): Promise { + async createTarget(url: string, browserContextId?: string, windowId?: number): Promise { const targetId = generateUuid(); const browserSession = browserContextId && BrowserSession.get(browserContextId) || BrowserSession.getOrCreateEphemeral(targetId); @@ -167,8 +168,13 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa logBrowserOpen(this.telemetryService, 'cdpCreated'); + const window = windowId !== undefined ? this.windowsMainService.getWindowById(windowId) : this.windowsMainService.getFocusedWindow(); + if (!window) { + throw new Error(`Window ${windowId} not found`); + } + // Request the workbench to open the editor - this.windowsMainService.sendToFocused('vscode:runAction', { + window.sendWhenReady('vscode:runAction', CancellationToken.None, { id: '_workbench.open', args: [BrowserViewUri.forUrl(url, targetId), [undefined, { preserveFocus: true }], undefined] }); diff --git a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts index b4aaffb612d17..3035a6828df3f 100644 --- a/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts +++ b/src/vs/platform/browserView/node/browserViewGroupRemoteService.ts @@ -6,12 +6,9 @@ import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { IBrowserViewGroup, IBrowserViewGroupService, IBrowserViewGroupViewEvent, ipcBrowserViewGroupChannelName } from '../common/browserViewGroup.js'; -export const IBrowserViewGroupRemoteService = createDecorator('browserViewGroupRemoteService'); - /** * Remote-process service for managing browser view groups. * @@ -22,12 +19,11 @@ export const IBrowserViewGroupRemoteService = createDecorator; + createGroup(windowId: number): Promise; } /** @@ -79,20 +75,18 @@ class RemoteBrowserViewGroup extends Disposable implements IBrowserViewGroup { } export class BrowserViewGroupRemoteService implements IBrowserViewGroupRemoteService { - declare readonly _serviceBrand: undefined; - private readonly _groupService: IBrowserViewGroupService; private readonly _groups = new Map(); constructor( - @IMainProcessService mainProcessService: IMainProcessService, + mainProcessService: IMainProcessService, ) { const channel = mainProcessService.getChannel(ipcBrowserViewGroupChannelName); this._groupService = ProxyChannel.toService(channel); } - async createGroup(): Promise { - const id = await this._groupService.createGroup(); + async createGroup(windowId: number): Promise { + const id = await this._groupService.createGroup(windowId); return this._wrap(id); } diff --git a/src/vs/platform/browserView/node/playwrightChannel.ts b/src/vs/platform/browserView/node/playwrightChannel.ts new file mode 100644 index 0000000000000..ca3e83c00a2e5 --- /dev/null +++ b/src/vs/platform/browserView/node/playwrightChannel.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../base/common/event.js'; +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { IPCServer, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { ILogService } from '../../log/common/log.js'; +import { BrowserViewGroupRemoteService } from './browserViewGroupRemoteService.js'; +import { PlaywrightService } from './playwrightService.js'; + +/** + * IPC channel for the Playwright service. + * + * Each connected window gets its own {@link PlaywrightService}, + * keyed by the opaque IPC connection context. The client sends an + * `__initialize` call with its numeric window ID before any other + * method calls, which eagerly creates the instance. When a window + * disconnects the instance is automatically disposed. + */ +export class PlaywrightChannel extends Disposable implements IServerChannel { + + private readonly _instances = this._register(new DisposableMap()); + private readonly browserViewGroupRemoteService: BrowserViewGroupRemoteService; + + constructor( + ipcServer: IPCServer, + mainProcessService: IMainProcessService, + private readonly logService: ILogService, + ) { + super(); + this.browserViewGroupRemoteService = new BrowserViewGroupRemoteService(mainProcessService); + this._register(ipcServer.onDidRemoveConnection(c => { + this._instances.deleteAndDispose(c.ctx); + })); + } + + listen(ctx: string, event: string): Event { + const instance = this._instances.get(ctx); + if (!instance) { + throw new Error(`Window not initialized for context: ${ctx}`); + } + const source = (instance as unknown as Record>)[event]; + if (typeof source !== 'function') { + throw new Error(`Event not found: ${event}`); + } + return source as Event; + } + + call(ctx: string, command: string, arg?: unknown): Promise { + // Handle the one-time initialization call that creates the instance + if (command === '__initialize') { + if (typeof arg !== 'number') { + throw new Error(`Invalid argument for __initialize: expected window ID as number, got ${typeof arg}`); + } + if (!this._instances.has(ctx)) { + const windowId = arg as number; + this._instances.set(ctx, new PlaywrightService(windowId, this.browserViewGroupRemoteService, this.logService)); + } + return Promise.resolve(undefined as T); + } + + const instance = this._instances.get(ctx); + if (!instance) { + throw new Error(`Window not initialized for context: ${ctx}`); + } + + const target = (instance as unknown as Record)[command]; + if (typeof target !== 'function') { + throw new Error(`Method not found: ${command}`); + } + + const methodArgs = Array.isArray(arg) ? arg : []; + let res = target.apply(instance, methodArgs); + if (!(res instanceof Promise)) { + res = Promise.resolve(res); + } + return res; + } +} diff --git a/src/vs/platform/browserView/node/playwrightService.ts b/src/vs/platform/browserView/node/playwrightService.ts index 0a55710ec61f2..9afb7963d6608 100644 --- a/src/vs/platform/browserView/node/playwrightService.ts +++ b/src/vs/platform/browserView/node/playwrightService.ts @@ -32,8 +32,9 @@ export class PlaywrightService extends Disposable implements IPlaywrightService private _initPromise: Promise | undefined; constructor( - @IBrowserViewGroupRemoteService private readonly browserViewGroupRemoteService: IBrowserViewGroupRemoteService, - @ILogService private readonly logService: ILogService, + private readonly windowId: number, + private readonly browserViewGroupRemoteService: IBrowserViewGroupRemoteService, + private readonly logService: ILogService, ) { super(); this._pages = this._register(new PlaywrightPageManager(logService)); @@ -76,7 +77,7 @@ export class PlaywrightService extends Disposable implements IPlaywrightService this._initPromise = (async () => { try { this.logService.debug('[PlaywrightService] Creating browser view group'); - const group = await this.browserViewGroupRemoteService.createGroup(); + const group = await this.browserViewGroupRemoteService.createGroup(this.windowId); this.logService.debug('[PlaywrightService] Connecting to browser via CDP'); const playwright = await import('playwright-core'); diff --git a/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts b/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts index f50672fd91398..12dcd6a2b038d 100644 --- a/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts +++ b/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts @@ -3,7 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { mainWindow } from '../../../../base/browser/window.js'; +import { IChannel, ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IPlaywrightService } from '../../../../platform/browserView/common/playwrightService.js'; import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; -registerSharedProcessRemoteService(IPlaywrightService, 'playwright'); +class PlaywrightChannelClient { + constructor( + channel: IChannel, + @ILogService logService: ILogService + ) { + /** + * send the current window's ID once via `__initialize`, so the server-side {@link PlaywrightChannel} + * can create a per-window {@link PlaywrightWindowInstance}. All subsequent calls and events are proxied directly. + */ + void channel.call('__initialize', mainWindow.vscodeWindowId).catch((e) => { + logService.error(`Failed to initialize Playwright service`, e); + }); + return ProxyChannel.toService(channel); + } +} + +registerSharedProcessRemoteService(IPlaywrightService, 'playwright', { channelClientCtor: PlaywrightChannelClient }); From e563f3c801ebf8b50a65fc52a6d7cd5305021c56 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 4 Mar 2026 18:22:53 -0500 Subject: [PATCH 10/21] test change (#299310) --- src/vs/workbench/contrib/chat/browser/chatTipService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 40a007908ddaf..593df2b765722 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -41,9 +41,7 @@ type ChatTipClassification = { }; // Re-export tracking commands for backwards compatibility -export { - TipTrackingCommands, -}; +export { TipTrackingCommands }; /** @deprecated Use TipTrackingCommands.AttachFilesReferenceUsed */ export const ATTACH_FILES_REFERENCE_TRACKING_COMMAND = TipTrackingCommands.AttachFilesReferenceUsed; /** @deprecated Use TipTrackingCommands.CreateAgentInstructionsUsed */ From 01fea9a3690f163847dedcb5979babce66fb6e2f Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:33:42 -0800 Subject: [PATCH 11/21] Enhance slash command expansion to include prompt type in message (#299305) * Enhance slash command expansion to include prompt type in message * Update src/vs/sessions/contrib/chat/browser/slashCommands.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/chat/browser/slashCommands.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/slashCommands.ts b/src/vs/sessions/contrib/chat/browser/slashCommands.ts index bda010579a626..4cf481f915e2f 100644 --- a/src/vs/sessions/contrib/chat/browser/slashCommands.ts +++ b/src/vs/sessions/contrib/chat/browser/slashCommands.ts @@ -23,6 +23,7 @@ import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../.. import { AICustomizationManagementCommands, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; /** * Static command ID used by completion items to trigger immediate slash command execution, @@ -128,7 +129,8 @@ export class SlashCommandHandler extends Disposable { const args = match[2]?.trim() ?? ''; const uri = promptCommand.promptPath.uri; - const expanded = `Use the prompt file located at [${promptCommand.name}](${uri.toString()}).`; + const typeLabel = promptCommand.promptPath.type === PromptsType.skill ? 'skill' : 'prompt file'; + const expanded = `Use the ${typeLabel} located at [${promptCommand.name}](${uri.toString()}).`; return args ? `${expanded} ${args}` : expanded; } From 5b2bd4495bc6618986cb894ab2f28b06395f4a63 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:16:59 -0800 Subject: [PATCH 12/21] Action widget: hover background for open pickers (#299301) * Action widget: full-width separators and hover background for open pickers - Remove horizontal padding from action widget, inset list rows instead so separators span edge-to-edge - Fire onDidChangeVisibility in ActionWidgetDropdown so aria-expanded is set correctly for all picker types - Apply toolbar hover background to picker buttons while their dropdown is open * Revert separator layout hack, keep hover background for open pickers --- .../platform/actionWidget/browser/actionWidgetDropdown.ts | 3 +++ .../contrib/chat/browser/widget/input/chatModelPicker.ts | 2 +- src/vs/workbench/contrib/chat/browser/widget/media/chat.css | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index 2b2022fb43572..82e6ff581bad9 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -170,6 +170,7 @@ export class ActionWidgetDropdown extends BaseDropdown { action.run(); }, onHide: () => { + this.hide(); if (isHTMLElement(previouslyFocusedElement)) { previouslyFocusedElement.focus(); } @@ -221,6 +222,8 @@ export class ActionWidgetDropdown extends BaseDropdown { getWidgetRole: () => 'menu', }; + super.show(); + this.actionWidgetService.show( this._options.label ?? '', false, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 2011b0f95e917..ce390754ad2ab 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -584,7 +584,7 @@ export class ModelPickerWidget extends Disposable { filterPlaceholder: localize('chat.modelPicker.search', "Search models"), focusFilterOnOpen: true, collapsedByDefault: new Set([ModelPickerSection.Other]), - minWidth: 300, + minWidth: 200, }; const previouslyFocusedElement = dom.getActiveElement(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index dd200620d82b9..e152c00e58a4c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1508,6 +1508,12 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-icon-foreground); } +/* Keep hover background while picker dropdown is open */ +.interactive-session .chat-input-toolbar .action-label[aria-expanded="true"], +.interactive-session .chat-secondary-toolbar .action-label[aria-expanded="true"] { + background-color: var(--vscode-toolbar-hoverBackground); +} + /* When chevrons are hidden and only showing an icon (no label), size to 22x22 with centered icon */ .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)), .interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:not(:has(.chat-input-picker-label)), From 0485c21d6bfd5cbb02bbafaf09da31eaf713ad01 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 16:17:38 -0800 Subject: [PATCH 13/21] plugins: refactor pluginSources with proper deletion (#299319) * plugins: refactor pluginSources with proper deletion - Consolidate source logic into IPluginSource implementations - Use that to implement more robust cleanup logic Closes https://github.com/microsoft/vscode/issues/297251 * pr comments --- .../browser/agentPluginRepositoryService.ts | 215 +++----- .../chat/browser/pluginInstallService.ts | 249 ++------- .../contrib/chat/browser/pluginSources.ts | 518 ++++++++++++++++++ .../plugins/agentPluginRepositoryService.ts | 20 +- .../common/plugins/agentPluginServiceImpl.ts | 14 +- .../chat/common/plugins/pluginSource.ts | 63 +++ .../agentPluginRepositoryService.test.ts | 189 +++++++ .../plugins/pluginInstallService.test.ts | 73 ++- 8 files changed, 975 insertions(+), 366 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/pluginSources.ts create mode 100644 src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts diff --git a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts index 41abb16b8f8a6..49a0f694d65e0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts @@ -6,12 +6,13 @@ import { Action } from '../../../../base/common/actions.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { revive } from '../../../../base/common/marshalling.js'; -import { dirname, isEqualOrParent, joinPath } from '../../../../base/common/resources.js'; +import { dirname, isEqual, isEqualOrParent, joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; @@ -19,6 +20,8 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import type { Dto } from '../../../services/extensions/common/proxyIdentifier.js'; import { IAgentPluginRepositoryService, IEnsureRepositoryOptions, IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js'; import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceReferenceKind, MarketplaceType, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IPluginSource } from '../common/plugins/pluginSource.js'; +import { GitHubPluginSource, GitUrlPluginSource, NpmPluginSource, PipPluginSource, RelativePathPluginSource } from './pluginSources.js'; const MARKETPLACE_INDEX_STORAGE_KEY = 'chat.plugins.marketplaces.index.v1'; @@ -34,17 +37,37 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi private readonly _cacheRoot: URI; private readonly _marketplaceIndex = new Lazy>(() => this._loadMarketplaceIndex()); + private readonly _pluginSources: ReadonlyMap; constructor( @ICommandService private readonly _commandService: ICommandService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService private readonly _fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @INotificationService private readonly _notificationService: INotificationService, @IProgressService private readonly _progressService: IProgressService, @IStorageService private readonly _storageService: IStorageService, ) { this._cacheRoot = joinPath(environmentService.cacheHome, 'agentPlugins'); + + // Build per-kind source repository map via instantiation service so + // each repository can inject its own dependencies. + this._pluginSources = new Map([ + [PluginSourceKind.RelativePath, new RelativePathPluginSource()], + [PluginSourceKind.GitHub, instantiationService.createInstance(GitHubPluginSource)], + [PluginSourceKind.GitUrl, instantiationService.createInstance(GitUrlPluginSource)], + [PluginSourceKind.Npm, instantiationService.createInstance(NpmPluginSource)], + [PluginSourceKind.Pip, instantiationService.createInstance(PipPluginSource)], + ]); + } + + getPluginSource(kind: PluginSourceKind): IPluginSource { + const repo = this._pluginSources.get(kind); + if (!repo) { + throw new Error(`No source repository registered for kind '${kind}'`); + } + return repo; } getRepositoryUri(marketplace: IMarketplaceReference, marketplaceType?: MarketplaceType): URI { @@ -214,176 +237,74 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi } getPluginSourceInstallUri(sourceDescriptor: IPluginSourceDescriptor): URI { - switch (sourceDescriptor.kind) { - case PluginSourceKind.RelativePath: - throw new Error('Use getPluginInstallUri() for relative-path sources'); - case PluginSourceKind.GitHub: { - const [owner, repo] = sourceDescriptor.repo.split('/'); - return joinPath(this._cacheRoot, 'github.com', owner, repo, ...this._getSourceRevisionCacheSuffix(sourceDescriptor)); - } - case PluginSourceKind.GitUrl: { - const segments = this._gitUrlCacheSegments(sourceDescriptor.url, sourceDescriptor.ref, sourceDescriptor.sha); - return joinPath(this._cacheRoot, ...segments); - } - case PluginSourceKind.Npm: - return joinPath(this._cacheRoot, 'npm', sanitizePackageName(sourceDescriptor.package), 'node_modules', sourceDescriptor.package); - case PluginSourceKind.Pip: - return joinPath(this._cacheRoot, 'pip', sanitizePackageName(sourceDescriptor.package)); - } + return this.getPluginSource(sourceDescriptor.kind).getInstallUri(this._cacheRoot, sourceDescriptor); } async ensurePluginSource(plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise { - const descriptor = plugin.sourceDescriptor; - switch (descriptor.kind) { - case PluginSourceKind.RelativePath: - return this.ensureRepository(plugin.marketplaceReference, options); - case PluginSourceKind.GitHub: { - const cloneUrl = `https://github.com/${descriptor.repo}.git`; - const repoDir = this.getPluginSourceInstallUri(descriptor); - const repoExists = await this._fileService.exists(repoDir); - if (repoExists) { - await this._checkoutPluginSourceRevision(repoDir, descriptor, options?.failureLabel ?? descriptor.repo); - return repoDir; - } - const progressTitle = options?.progressTitle ?? localize('cloningPluginSource', "Cloning plugin source '{0}'...", descriptor.repo); - const failureLabel = options?.failureLabel ?? descriptor.repo; - await this._cloneRepository(repoDir, cloneUrl, progressTitle, failureLabel, descriptor.ref); - await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); - return repoDir; - } - case PluginSourceKind.GitUrl: { - const repoDir = this.getPluginSourceInstallUri(descriptor); - const repoExists = await this._fileService.exists(repoDir); - if (repoExists) { - await this._checkoutPluginSourceRevision(repoDir, descriptor, options?.failureLabel ?? descriptor.url); - return repoDir; - } - const progressTitle = options?.progressTitle ?? localize('cloningPluginSourceUrl', "Cloning plugin source '{0}'...", descriptor.url); - const failureLabel = options?.failureLabel ?? descriptor.url; - await this._cloneRepository(repoDir, descriptor.url, progressTitle, failureLabel, descriptor.ref); - await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); - return repoDir; - } - case PluginSourceKind.Npm: { - // npm/pip install directories are managed by the install service. - // Return the expected install URI without performing installation. - return joinPath(this._cacheRoot, 'npm', sanitizePackageName(descriptor.package)); - } - case PluginSourceKind.Pip: { - return joinPath(this._cacheRoot, 'pip', sanitizePackageName(descriptor.package)); - } + const repo = this.getPluginSource(plugin.sourceDescriptor.kind); + if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { + return this.ensureRepository(plugin.marketplaceReference, options); } + return repo.ensure(this._cacheRoot, plugin, options); } async updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { - const descriptor = plugin.sourceDescriptor; - if (descriptor.kind !== PluginSourceKind.GitHub && descriptor.kind !== PluginSourceKind.GitUrl) { - return; + const repo = this.getPluginSource(plugin.sourceDescriptor.kind); + if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) { + return this.pullRepository(plugin.marketplaceReference, options); } + return repo.update(this._cacheRoot, plugin, options); + } - const repoDir = this.getPluginSourceInstallUri(descriptor); - const repoExists = await this._fileService.exists(repoDir); - if (!repoExists) { - this._logService.warn(`[AgentPluginRepositoryService] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`); + async cleanupPluginSource(plugin: IMarketplacePlugin): Promise { + const repo = this.getPluginSource(plugin.sourceDescriptor.kind); + const cleanupDir = repo.getCleanupTarget(this._cacheRoot, plugin.sourceDescriptor); + if (!cleanupDir) { return; } - const updateLabel = options?.pluginName ?? plugin.name; - const failureLabel = options?.failureLabel ?? updateLabel; - try { - await this._progressService.withProgress( - { - location: ProgressLocation.Notification, - title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel), - cancellable: false, - }, - async () => { - await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); - if (descriptor.sha) { - await this._commandService.executeCommand('git.fetch', repoDir.fsPath); - } else { - await this._commandService.executeCommand('_git.pull', repoDir.fsPath); - } - await this._checkoutPluginSourceRevision(repoDir, descriptor, failureLabel); - } - ); + const exists = await this._fileService.exists(cleanupDir); + if (exists) { + await this._fileService.del(cleanupDir, { recursive: true }); + this._logService.info(`[${plugin.sourceDescriptor.kind}] Removed plugin cache: ${cleanupDir.toString()}`); + } } catch (err) { - this._logService.error(`[AgentPluginRepositoryService] Failed to update plugin source ${updateLabel}:`, err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), - actions: { - primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { - this._commandService.executeCommand('git.showOutput'); - })], - }, - }); + this._logService.warn(`[${plugin.sourceDescriptor.kind}] Failed to remove plugin cache '${cleanupDir.toString()}':`, err); } - } - private _gitUrlCacheSegments(url: string, ref?: string, sha?: string): string[] { try { - const parsed = URI.parse(url); - const authority = (parsed.authority || 'unknown').replace(/[\\/:*?"<>|]/g, '_').toLowerCase(); - const pathPart = parsed.path.replace(/^\/+/, '').replace(/\.git$/i, '').replace(/\/+$/g, ''); - const segments = pathPart.split('/').map(s => s.replace(/[\\/:*?"<>|]/g, '_')); - return [authority, ...segments, ...this._getSourceRevisionCacheSuffix(ref, sha)]; - } catch { - return ['git', url.replace(/[\\/:*?"<>|]/g, '_'), ...this._getSourceRevisionCacheSuffix(ref, sha)]; - } - } - - private _getSourceRevisionCacheSuffix(descriptorOrRef: IPluginSourceDescriptor | string | undefined, sha?: string): string[] { - if (typeof descriptorOrRef === 'object' && descriptorOrRef) { - if (descriptorOrRef.kind === PluginSourceKind.GitHub || descriptorOrRef.kind === PluginSourceKind.GitUrl) { - return this._getSourceRevisionCacheSuffix(descriptorOrRef.ref, descriptorOrRef.sha); - } - return []; - } - - const ref = descriptorOrRef; - if (sha) { - return [`sha_${sanitizePackageName(sha)}`]; - } - if (ref) { - return [`ref_${sanitizePackageName(ref)}`]; + // Prune empty parent directories up to (but not including) the cache root + // so we don't leave dangling owner/authority folders behind. + await this._pruneEmptyParents(cleanupDir); + } catch (err) { + this._logService.warn(`[${plugin.sourceDescriptor.kind}] Failed to cleanup plugin source:`, err); } - return []; } - private async _checkoutPluginSourceRevision(repoDir: URI, descriptor: IPluginSourceDescriptor, failureLabel: string): Promise { - if (descriptor.kind !== PluginSourceKind.GitHub && descriptor.kind !== PluginSourceKind.GitUrl) { - return; - } - - if (!descriptor.sha && !descriptor.ref) { + /** + * Walk from {@link child}'s parent toward {@link _cacheRoot}, removing + * each directory that is empty. Stops as soon as a non-empty directory + * is found or the cache root is reached. Only operates on descendants + * of the cache root — returns immediately for paths outside it. + */ + private async _pruneEmptyParents(child: URI): Promise { + if (!isEqualOrParent(child, this._cacheRoot)) { return; } - - try { - if (descriptor.sha) { - await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, descriptor.sha, true); - return; + let current = dirname(child); + while (isEqualOrParent(current, this._cacheRoot) && !isEqual(current, this._cacheRoot)) { + try { + const stat = await this._fileService.resolve(current); + if (stat.children && stat.children.length > 0) { + break; + } + await this._fileService.del(current); + } catch { + break; } - - await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, descriptor.ref); - } catch (err) { - this._logService.error(`[AgentPluginRepositoryService] Failed to checkout plugin source revision for ${failureLabel}:`, err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('checkoutPluginSourceFailed', "Failed to checkout plugin '{0}' to requested revision: {1}", failureLabel, err?.message ?? String(err)), - actions: { - primary: [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { - this._commandService.executeCommand('git.showOutput'); - })], - }, - }); - throw err; + current = dirname(current); } } -} -function sanitizePackageName(name: string): string { - return name.replace(/[\\/:*?"<>|]/g, '_'); } diff --git a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts index ae1bbf6cf4a96..5beccafafbc51 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginInstallService.ts @@ -3,21 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancelablePromise, timeout } from '../../../../base/common/async.js'; -import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { isWindows } from '../../../../base/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; -import { TerminalCapability, type ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; -import { ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js'; import { IAgentPluginRepositoryService } from '../common/plugins/agentPluginRepositoryService.js'; import { IPluginInstallService } from '../common/plugins/pluginInstallService.js'; -import { getPluginSourceLabel, IMarketplacePlugin, INpmPluginSource, IPipPluginSource, IPluginMarketplaceService, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IMarketplacePlugin, IPluginMarketplaceService, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; export class PluginInstallService implements IPluginInstallService { declare readonly _serviceBrand: undefined; @@ -27,46 +20,38 @@ export class PluginInstallService implements IPluginInstallService { @IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService, @IFileService private readonly _fileService: IFileService, @INotificationService private readonly _notificationService: INotificationService, - @IDialogService private readonly _dialogService: IDialogService, - @ITerminalService private readonly _terminalService: ITerminalService, - @IProgressService private readonly _progressService: IProgressService, @ILogService private readonly _logService: ILogService, ) { } async installPlugin(plugin: IMarketplacePlugin): Promise { - switch (plugin.sourceDescriptor.kind) { - case PluginSourceKind.RelativePath: - return this._installRelativePathPlugin(plugin); - case PluginSourceKind.GitHub: - case PluginSourceKind.GitUrl: - return this._installGitPlugin(plugin); - case PluginSourceKind.Npm: - return this._installNpmPlugin(plugin, plugin.sourceDescriptor); - case PluginSourceKind.Pip: - return this._installPipPlugin(plugin, plugin.sourceDescriptor); + const kind = plugin.sourceDescriptor.kind; + + if (kind === PluginSourceKind.RelativePath) { + return this._installRelativePathPlugin(plugin); + } + + if (kind === PluginSourceKind.Npm || kind === PluginSourceKind.Pip) { + return this._installPackagePlugin(plugin); } + + // GitHub / GitUrl + return this._installGitPlugin(plugin); } async updatePlugin(plugin: IMarketplacePlugin): Promise { - switch (plugin.sourceDescriptor.kind) { - case PluginSourceKind.RelativePath: - return this._pluginRepositoryService.pullRepository(plugin.marketplaceReference, { - pluginName: plugin.name, - failureLabel: plugin.name, - marketplaceType: plugin.marketplaceType, - }); - case PluginSourceKind.GitHub: - case PluginSourceKind.GitUrl: - return this._pluginRepositoryService.updatePluginSource(plugin, { - pluginName: plugin.name, - failureLabel: plugin.name, - marketplaceType: plugin.marketplaceType, - }); - case PluginSourceKind.Npm: - return this._installNpmPlugin(plugin, plugin.sourceDescriptor); - case PluginSourceKind.Pip: - return this._installPipPlugin(plugin, plugin.sourceDescriptor); + const kind = plugin.sourceDescriptor.kind; + + if (kind === PluginSourceKind.Npm || kind === PluginSourceKind.Pip) { + // Package-manager "update" re-runs install via terminal + return this._installPackagePlugin(plugin); } + + // For relative-path and git sources, delegate to repository service + return this._pluginRepositoryService.updatePluginSource(plugin, { + pluginName: plugin.name, + failureLabel: plugin.name, + marketplaceType: plugin.marketplaceType, + }); } getPluginInstallUri(plugin: IMarketplacePlugin): URI { @@ -115,6 +100,7 @@ export class PluginInstallService implements IPluginInstallService { // --- GitHub / Git URL source (independent clone) -------------------------- private async _installGitPlugin(plugin: IMarketplacePlugin): Promise { + const repo = this._pluginRepositoryService.getPluginSource(plugin.sourceDescriptor.kind); let pluginDir: URI; try { pluginDir = await this._pluginRepositoryService.ensurePluginSource(plugin, { @@ -130,7 +116,7 @@ export class PluginInstallService implements IPluginInstallService { if (!pluginExists) { this._notificationService.notify({ severity: Severity.Error, - message: localize('pluginSourceNotFound', "Plugin source '{0}' not found after cloning.", getPluginSourceLabel(plugin.sourceDescriptor)), + message: localize('pluginSourceNotFound', "Plugin source '{0}' not found after cloning.", repo.getLabel(plugin.sourceDescriptor)), }); return; } @@ -138,186 +124,25 @@ export class PluginInstallService implements IPluginInstallService { this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); } - // --- npm source ----------------------------------------------------------- + // --- Package-manager sources (npm / pip) ---------------------------------- - private async _installNpmPlugin(plugin: IMarketplacePlugin, source: INpmPluginSource): Promise { - const packageSpec = source.version ? `${source.package}@${source.version}` : source.package; - const installDir = await this._pluginRepositoryService.ensurePluginSource(plugin); - const args = ['npm', 'install', '--prefix', installDir.fsPath, packageSpec]; - if (source.registry) { - args.push('--registry', source.registry); - } - const command = this._formatShellCommand(args); - - const confirmed = await this._confirmTerminalCommand(plugin.name, command); - if (!confirmed) { - return; - } - - const { success, terminal } = await this._runTerminalCommand( - command, - localize('installingNpmPlugin', "Installing npm plugin '{0}'...", plugin.name), - ); - if (!success) { + private async _installPackagePlugin(plugin: IMarketplacePlugin): Promise { + const repo = this._pluginRepositoryService.getPluginSource(plugin.sourceDescriptor.kind); + if (!repo.runInstall) { + this._logService.error(`[PluginInstallService] Expected package repository for kind '${plugin.sourceDescriptor.kind}'`); return; } - const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(source); - const pluginExists = await this._fileService.exists(pluginDir); - if (!pluginExists) { - this._notificationService.notify({ - severity: Severity.Error, - message: localize('npmPluginNotFound', "npm package '{0}' was not found after installation.", source.package), - }); - return; - } - - terminal?.dispose(); - this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); - } - - // --- pip source ----------------------------------------------------------- - - private async _installPipPlugin(plugin: IMarketplacePlugin, source: IPipPluginSource): Promise { - const packageSpec = source.version ? `${source.package}==${source.version}` : source.package; + // Ensure the parent cache directory exists (returns npm/ or pip/) const installDir = await this._pluginRepositoryService.ensurePluginSource(plugin); - const args = ['pip', 'install', '--target', installDir.fsPath, packageSpec]; - if (source.registry) { - args.push('--index-url', source.registry); - } - const command = this._formatShellCommand(args); - - const confirmed = await this._confirmTerminalCommand(plugin.name, command); - if (!confirmed) { - return; - } + // The actual plugin content location (e.g. npm//node_modules/) + const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(plugin.sourceDescriptor); - const { success, terminal } = await this._runTerminalCommand( - command, - localize('installingPipPlugin', "Installing pip plugin '{0}'...", plugin.name), - ); - if (!success) { + const result = await repo.runInstall(installDir, pluginDir, plugin); + if (!result) { return; } - const pluginDir = this._pluginRepositoryService.getPluginSourceInstallUri(source); - const pluginExists = await this._fileService.exists(pluginDir); - if (!pluginExists) { - this._notificationService.notify({ - severity: Severity.Error, - message: localize('pipPluginNotFound', "pip package '{0}' was not found after installation.", source.package), - }); - return; - } - - terminal?.dispose(); - this._pluginMarketplaceService.addInstalledPlugin(pluginDir, plugin); - } - - // --- Helpers -------------------------------------------------------------- - - private async _confirmTerminalCommand(pluginName: string, command: string): Promise { - const { confirmed } = await this._dialogService.confirm({ - type: 'question', - message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName), - detail: localize('confirmPluginInstallDetail', "This will run the following command in a terminal:\n\n{0}", command), - primaryButton: localize({ key: 'confirmInstall', comment: ['&& denotes a mnemonic'] }, "&&Install"), - }); - return confirmed; - } - - private async _runTerminalCommand(command: string, progressTitle: string) { - let terminal: ITerminalInstance | undefined; - try { - await this._progressService.withProgress( - { - location: ProgressLocation.Notification, - title: progressTitle, - cancellable: false, - }, - async () => { - terminal = await this._terminalService.createTerminal({ - config: { - name: localize('pluginInstallTerminal', "Plugin Install"), - forceShellIntegration: true, - isTransient: true, - isFeatureTerminal: true, - }, - }); - - await terminal.processReady; - this._terminalService.setActiveInstance(terminal); - - const commandResultPromise = this._waitForTerminalCommandCompletion(terminal); - await terminal.runCommand(command, true); - const exitCode = await commandResultPromise; - if (exitCode !== 0) { - throw new Error(localize('terminalCommandExitCode', "Command exited with code {0}", exitCode)); - } - } - ); - return { success: true, terminal }; - } catch (err) { - this._logService.error('[PluginInstallService] Terminal command failed:', err); - this._notificationService.notify({ - severity: Severity.Error, - message: localize('terminalCommandFailed', "Plugin installation command failed: {0}", err?.message ?? String(err)), - }); - return { success: false, terminal }; - } - } - - private _waitForTerminalCommandCompletion(terminal: ITerminalInstance): Promise { - return new Promise(resolve => { - const disposables = new DisposableStore(); - let isResolved = false; - - const resolveAndDispose = (exitCode: number | undefined): void => { - if (isResolved) { - return; - } - isResolved = true; - disposables.dispose(); - resolve(exitCode); - }; - - const attachCommandFinishedListener = (): void => { - const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection); - if (!commandDetection) { - return; - } - - disposables.add(commandDetection.onCommandFinished((command: ITerminalCommand) => { - resolveAndDispose(command.exitCode ?? 0); - })); - }; - - attachCommandFinishedListener(); - disposables.add(terminal.capabilities.onDidAddCommandDetectionCapability(() => attachCommandFinishedListener())); - - const timeoutHandle: CancelablePromise = timeout(120_000); - disposables.add(toDisposable(() => timeoutHandle.cancel())); - void timeoutHandle.then(() => { - if (isResolved) { - return; - } - this._logService.warn('[PluginInstallService] Terminal command completion timed out'); - resolveAndDispose(undefined); - }); - }); - } - - private _formatShellCommand(args: readonly string[]): string { - const [command, ...rest] = args; - return [command, ...rest.map(arg => this._shellEscapeArg(arg))].join(' '); - } - - private _shellEscapeArg(value: string): string { - if (isWindows) { - // PowerShell: use double quotes, escape backticks, dollar signs, and double quotes - return `"${value.replace(/[`$"]/g, '`$&')}"`; - } - // POSIX shells: use single quotes, escape by ending quote, adding escaped quote, reopening - return `'${value.replace(/'/g, `'\\''`)}'`; + this._pluginMarketplaceService.addInstalledPlugin(result.pluginDir, plugin); } } diff --git a/src/vs/workbench/contrib/chat/browser/pluginSources.ts b/src/vs/workbench/contrib/chat/browser/pluginSources.ts new file mode 100644 index 0000000000000..fef8536d1d9c2 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/pluginSources.ts @@ -0,0 +1,518 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from '../../../../base/common/actions.js'; +import { CancelablePromise, timeout } from '../../../../base/common/async.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { isWindows } from '../../../../base/common/platform.js'; +import { dirname, joinPath } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { TerminalCapability, type ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; +import { ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js'; +import { IEnsureRepositoryOptions, IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js'; +import { IGitHubPluginSource, IGitUrlPluginSource, IMarketplacePlugin, INpmPluginSource, IPipPluginSource, IPluginSourceDescriptor, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js'; +import { IPluginSource } from '../common/plugins/pluginSource.js'; + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +function sanitizeCacheSegment(name: string): string { + return name.replace(/[\\/:*?"<>|]/g, '_'); +} + +function gitRevisionCacheSuffix(ref?: string, sha?: string): string[] { + if (sha) { + return [`sha_${sanitizeCacheSegment(sha)}`]; + } + if (ref) { + return [`ref_${sanitizeCacheSegment(ref)}`]; + } + return []; +} + +function showGitOutputAction(commandService: ICommandService): Action { + return new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => { + commandService.executeCommand('git.showOutput'); + }); +} + +function shellEscapeArg(value: string): string { + if (isWindows) { + return `"${value.replace(/[`$"]/g, '`$&')}"`; + } + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function formatShellCommand(args: readonly string[]): string { + const [command, ...rest] = args; + return [command, ...rest.map(arg => shellEscapeArg(arg))].join(' '); +} + +// --------------------------------------------------------------------------- +// Base for git-based sources (GitHub shorthand & arbitrary Git URL) +// --------------------------------------------------------------------------- + +abstract class AbstractGitPluginSource implements IPluginSource { + abstract readonly kind: PluginSourceKind; + constructor( + @ICommandService protected readonly _commandService: ICommandService, + @IFileService protected readonly _fileService: IFileService, + @ILogService protected readonly _logService: ILogService, + @INotificationService protected readonly _notificationService: INotificationService, + @IProgressService protected readonly _progressService: IProgressService, + ) { } + + abstract getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + abstract getLabel(descriptor: IPluginSourceDescriptor): string; + protected abstract _cloneUrl(descriptor: IPluginSourceDescriptor): string; + protected abstract _displayLabel(descriptor: IPluginSourceDescriptor): string; + + getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined { + return this.getInstallUri(cacheRoot, descriptor); + } + + async ensure(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise { + const descriptor = plugin.sourceDescriptor; + const repoDir = this.getInstallUri(cacheRoot, descriptor); + const repoExists = await this._fileService.exists(repoDir); + const label = this._displayLabel(descriptor); + + if (repoExists) { + await this._checkoutRevision(repoDir, descriptor, options?.failureLabel ?? label); + return repoDir; + } + + const progressTitle = options?.progressTitle ?? localize('cloningPluginSource', "Cloning plugin source '{0}'...", label); + const failureLabel = options?.failureLabel ?? label; + const ref = (descriptor as IGitHubPluginSource | IGitUrlPluginSource).ref; + + await this._cloneRepository(repoDir, this._cloneUrl(descriptor), progressTitle, failureLabel, ref); + await this._checkoutRevision(repoDir, descriptor, failureLabel); + return repoDir; + } + + async update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise { + const descriptor = plugin.sourceDescriptor; + const repoDir = this.getInstallUri(cacheRoot, descriptor); + const repoExists = await this._fileService.exists(repoDir); + if (!repoExists) { + this._logService.warn(`[${this.kind}] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`); + return; + } + + const updateLabel = options?.pluginName ?? plugin.name; + const failureLabel = options?.failureLabel ?? updateLabel; + + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel), + cancellable: false, + }, + async () => { + await this._commandService.executeCommand('git.openRepository', repoDir.fsPath); + const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource; + if (git.sha) { + await this._commandService.executeCommand('git.fetch', repoDir.fsPath); + } else { + await this._commandService.executeCommand('_git.pull', repoDir.fsPath); + } + await this._checkoutRevision(repoDir, descriptor, failureLabel); + } + ); + } catch (err) { + this._logService.error(`[${this.kind}] Failed to update plugin source '${updateLabel}':`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + } + } + + // -- internal helpers --- + + private async _cloneRepository(repoDir: URI, cloneUrl: string, progressTitle: string, failureLabel: string, ref?: string): Promise { + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: progressTitle, + cancellable: false, + }, + async () => { + await this._fileService.createFolder(dirname(repoDir)); + await this._commandService.executeCommand('_git.cloneRepository', cloneUrl, repoDir.fsPath, ref); + } + ); + } catch (err) { + this._logService.error(`[${this.kind}] Failed to clone ${cloneUrl}:`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('cloneFailed', "Failed to install plugin '{0}': {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + throw err; + } + } + + private async _checkoutRevision(repoDir: URI, descriptor: IPluginSourceDescriptor, failureLabel: string): Promise { + const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource; + if (!git.sha && !git.ref) { + return; + } + + try { + if (git.sha) { + await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, git.sha, true); + return; + } + await this._commandService.executeCommand('_git.checkout', repoDir.fsPath, git.ref); + } catch (err) { + this._logService.error(`[${this.kind}] Failed to checkout revision for '${failureLabel}':`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('checkoutPluginSourceFailed', "Failed to checkout plugin '{0}' to requested revision: {1}", failureLabel, err?.message ?? String(err)), + actions: { primary: [showGitOutputAction(this._commandService)] }, + }); + throw err; + } + } +} + +// --------------------------------------------------------------------------- +// RelativePath — plugin lives inside a shared marketplace repository +// --------------------------------------------------------------------------- + +export class RelativePathPluginSource implements IPluginSource { + readonly kind = PluginSourceKind.RelativePath; + + getInstallUri(_cacheRoot: URI, _descriptor: IPluginSourceDescriptor): URI { + throw new Error('Use getPluginInstallUri() for relative-path sources'); + } + + async ensure(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IEnsureRepositoryOptions): Promise { + throw new Error('Use ensureRepository() for relative-path sources'); + } + + async update(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { + throw new Error('Use pullRepository() for relative-path sources'); + } + + getCleanupTarget(_cacheRoot: URI, _descriptor: IPluginSourceDescriptor): URI | undefined { + return undefined; + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as { path: string }).path || '.'; + } +} + +// --------------------------------------------------------------------------- +// GitHub — `{ source: "github", repo: "owner/repo" }` +// --------------------------------------------------------------------------- + +export class GitHubPluginSource extends AbstractGitPluginSource { + readonly kind = PluginSourceKind.GitHub; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const gh = descriptor as IGitHubPluginSource; + const [owner, repo] = gh.repo.split('/'); + return joinPath(cacheRoot, 'github.com', owner, repo, ...gitRevisionCacheSuffix(gh.ref, gh.sha)); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitHubPluginSource).repo; + } + + protected _cloneUrl(descriptor: IPluginSourceDescriptor): string { + return `https://github.com/${(descriptor as IGitHubPluginSource).repo}.git`; + } + + protected _displayLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitHubPluginSource).repo; + } +} + +// --------------------------------------------------------------------------- +// GitUrl — `{ source: "url", url: "https://…/repo.git" }` +// --------------------------------------------------------------------------- + +export class GitUrlPluginSource extends AbstractGitPluginSource { + readonly kind = PluginSourceKind.GitUrl; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const git = descriptor as IGitUrlPluginSource; + const segments = this._gitUrlCacheSegments(git.url, git.ref, git.sha); + return joinPath(cacheRoot, ...segments); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitUrlPluginSource).url; + } + + protected _cloneUrl(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitUrlPluginSource).url; + } + + protected _displayLabel(descriptor: IPluginSourceDescriptor): string { + return (descriptor as IGitUrlPluginSource).url; + } + + private _gitUrlCacheSegments(url: string, ref?: string, sha?: string): string[] { + try { + const parsed = URI.parse(url); + const authority = (parsed.authority || 'unknown').replace(/[\\/:*?"<>|]/g, '_').toLowerCase(); + const pathPart = parsed.path.replace(/^\/+/, '').replace(/\.git$/i, '').replace(/\/+$/g, ''); + const segments = pathPart.split('/').map(s => s.replace(/[\\/:*?"<>|]/g, '_')); + return [authority, ...segments, ...gitRevisionCacheSuffix(ref, sha)]; + } catch { + return ['git', url.replace(/[\\/:*?"<>|]/g, '_'), ...gitRevisionCacheSuffix(ref, sha)]; + } + } +} + +// --------------------------------------------------------------------------- +// Base for package-manager-based sources (npm, pip) +// --------------------------------------------------------------------------- + +export abstract class AbstractPackagePluginSource implements IPluginSource { + abstract readonly kind: PluginSourceKind; + constructor( + @IDialogService protected readonly _dialogService: IDialogService, + @IFileService protected readonly _fileService: IFileService, + @ILogService protected readonly _logService: ILogService, + @INotificationService protected readonly _notificationService: INotificationService, + @IProgressService protected readonly _progressService: IProgressService, + @ITerminalService protected readonly _terminalService: ITerminalService, + ) { } + + abstract getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + abstract getLabel(descriptor: IPluginSourceDescriptor): string; + + getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined { + return this._getCacheDir(cacheRoot, descriptor); + } + + /** + * Return the parent directory (prefix / target) where the package + * manager installs into. This is above the actual plugin content dir. + */ + protected abstract _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + + /** Build the terminal command args for install. */ + protected abstract _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[]; + + /** Human-readable package manager name for messages. */ + protected abstract get _managerName(): string; + + async ensure(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IEnsureRepositoryOptions): Promise { + const cacheDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor); + await this._fileService.createFolder(cacheDir); + return cacheDir; + } + + async update(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise { + // For package-manager sources, "update" re-runs install. + const installDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor); + const pluginDir = this.getInstallUri(cacheRoot, plugin.sourceDescriptor); + await this.runInstall(installDir, pluginDir, plugin); + } + + async runInstall(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin): Promise<{ pluginDir: URI } | undefined> { + const args = this._buildInstallArgs(installDir, plugin); + const command = formatShellCommand(args); + const confirmed = await this._confirmTerminalCommand(plugin.name, command); + if (!confirmed) { + return undefined; + } + + const progressTitle = localize('installingPackagePlugin', "Installing {0} plugin '{1}'...", this._managerName, plugin.name); + const { success, terminal } = await this._runTerminalCommand(command, progressTitle); + if (!success) { + return undefined; + } + + const exists = await this._fileService.exists(pluginDir); + if (!exists) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('packagePluginNotFound', "{0} package '{1}' was not found after installation.", this._managerName, this.getLabel(plugin.sourceDescriptor)), + }); + return undefined; + } + + terminal?.dispose(); + return { pluginDir }; + } + + // -- terminal helpers (moved from PluginInstallService) --- + + private async _confirmTerminalCommand(pluginName: string, command: string): Promise { + const { confirmed } = await this._dialogService.confirm({ + type: 'question', + message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName), + detail: localize('confirmPluginInstallDetail', "This will run the following command in a terminal:\n\n{0}", command), + primaryButton: localize({ key: 'confirmInstall', comment: ['&& denotes a mnemonic'] }, "&&Install"), + }); + return confirmed; + } + + private async _runTerminalCommand(command: string, progressTitle: string) { + let terminal: ITerminalInstance | undefined; + try { + await this._progressService.withProgress( + { + location: ProgressLocation.Notification, + title: progressTitle, + cancellable: false, + }, + async () => { + terminal = await this._terminalService.createTerminal({ + config: { + name: localize('pluginInstallTerminal', "Plugin Install"), + forceShellIntegration: true, + isTransient: true, + isFeatureTerminal: true, + }, + }); + await terminal.processReady; + this._terminalService.setActiveInstance(terminal); + + const commandResultPromise = this._waitForTerminalCommandCompletion(terminal); + await terminal.runCommand(command, true); + const exitCode = await commandResultPromise; + if (exitCode !== 0) { + throw new Error(localize('terminalCommandExitCode', "Command exited with code {0}", exitCode)); + } + } + ); + return { success: true, terminal }; + } catch (err) { + this._logService.error(`[${this.kind}] Terminal command failed:`, err); + this._notificationService.notify({ + severity: Severity.Error, + message: localize('terminalCommandFailed', "Plugin installation command failed: {0}", err?.message ?? String(err)), + }); + return { success: false, terminal }; + } + } + + private _waitForTerminalCommandCompletion(terminal: ITerminalInstance): Promise { + return new Promise(resolve => { + const disposables = new DisposableStore(); + let isResolved = false; + + const resolveAndDispose = (exitCode: number | undefined): void => { + if (isResolved) { + return; + } + isResolved = true; + disposables.dispose(); + resolve(exitCode); + }; + + const attachCommandFinishedListener = (): void => { + const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection); + if (!commandDetection) { + return; + } + disposables.add(commandDetection.onCommandFinished((command: ITerminalCommand) => { + resolveAndDispose(command.exitCode ?? 0); + })); + }; + + attachCommandFinishedListener(); + disposables.add(terminal.capabilities.onDidAddCommandDetectionCapability(() => attachCommandFinishedListener())); + + const timeoutHandle: CancelablePromise = timeout(120_000); + disposables.add(toDisposable(() => timeoutHandle.cancel())); + void timeoutHandle.then(() => { + if (isResolved) { + return; + } + this._logService.warn(`[${this.kind}] Terminal command completion timed out`); + resolveAndDispose(undefined); + }); + }); + } +} + +// --------------------------------------------------------------------------- +// npm — `{ source: "npm", package: "@org/plugin" }` +// --------------------------------------------------------------------------- + +export class NpmPluginSource extends AbstractPackagePluginSource { + readonly kind = PluginSourceKind.Npm; + protected readonly _managerName = 'npm'; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const npm = descriptor as INpmPluginSource; + return joinPath(cacheRoot, 'npm', sanitizeCacheSegment(npm.package), 'node_modules', npm.package); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + const npm = descriptor as INpmPluginSource; + return npm.version ? `${npm.package}@${npm.version}` : npm.package; + } + + protected _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const npm = descriptor as INpmPluginSource; + return joinPath(cacheRoot, 'npm', sanitizeCacheSegment(npm.package)); + } + + protected _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[] { + const npm = plugin.sourceDescriptor as INpmPluginSource; + const packageSpec = npm.version ? `${npm.package}@${npm.version}` : npm.package; + const args = ['npm', 'install', '--prefix', installDir.fsPath, packageSpec]; + if (npm.registry) { + args.push('--registry', npm.registry); + } + return args; + } +} + +// --------------------------------------------------------------------------- +// pip — `{ source: "pip", package: "my-plugin" }` +// --------------------------------------------------------------------------- + +export class PipPluginSource extends AbstractPackagePluginSource { + readonly kind = PluginSourceKind.Pip; + protected readonly _managerName = 'pip'; + + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const pip = descriptor as IPipPluginSource; + return joinPath(cacheRoot, 'pip', sanitizeCacheSegment(pip.package)); + } + + getLabel(descriptor: IPluginSourceDescriptor): string { + const pip = descriptor as IPipPluginSource; + return pip.version ? `${pip.package}==${pip.version}` : pip.package; + } + + protected _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI { + const pip = descriptor as IPipPluginSource; + return joinPath(cacheRoot, 'pip', sanitizeCacheSegment(pip.package)); + } + + protected _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[] { + const pip = plugin.sourceDescriptor as IPipPluginSource; + const packageSpec = pip.version ? `${pip.package}==${pip.version}` : pip.package; + const args = ['pip', 'install', '--target', installDir.fsPath, packageSpec]; + if (pip.registry) { + args.push('--index-url', pip.registry); + } + return args; + } +} diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts index cfb76de1c1d75..c0708bb503d6c 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginRepositoryService.ts @@ -5,7 +5,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType } from './pluginMarketplaceService.js'; +import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceType, PluginSourceKind } from './pluginMarketplaceService.js'; +import { IPluginSource } from './pluginSource.js'; export const IAgentPluginRepositoryService = createDecorator('agentPluginRepositoryService'); @@ -83,4 +84,21 @@ export interface IAgentPluginRepositoryService { * ref/sha checkout. For npm/pip sources this is a no-op. */ updatePluginSource(plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; + + /** + * Returns the {@link IPluginSource} strategy for the given + * source kind, allowing callers to invoke kind-specific operations + * (install, update, label, etc.) directly. + */ + getPluginSource(kind: PluginSourceKind): IPluginSource; + + /** + * Cleans up on-disk cache for a plugin source that owns its own install + * directory. For marketplace-relative sources this is a no-op (they share + * the marketplace repository cache). For direct sources (github, url, npm, + * pip) the cache directory is deleted. + * + * This is best-effort: failures are logged but do not throw. + */ + cleanupPluginSource(plugin: IMarketplacePlugin): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 722ff6da5769d..7377041a9e6de 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -34,6 +34,7 @@ import { parseClaudeHooks } from '../promptSyntax/hookClaudeCompat.js'; import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js'; import { IHookCommand } from '../promptSyntax/hookSchema.js'; import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; +import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js'; import { IMarketplacePlugin, IPluginMarketplaceService } from './pluginMarketplaceService.js'; const COMMAND_FILE_SUFFIX = '.md'; @@ -865,6 +866,7 @@ export class MarketplaceAgentPluginDiscovery extends AbstractAgentPluginDiscover constructor( @IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService, + @IAgentPluginRepositoryService private readonly _pluginRepositoryService: IAgentPluginRepositoryService, @IFileService fileService: IFileService, @IPathService pathService: IPathService, @ILogService logService: ILogService, @@ -905,7 +907,17 @@ export class MarketplaceAgentPluginDiscovery extends AbstractAgentPluginDiscover enabled: entry.enabled, fromMarketplace: entry.plugin, setEnabled: (value: boolean) => this._pluginMarketplaceService.setInstalledPluginEnabled(entry.pluginUri, value), - remove: () => this._pluginMarketplaceService.removeInstalledPlugin(entry.pluginUri), + remove: () => { + // Always remove the metadata entry first so the plugin + // disappears from the UI immediately. + this._pluginMarketplaceService.removeInstalledPlugin(entry.pluginUri); + // For non-marketplace (direct-source) plugins, also clean up the + // on-disk cache. This is best-effort — failures are logged but + // do not block removal. + this._pluginRepositoryService.cleanupPluginSource(entry.plugin).catch(error => { + this._logService.error('[MarketplaceAgentPluginDiscovery] Failed to clean up plugin source', error); + }); + }, }); } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts new file mode 100644 index 0000000000000..704d4334b0ce5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginSource.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../base/common/uri.js'; +import { IEnsureRepositoryOptions, IPullRepositoryOptions } from './agentPluginRepositoryService.js'; +import { IMarketplacePlugin, IPluginSourceDescriptor, PluginSourceKind } from './pluginMarketplaceService.js'; + +/** + * Per-kind strategy that centralizes install-path computation, source + * provisioning, update, label formatting, and uninstall cleanup for a + * single {@link PluginSourceKind}. + * + * Implementations are created via {@link IInstantiationService} so they + * can dependency-inject any services they need (git commands, file service, + * terminal service, etc.). + */ +export interface IPluginSource { + readonly kind: PluginSourceKind; + + /** + * Compute the local cache URI where this source's plugin files live. + * @param cacheRoot The root cache directory for all agent plugins. + */ + getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI; + + /** + * Ensure the plugin source is available locally (clone, npm install, etc.). + * Returns the install directory URI. + */ + ensure(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise; + + /** + * Update an already-installed plugin source (git pull, npm update, etc.). + */ + update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise; + + /** + * Returns the on-disk directory to delete when this plugin is + * uninstalled, or `undefined` if no cleanup is needed. + * + * Marketplace-relative sources return `undefined` because they share + * a marketplace repository cache. Direct sources (github, url, npm, + * pip) return the directory they own. + */ + getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined; + + /** + * Returns a human-readable label for a source descriptor of this kind, + * suitable for error messages and UI display. + */ + getLabel(descriptor: IPluginSourceDescriptor): string; + + /** + * For package-manager sources (npm, pip): run the terminal install + * command and return the resulting plugin directory, or `undefined` + * if the user cancelled or the command failed. + * + * Not implemented by non-package-manager sources. + */ + runInstall?(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin): Promise<{ pluginDir: URI } | undefined>; +} diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts index 0aad10fc12286..f2f3b310a1312 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/agentPluginRepositoryService.test.ts @@ -210,4 +210,193 @@ suite('AgentPluginRepositoryService', () => { assert.deepStrictEqual(commands, ['git.openRepository', 'git.fetch', '_git.checkout']); }); + + // ========================================================================= + // cleanupPluginSource — issue #297251 regression + // ========================================================================= + + suite('cleanupPluginSource', () => { + + function createServiceWithDel( + onDel: (resource: URI) => void, + options?: { resolve?: (resource: URI) => { children?: unknown[] } }, + ) { + const instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(ICommandService, { executeCommand: async () => undefined } as unknown as ICommandService); + instantiationService.stub(IEnvironmentService, { cacheHome: URI.file('/cache') } as unknown as IEnvironmentService); + instantiationService.stub(IFileService, { + exists: async () => true, + del: async (resource: URI) => { onDel(resource); }, + createFolder: async () => undefined, + resolve: async (resource: URI) => options?.resolve?.(resource) ?? { children: [] }, + } as unknown as IFileService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(INotificationService, { notify: () => undefined } as unknown as INotificationService); + instantiationService.stub(IProgressService, { withProgress: async (_o: unknown, cb: (...a: unknown[]) => Promise) => cb() } as unknown as IProgressService); + instantiationService.stub(IStorageService, store.add(new InMemoryStorageService())); + return instantiationService.createInstance(AgentPluginRepositoryService); + } + + test('does not delete files for relative-path (marketplace) plugin', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'marketplace-plugin', + description: '', + version: '', + source: 'plugins/foo', + sourceDescriptor: { kind: PluginSourceKind.RelativePath, path: 'plugins/foo' }, + marketplace: 'microsoft/vscode', + marketplaceReference: parseMarketplaceReference('microsoft/vscode')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.strictEqual(deleted.length, 0); + }); + + test('deletes cache for github plugin source', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.ok(deleted.length >= 1); + assert.ok(deleted[0].includes('github.com/owner/repo')); + }); + + test('deletes parent cache dir for npm plugin source', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'npm-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.Npm, package: '@acme/plugin' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.ok(deleted.length >= 1); + // First delete should be the npm/ cache dir + assert.ok(deleted[0].includes('/npm/'), `Expected npm path, got: ${deleted[0]}`); + }); + + test('deletes cache for pip plugin source', async () => { + const deleted: string[] = []; + const service = createServiceWithDel(r => deleted.push(r.path)); + + await service.cleanupPluginSource({ + name: 'pip-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.Pip, package: 'my-pip-pkg' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + assert.ok(deleted.length >= 1); + assert.ok(deleted[0].includes('pip/my-pip-pkg')); + }); + + test('does not throw when delete fails', async () => { + const instantiationService = store.add(new TestInstantiationService()); + instantiationService.stub(ICommandService, { executeCommand: async () => undefined } as unknown as ICommandService); + instantiationService.stub(IEnvironmentService, { cacheHome: URI.file('/cache') } as unknown as IEnvironmentService); + instantiationService.stub(IFileService, { + exists: async () => true, + del: async () => { throw new Error('permission denied'); }, + createFolder: async () => undefined, + resolve: async () => ({ children: [] }), + } as unknown as IFileService); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(INotificationService, { notify: () => undefined } as unknown as INotificationService); + instantiationService.stub(IProgressService, { withProgress: async (_o: unknown, cb: (...a: unknown[]) => Promise) => cb() } as unknown as IProgressService); + instantiationService.stub(IStorageService, store.add(new InMemoryStorageService())); + const service = instantiationService.createInstance(AgentPluginRepositoryService); + + // Should not throw — cleanup is best-effort + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + }); + + test('prunes empty parent directories up to cache root', async () => { + // After deleting github.com/owner/repo, the "owner" dir is empty + // and should also be removed. + const deleted: string[] = []; + const service = createServiceWithDel( + r => deleted.push(r.path), + { resolve: () => ({ children: [] }) }, + ); + + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + // Should have deleted the repo dir + empty parents (owner, github.com) + assert.ok(deleted.length >= 2, `Expected at least 2 deletions (repo + parent), got ${deleted.length}: ${deleted.join(', ')}`); + assert.ok(deleted[0].includes('github.com/owner/repo'), 'First delete should be the repo dir'); + assert.ok(deleted.some(p => p.endsWith('/owner')), 'Should prune empty owner directory'); + }); + + test('stops pruning at non-empty parent', async () => { + const deleted: string[] = []; + const service = createServiceWithDel( + r => deleted.push(r.path), + { + resolve: (resource: URI) => { + // owner dir still has another repo + if (resource.path.endsWith('/owner')) { + return { children: [{ name: 'other-repo' }] }; + } + return { children: [] }; + }, + }, + ); + + await service.cleanupPluginSource({ + name: 'gh-plugin', + description: '', + version: '', + source: '', + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: 'owner/repo' }, + marketplace: 'owner/marketplace', + marketplaceReference: parseMarketplaceReference('owner/marketplace')!, + marketplaceType: MarketplaceType.Copilot, + }); + + // Should only delete the repo dir, stop at non-empty owner dir + assert.strictEqual(deleted.length, 1); + assert.ok(deleted[0].includes('github.com/owner/repo')); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts index 7a55baa369a43..a0652f4664119 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginInstallService.test.ts @@ -16,6 +16,7 @@ import { ITerminalService } from '../../../../terminal/browser/terminal.js'; import { PluginInstallService } from '../../../browser/pluginInstallService.js'; import { IAgentPluginRepositoryService, IEnsureRepositoryOptions, IPullRepositoryOptions } from '../../../common/plugins/agentPluginRepositoryService.js'; import { IMarketplacePlugin, IMarketplaceReference, IPluginMarketplaceService, IPluginSourceDescriptor, MarketplaceType, parseMarketplaceReference, PluginSourceKind } from '../../../common/plugins/pluginMarketplaceService.js'; +import { IPluginSource } from '../../../common/plugins/pluginSource.js'; suite('PluginInstallService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -145,6 +146,69 @@ suite('PluginInstallService', () => { instantiationService.stub(ILogService, new NullLogService()); // IAgentPluginRepositoryService + // Build mock source repositories for npm/pip that simulate terminal-based install + const makeMockPackageRepo = (kind: PluginSourceKind): IPluginSource => ({ + kind, + getCleanupTarget: () => URI.file('/mock-cleanup'), + getInstallUri: () => URI.file('/mock'), + ensure: async () => state.ensurePluginSourceResult, + update: async () => { }, + getLabel: (d) => kind === PluginSourceKind.Npm ? (d as { package: string }).package : (d as { package: string }).package, + runInstall: async (_installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin) => { + // Simulate confirmation dialog + if (!state.dialogConfirmResult) { + return undefined; + } + + // Simulate building and running the command + const descriptor = plugin.sourceDescriptor; + let args: string[]; + if (kind === PluginSourceKind.Npm) { + const npm = descriptor as { package: string; version?: string; registry?: string }; + const packageSpec = npm.version ? `${npm.package}@${npm.version}` : npm.package; + args = ['npm', 'install', '--prefix', _installDir.fsPath, packageSpec]; + if (npm.registry) { + args.push('--registry', npm.registry); + } + } else { + const pip = descriptor as { package: string; version?: string; registry?: string }; + const packageSpec = pip.version ? `${pip.package}==${pip.version}` : pip.package; + args = ['pip', 'install', '--target', _installDir.fsPath, packageSpec]; + if (pip.registry) { + args.push('--index-url', pip.registry); + } + } + const command = args.join(' '); + state.terminalCommands.push(command); + + if (state.terminalExitCode !== 0) { + state.notifications.push({ severity: 3, message: `Plugin installation command failed: Command exited with code ${state.terminalExitCode}` }); + return undefined; + } + + // Check if plugin dir exists + const exists = typeof state.fileExistsResult === 'function' + ? await state.fileExistsResult(pluginDir) + : state.fileExistsResult; + if (!exists) { + const label = kind === PluginSourceKind.Npm ? 'npm' : 'pip'; + const pkg = (descriptor as { package: string }).package; + state.notifications.push({ severity: 3, message: `${label} package '${pkg}' was not found after installation.` }); + return undefined; + } + + return { pluginDir }; + }, + }); + + const mockSourceRepos = new Map([ + [PluginSourceKind.RelativePath, { kind: PluginSourceKind.RelativePath, getCleanupTarget: () => undefined, getInstallUri: () => { throw new Error(); }, ensure: async () => { throw new Error(); }, update: async () => { throw new Error(); }, getLabel: (d) => (d as { path: string }).path || '.' }], + [PluginSourceKind.GitHub, { kind: PluginSourceKind.GitHub, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => { }, getLabel: (d) => (d as { repo: string }).repo }], + [PluginSourceKind.GitUrl, { kind: PluginSourceKind.GitUrl, getCleanupTarget: () => URI.file('/mock'), getInstallUri: () => URI.file('/mock'), ensure: async () => URI.file('/mock'), update: async () => { }, getLabel: (d) => (d as { url: string }).url }], + [PluginSourceKind.Npm, makeMockPackageRepo(PluginSourceKind.Npm)], + [PluginSourceKind.Pip, makeMockPackageRepo(PluginSourceKind.Pip)], + ]); + instantiationService.stub(IAgentPluginRepositoryService, { getPluginInstallUri: (plugin: IMarketplacePlugin) => { return URI.joinPath(state.ensureRepositoryResult, plugin.source); @@ -164,6 +228,8 @@ suite('PluginInstallService', () => { updatePluginSource: async (plugin: IMarketplacePlugin, options?: IPullRepositoryOptions) => { state.updatePluginSourceCalls.push({ plugin, options }); }, + getPluginSource: (kind: PluginSourceKind) => mockSourceRepos.get(kind)!, + cleanupPluginSource: async () => { }, } as unknown as IAgentPluginRepositoryService); // IPluginMarketplaceService @@ -540,7 +606,7 @@ suite('PluginInstallService', () => { suite('updatePlugin', () => { - test('calls pullRepository for relative-path plugins', async () => { + test('calls updatePluginSource for relative-path plugins', async () => { const { service, state } = createService(); const plugin = createPlugin({ source: 'plugins/myPlugin', @@ -549,8 +615,7 @@ suite('PluginInstallService', () => { await service.updatePlugin(plugin); - assert.strictEqual(state.pullRepositoryCalls.length, 1); - assert.strictEqual(state.updatePluginSourceCalls.length, 0); + assert.strictEqual(state.updatePluginSourceCalls.length, 1); }); test('calls updatePluginSource for GitHub plugins', async () => { @@ -562,7 +627,6 @@ suite('PluginInstallService', () => { await service.updatePlugin(plugin); assert.strictEqual(state.updatePluginSourceCalls.length, 1); - assert.strictEqual(state.pullRepositoryCalls.length, 0); }); test('calls updatePluginSource for GitUrl plugins', async () => { @@ -574,7 +638,6 @@ suite('PluginInstallService', () => { await service.updatePlugin(plugin); assert.strictEqual(state.updatePluginSourceCalls.length, 1); - assert.strictEqual(state.pullRepositoryCalls.length, 0); }); test('re-installs for npm plugin updates', async () => { From 44e1948e8cc7dd1115bd3c9bbbd86d812b0e0950 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 4 Mar 2026 16:24:52 -0800 Subject: [PATCH 14/21] Refactor agent session provider handling and update background agent display name logic (#299328) --- .../chat/browser/agentSessions/agentSessions.ts | 10 ++-------- .../contrib/chat/browser/chat.contribution.ts | 11 +---------- .../chatSessions/chatSessions.contribution.ts | 3 +-- .../widget/input/sessionTargetPickerActionItem.ts | 14 +++----------- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index a8be72f98ff29..f7662aa69144b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../../nls.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { observableValue } from '../../../../../base/common/observable.js'; + import { IChatSessionTiming } from '../../common/chatService/chatService.js'; import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; @@ -42,18 +42,12 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes } } -/** - * Observable holding the display name for the background agent session provider. - * Updated via experiment treatment to allow A/B testing of the display name. - */ -export const backgroundAgentDisplayName = observableValue('backgroundAgentDisplayName', localize('chat.session.providerLabel.background', "Background")); - export function getAgentSessionProviderName(provider: AgentSessionProviders): string { switch (provider) { case AgentSessionProviders.Local: return localize('chat.session.providerLabel.local', "Local"); case AgentSessionProviders.Background: - return backgroundAgentDisplayName.get(); + return localize('chat.session.providerLabel.background', "Copilot CLI"); case AgentSessionProviders.Cloud: return localize('chat.session.providerLabel.cloud', "Cloud"); case AgentSessionProviders.Claude: diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2c38f3c9874da..7b9d6e57798ec 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -100,7 +100,7 @@ import { ChatDebugEditor } from './chatDebug/chatDebugEditor.js'; import { PromptsDebugContribution } from './promptsDebugContribution.js'; import { ChatDebugEditorInput, ChatDebugEditorInputSerializer } from './chatDebug/chatDebugEditorInput.js'; import './agentSessions/agentSessions.contribution.js'; -import { backgroundAgentDisplayName } from './agentSessions/agentSessions.js'; + import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { ChatViewId, IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService, isIChatResourceViewContext, isIChatViewViewContext } from './chat.js'; @@ -1433,7 +1433,6 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr super(); this.newChatButtonExperimentIcon = ChatContextKeys.newChatButtonExperimentIcon.bindTo(this.contextKeyService); this.registerMaxRequestsSetting(); - this.registerBackgroundAgentDisplayName(); this.registerNewChatButtonIcon(); } @@ -1465,14 +1464,6 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr this._register(Event.runAndSubscribe(Event.debounce(this.entitlementService.onDidChangeEntitlement, () => { }, 1000), () => registerMaxRequestsSetting())); } - private registerBackgroundAgentDisplayName(): void { - this.experimentService.getTreatment('backgroundAgentDisplayName').then((value) => { - if (value) { - backgroundAgentDisplayName.set(value, undefined); - } - }); - } - private registerNewChatButtonIcon(): void { this.experimentService.getTreatment('chatNewButtonIcon').then((value) => { const supportedValues = ['copilot', 'new-session', 'comment']; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 309e48a4472d0..34118f93f1f8a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -44,7 +44,7 @@ import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatViewId } from '../chat.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; -import { AgentSessionProviders, backgroundAgentDisplayName, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { BugIndicatingError, isCancellationError } from '../../../../../base/common/errors.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { LocalChatSessionUri } from '../../common/model/chatUri.js'; @@ -347,7 +347,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ ).recomputeInitiallyAndOnChange(this._store); this._register(autorun(reader => { - backgroundAgentDisplayName.read(reader); const activatedProviders = [...builtinSessionProviders, ...contributedSessionProviders.read(reader)]; for (const provider of Object.values(AgentSessionProviders)) { if (activatedProviders.includes(provider)) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 9ef9fb5eab66a..d70bfd5969581 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -18,11 +18,11 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; -import { AgentSessionProviders, backgroundAgentDisplayName, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { ISessionTypePickerDelegate } from '../../chat.js'; import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; -import { autorun } from '../../../../../../base/common/observable.js'; + export interface ISessionTypeItem { type: AgentSessionProviders; @@ -105,15 +105,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { this._updateAgentSessionItems(); })); - // Re-render when the background agent display name changes via experiment - // Note: autorun runs immediately, so this also handles initial population - this._register(autorun(reader => { - backgroundAgentDisplayName.read(reader); - this._updateAgentSessionItems(); - if (this.element) { - this.renderLabel(this.element); - } - })); + this._updateAgentSessionItems(); } protected _run(sessionTypeItem: ISessionTypeItem): void { From 5222fc6f558431d8982f20a34e800082cd9cd28d Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 4 Mar 2026 16:51:02 -0800 Subject: [PATCH 15/21] Add feature flag for custom agent hooks (#299316) --- .../contrib/chat/browser/chat.contribution.ts | 9 ++++++++ .../chat/common/promptSyntax/config/config.ts | 5 ++++ .../promptHeaderAutocompletion.ts | 6 +++++ .../languageProviders/promptHovers.ts | 6 +++++ .../languageProviders/promptValidator.ts | 23 +++++++++++++++---- .../service/promptsServiceImpl.ts | 10 ++++++-- .../promptHeaderAutocompletion.test.ts | 1 + .../languageProviders/promptHovers.test.ts | 1 + .../languageProviders/promptValidator.test.ts | 1 + 9 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7b9d6e57798ec..d9bcf54ea39cb 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1070,6 +1070,15 @@ configurationRegistry.registerConfiguration({ disallowConfigurationDefault: true, tags: ['preview', 'prompts', 'hooks', 'agent'] }, + [PromptsConfig.USE_CUSTOM_AGENT_HOOKS]: { + type: 'boolean', + title: nls.localize('chat.useCustomAgentHooks.title', "Use Custom Agent Hooks",), + markdownDescription: nls.localize('chat.useCustomAgentHooks.description', "Controls whether hooks defined in custom agent frontmatter are parsed and executed. When disabled, hooks from agent files are ignored.",), + default: false, + restricted: true, + disallowConfigurationDefault: true, + tags: ['preview', 'prompts', 'hooks', 'agent'] + }, [PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: { type: 'object', scope: ConfigurationScope.RESOURCE, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts index 1f0b9da69ca91..4c7497b9f7d6f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts @@ -115,6 +115,11 @@ export namespace PromptsConfig { */ export const USE_CLAUDE_HOOKS = 'chat.useClaudeHooks'; + /** + * Configuration key for enabling hooks defined in custom agent frontmatter. + */ + export const USE_CUSTOM_AGENT_HOOKS = 'chat.useCustomAgentHooks'; + /** * Configuration key for enabling stronger skill adherence prompt (experimental). */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 4884aed9fa863..3b4151e65ebc6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -21,6 +21,8 @@ import { localize } from '../../../../../../nls.js'; import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.js'; import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { PromptsConfig } from '../config/config.js'; export class PromptHeaderAutocompletion implements CompletionItemProvider { /** @@ -38,6 +40,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @IChatModeService private readonly chatModeService: IChatModeService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -138,6 +141,9 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { const target = getTarget(promptType, header); const attributesToPropose = new Set(getValidAttributeNames(promptType, false, target)); + if (!this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + attributesToPropose.delete(PromptHeaderAttributes.hooks); + } for (const attr of header.attributes) { attributesToPropose.delete(attr.key); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 00065d1b40c3a..e3d4b279cd93b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -19,6 +19,8 @@ import { IHeaderAttribute, ISequenceValue, parseCommaSeparatedList, PromptBody, import { ClaudeHeaderAttributes, getAttributeDefinition, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptFileAttributes.js'; import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { PromptsConfig } from '../config/config.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -31,6 +33,7 @@ export class PromptHoverProvider implements HoverProvider { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IChatModeService private readonly chatModeService: IChatModeService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -89,6 +92,9 @@ export class PromptHoverProvider implements HoverProvider { case PromptHeaderAttributes.handOffs: return this.getHandsOffHover(attribute, position, target); case PromptHeaderAttributes.hooks: + if (!this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + return undefined; + } return this.getHooksHover(attribute, position, description, target); case PromptHeaderAttributes.infer: return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', 'Deprecated: Use `user-invocable` and `disable-model-invocation` instead.'), attribute.range); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index d6e71491d019d..d814c0b866ad0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -21,6 +21,7 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../../../bas import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IPromptsService } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { AGENTS_SOURCE_FOLDER, CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; @@ -29,6 +30,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; import { HOOKS_BY_TARGET } from '../hookTypes.js'; +import { PromptsConfig } from '../config/config.js'; import { GithubPromptHeaderAttributes } from './promptFileAttributes.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -40,7 +42,8 @@ export class PromptValidator { @IChatModeService private readonly chatModeService: IChatModeService, @IFileService private readonly fileService: IFileService, @ILabelService private readonly labelService: ILabelService, - @IPromptsService private readonly promptsService: IPromptsService + @IPromptsService private readonly promptsService: IPromptsService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { } public async validate(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { @@ -192,7 +195,9 @@ export class PromptValidator { this.validateUserInvokable(attributes, report); this.validateDisableModelInvocation(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, target, report); - this.validateHooks(attributes, target, report); + if (this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + this.validateHooks(attributes, target, report); + } if (isVSCodeOrDefaultTarget(target)) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); @@ -215,11 +220,21 @@ export class PromptValidator { } private checkForInvalidArguments(attributes: IHeaderAttribute[], promptType: PromptsType, target: Target, report: (markers: IMarkerData) => void): void { - const validAttributeNames = getValidAttributeNames(promptType, true, target); + let validAttributeNames = getValidAttributeNames(promptType, true, target); + if (!this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) { + validAttributeNames = validAttributeNames.filter(name => name !== PromptHeaderAttributes.hooks); + } + const useCustomAgentHooks = this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS); const validGithubCopilotAttributeNames = new Lazy(() => new Set(getValidAttributeNames(promptType, false, Target.GitHubCopilot))); for (const attribute of attributes) { if (!validAttributeNames.includes(attribute.key)) { - const supportedNames = new Lazy(() => getValidAttributeNames(promptType, false, target).sort().join(', ')); + const supportedNames = new Lazy(() => { + let names = getValidAttributeNames(promptType, false, target); + if (!useCustomAgentHooks) { + names = names.filter(name => name !== PromptHeaderAttributes.hooks); + } + return names.sort().join(', '); + }); switch (promptType) { case PromptsType.prompt: report(toMarker(localize('promptValidator.unknownAttribute.prompt', "Attribute '{0}' is not supported in prompt files. Supported: {1}.", attribute.key, supportedNames.value), attribute.range, MarkerSeverity.Warning)); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 688ad88d715ca..63f3e0b0e418d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -193,7 +193,12 @@ export class PromptsService extends Disposable implements IPromptsService { const modelChangeEvent = this._register(new ModelChangeTracker(this.modelService)).onDidPromptChange; this.cachedCustomAgents = this._register(new CachedPromise( (token) => this.computeCustomAgents(token), - () => Event.any(this.getFileLocatorEvent(PromptsType.agent), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent), this._onDidContributedWhenChange.event) + () => Event.any( + this.getFileLocatorEvent(PromptsType.agent), + Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent), + this._onDidContributedWhenChange.event, + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)), + ) )); this.cachedSlashCommands = this._register(new CachedPromise( @@ -742,8 +747,9 @@ export class PromptsService extends Disposable implements IPromptsService { // Parse hooks from the frontmatter if present let hooks: ChatRequestHooks | undefined; + const useCustomAgentHooks = this.configurationService.getValue(PromptsConfig.USE_CUSTOM_AGENT_HOOKS); const hooksRaw = ast.header.hooksRaw; - if (hooksRaw) { + if (useCustomAgentHooks && hooksRaw) { const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder; const workspaceRootUri = hookWorkspaceFolder?.uri; hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index 4ba131c89506a..101e3235aeabe 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -37,6 +37,7 @@ suite('PromptHeaderAutocompletion', () => { setup(async () => { const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true); instaService = workbenchInstantiationService({ contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), configurationService: () => testConfigService diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts index f90f6d4570794..ab2a3b4067c0d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -37,6 +37,7 @@ suite('PromptHoverProvider', () => { setup(async () => { const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true); instaService = workbenchInstantiationService({ contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), configurationService: () => testConfigService diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index e1ad97bb79165..be2df9c04566b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -41,6 +41,7 @@ suite('PromptValidator', () => { const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true); instaService = workbenchInstantiationService({ contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), configurationService: () => testConfigService From 9a207cb6962daf8a7c2fb061e056c8072a78e573 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 4 Mar 2026 16:51:57 -0800 Subject: [PATCH 16/21] chat: fix undo/redo skipping multiple no-edit requests (#299330) - Fixes _willRedoToEpoch to advance one request at a time when there are no edit operations ahead, instead of jumping past all remaining requests - When redoing with no operations in the queue, now finds the next request-start checkpoint boundary and advances there, following the same single-step pattern as undo - Adds test case verifying undo and redo step through consecutive no-edit requests one at a time Fixes #275234 (Commit message generated by Copilot) --- .../chatEditingCheckpointTimelineImpl.ts | 14 +++++- .../chatEditingCheckpointTimeline.test.ts | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts index 0877eac0fd6af..500774bfeb898 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCheckpointTimelineImpl.ts @@ -123,7 +123,19 @@ export class ChatEditingCheckpointTimelineImpl implements IChatEditingCheckpoint // Find the next edit operation that would be applied... const nextOperation = operations.find(op => op.epoch >= currentEpoch); - const nextCheckpoint = nextOperation && checkpoints.find(op => op.epoch > nextOperation.epoch); + + // When there are no more operations, advance one request at a time + // by finding the next request-start checkpoint boundary. + if (!nextOperation) { + const nextRequestStart = checkpoints.find(cp => cp.epoch >= currentEpoch && cp.undoStopId === undefined); + if (!nextRequestStart) { + return maxEncounteredEpoch + 1; + } + const requestAfter = checkpoints.find(cp => cp.epoch > nextRequestStart.epoch && cp.undoStopId === undefined); + return requestAfter ? requestAfter.epoch : (maxEncounteredEpoch + 1); + } + + const nextCheckpoint = checkpoints.find(op => op.epoch > nextOperation.epoch); // And figure out where we're going if we're navigating across request // 1. If there is no next request or if the next target checkpoint is in diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts index 57478466f4f5f..f6f3af2a699e0 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts @@ -1248,6 +1248,54 @@ suite('ChatEditingCheckpointTimeline', function () { await timeline.navigateToCheckpoint(stop2NewCheckpointId); assert.strictEqual(fileContents.get(uri), 'replacement edit', 'Content should still be correct after full timeline traversal'); }); + + test('undo/redo with multiple no-edit requests advances one request at a time', async function () { + // req1: no edits + timeline.createCheckpoint('req1', undefined, 'Start req1'); + + // req2: no edits + timeline.createCheckpoint('req2', undefined, 'Start req2'); + + // req3: no edits + timeline.createCheckpoint('req3', undefined, 'Start req3'); + + // req4: no edits + timeline.createCheckpoint('req4', undefined, 'Start req4'); + + // Undo should step one request at a time + assert.strictEqual(timeline.canUndo.get(), true); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4']); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3']); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3', 'req2']); + + await timeline.undoToLastCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3', 'req2', 'req1']); + + assert.strictEqual(timeline.canUndo.get(), false); + + // Redo should also step one request at a time (not skip all at once) + assert.strictEqual(timeline.canRedo.get(), true); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3', 'req2']); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4', 'req3']); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), ['req4']); + + await timeline.redoToNextCheckpoint(); + assert.deepStrictEqual(timeline.requestDisablement.get().map(d => d.requestId), []); + + assert.strictEqual(timeline.canRedo.get(), false); + }); }); // Mock notebook service for tests that don't need notebook functionality From f6fa90787236c36c2028ce9a62116568858e3c5a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Mar 2026 12:01:36 +1100 Subject: [PATCH 17/21] fix: Display chat session contributed models and ignore inline chat as they don't apply (#299335) --- .../chat/browser/widget/input/chatModelSelectionLogic.ts | 4 +--- .../test/browser/widget/input/chatModelSelectionLogic.test.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts index 62dbca81dc303..206a3d5476563 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts @@ -31,9 +31,7 @@ export function filterModelsForSession( if (sessionType && sessionType !== 'local' && hasModelsTargetingSession(models, sessionType)) { return models.filter(entry => entry.metadata?.targetChatSessionType === sessionType && - entry.metadata?.isUserSelectable && - isModelSupportedForMode(entry, currentModeKind) && - isModelSupportedForInlineChat(entry, location, isInlineChatV2Enabled) + entry.metadata?.isUserSelectable ); } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts index d282483d7d765..581f0db3fb30e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts @@ -239,7 +239,7 @@ suite('ChatModelSelectionLogic', () => { assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o']); }); - test('filters by mode for session-targeted models', () => { + test.skip('filters by mode for session-targeted models', () => { const cloudNoTools = createSessionModel('cloud-basic', 'Cloud Basic', 'cloud', { capabilities: { toolCalling: false, agentMode: false }, }); From 736ef2e05d6788bdd4a6ecfb981925e17e7cf3d6 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:06:06 -0800 Subject: [PATCH 18/21] Add 'launch' skill for VS Code UI automation via agent-browser (#299258) --- .agents/skills/launch/SKILL.md | 291 ++++ .claude/skills/launch | 1 + package-lock.json | 2311 ++++++++++++++++++++++++++++++-- package.json | 1 + 4 files changed, 2496 insertions(+), 108 deletions(-) create mode 100644 .agents/skills/launch/SKILL.md create mode 120000 .claude/skills/launch diff --git a/.agents/skills/launch/SKILL.md b/.agents/skills/launch/SKILL.md new file mode 100644 index 0000000000000..e1b425e497db4 --- /dev/null +++ b/.agents/skills/launch/SKILL.md @@ -0,0 +1,291 @@ +--- +name: launch +description: "Launch and automate VS Code (Code OSS) using agent-browser via Chrome DevTools Protocol. Use when you need to interact with the VS Code UI, automate the chat panel, test UI features, or take screenshots of VS Code. Triggers include 'automate VS Code', 'interact with chat', 'test the UI', 'take a screenshot', 'launch Code OSS with debugging'." +metadata: + allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*) +--- + +# VS Code Automation + +Automate VS Code (Code OSS) using agent-browser. VS Code is built on Electron/Chromium and exposes a Chrome DevTools Protocol (CDP) port that agent-browser can connect to, enabling the same snapshot-interact workflow used for web pages. + +## Prerequisites + +- **`agent-browser` must be installed.** It's listed in devDependencies — run `npm install` in the repo root. Use `npx agent-browser` if it's not on your PATH, or install globally with `npm install -g agent-browser`. +- **For Code OSS (VS Code dev build):** The repo must be built before launching. `./scripts/code.sh` runs the build automatically if needed, or set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built. +- **CSS selectors are internal implementation details.** Selectors like `.interactive-input-part`, `.interactive-input-editor`, and `.part.auxiliarybar` used in `eval` commands are VS Code internals that may change across versions. If they stop working, use `agent-browser snapshot -i` to re-discover the current DOM structure. + +## Core Workflow + +1. **Launch** Code OSS with remote debugging enabled +2. **Connect** agent-browser to the CDP port +3. **Snapshot** to discover interactive elements +4. **Interact** using element refs +5. **Re-snapshot** after navigation or state changes + +```bash +# Launch Code OSS with remote debugging +./scripts/code.sh --remote-debugging-port=9224 + +# Wait for Code OSS to start, retry until connected +for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done + +# Discover UI elements +agent-browser snapshot -i + +# Focus the chat input (macOS) +agent-browser press Control+Meta+i +``` + +## Connecting + +```bash +# Connect to a specific port +agent-browser connect 9222 + +# Or use --cdp on each command +agent-browser --cdp 9222 snapshot -i + +# Auto-discover a running Chromium-based app +agent-browser --auto-connect snapshot -i +``` + +After `connect`, all subsequent commands target the connected app without needing `--cdp`. + +## Tab Management + +Electron apps often have multiple windows or webviews. Use tab commands to list and switch between them: + +```bash +# List all available targets (windows, webviews, etc.) +agent-browser tab + +# Switch to a specific tab by index +agent-browser tab 2 + +# Switch by URL pattern +agent-browser tab --url "*settings*" +``` + +## Launching Code OSS (VS Code Dev Build) + +The VS Code repository includes `scripts/code.sh` which launches Code OSS from source. It passes all arguments through to the Electron binary, so `--remote-debugging-port` works directly: + +```bash +cd # the root of your VS Code checkout +./scripts/code.sh --remote-debugging-port=9224 +``` + +Wait for the window to fully initialize, then connect: + +```bash +# Wait for Code OSS to start, retry until connected +for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done +agent-browser snapshot -i +``` + +**Tips:** +- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built: `VSCODE_SKIP_PRELAUNCH=1 ./scripts/code.sh --remote-debugging-port=9224` (from the repo root) +- Code OSS uses the default user data directory. Unlike VS Code Insiders, you don't typically need `--user-data-dir` since there's usually only one Code OSS instance running. +- If you see "Sent env to running instance. Terminating..." it means Code OSS is already running and forwarded your args to the existing instance. Quit Code OSS and relaunch with the flag, or use `--user-data-dir=/tmp/code-oss-debug` to force a new instance. + +## Launching VS Code Extensions for Debugging + +To debug a VS Code extension via agent-browser, launch VS Code Insiders with `--extensionDevelopmentPath` and `--remote-debugging-port`. Use `--user-data-dir` to avoid conflicting with an already-running instance. + +```bash +# Build the extension first +cd # e.g., the root of your extension checkout +npm run compile + +# Launch VS Code Insiders with the extension and CDP +code-insiders \ + --extensionDevelopmentPath="" \ + --remote-debugging-port=9223 \ + --user-data-dir=/tmp/vscode-ext-debug + +# Wait for VS Code to start, retry until connected +for i in 1 2 3 4 5; do agent-browser connect 9223 2>/dev/null && break || sleep 3; done +agent-browser snapshot -i +``` + +**Key flags:** +- `--extensionDevelopmentPath=` — loads your extension from source (must be compiled first) +- `--remote-debugging-port=9223` — enables CDP (use 9223 to avoid conflicts with other apps on 9222) +- `--user-data-dir=` — uses a separate profile so it starts a new process instead of sending to an existing VS Code instance + +**Without `--user-data-dir`**, VS Code detects the running instance, forwards the args to it, and exits immediately — you'll see "Sent env to running instance. Terminating..." and CDP never starts. + +## Interacting with Monaco Editor (Chat Input, Code Editors) + +VS Code uses Monaco Editor for all text inputs including the Copilot Chat input. Monaco editors require specific agent-browser techniques — standard `click`, `fill`, and `keyboard type` commands may not work depending on the VS Code build. + +### The Universal Pattern: Focus via Keyboard Shortcut + `press` + +This works on **all** VS Code builds (Code OSS, Insiders, stable): + +```bash +# 1. Open and focus the chat input with the keyboard shortcut +# macOS: +agent-browser press Control+Meta+i +# Linux / Windows: +agent-browser press Control+Alt+i + +# 2. Type using individual press commands +agent-browser press H +agent-browser press e +agent-browser press l +agent-browser press l +agent-browser press o +agent-browser press Space # Use "Space" for spaces +agent-browser press w +agent-browser press o +agent-browser press r +agent-browser press l +agent-browser press d + +# Verify text appeared (optional) +agent-browser eval ' +(() => { + const sidebar = document.querySelector(".part.auxiliarybar"); + const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line"); + return Array.from(viewLines).map(vl => vl.textContent).join("|"); +})()' + +# 3. Send the message (same on all platforms) +agent-browser press Enter +``` + +**Chat focus shortcut by platform:** +- **macOS:** `Ctrl+Cmd+I` → `agent-browser press Control+Meta+i` +- **Linux:** `Ctrl+Alt+I` → `agent-browser press Control+Alt+i` +- **Windows:** `Ctrl+Alt+I` → `agent-browser press Control+Alt+i` + +This shortcut focuses the chat input and sets `document.activeElement` to a `DIV` with class `native-edit-context` — VS Code's native text editing surface that correctly processes key events from `agent-browser press`. + +### `type @ref` — Works on Some Builds + +On VS Code Insiders (extension debug mode), `type @ref` handles focus and input in one step: + +```bash +agent-browser snapshot -i +# Look for: textbox "The editor is not accessible..." [ref=e62] +agent-browser type @e62 "Hello from George!" +``` + +However, **`type @ref` silently fails on Code OSS** — the command completes without error but no text appears. This also applies to `keyboard type` and `keyboard inserttext`. Always verify text appeared after typing, and fall back to the keyboard shortcut + `press` pattern if it didn't. The `press`-per-key approach works universally across all builds. + +### Compatibility Matrix + +| Method | VS Code Insiders | Code OSS | +|--------|-----------------|----------| +| `press` per key (after focus shortcut) | ✅ Works | ✅ Works | +| `type @ref` | ✅ Works | ❌ Silent fail | +| `keyboard type` (after focus) | ✅ Works | ❌ Silent fail | +| `keyboard inserttext` (after focus) | ✅ Works | ❌ Silent fail | +| `click @ref` | ❌ Blocked by overlay | ❌ Blocked by overlay | +| `fill @ref` | ❌ Element not visible | ❌ Element not visible | + +### Fallback: Focus via JavaScript Mouse Events + +If the keyboard shortcut doesn't work (e.g., chat panel isn't configured), you can focus the editor via JavaScript: + +```bash +agent-browser eval ' +(() => { + const inputPart = document.querySelector(".interactive-input-part"); + const editor = inputPart.querySelector(".monaco-editor"); + const rect = editor.getBoundingClientRect(); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + editor.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: x, clientY: y })); + editor.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX: x, clientY: y })); + editor.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: x, clientY: y })); + return "activeElement: " + document.activeElement?.className; +})()' + +# Then use press for each character +agent-browser press H +agent-browser press e +# ... +``` + +### Verifying Text and Clearing + +```bash +# Verify text in the chat input +agent-browser eval ' +(() => { + const sidebar = document.querySelector(".part.auxiliarybar"); + const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line"); + return Array.from(viewLines).map(vl => vl.textContent).join("|"); +})()' + +# Clear the input (Select All + Backspace) +# macOS: +agent-browser press Meta+a +# Linux / Windows: +agent-browser press Control+a +# Then delete: +agent-browser press Backspace +``` + +### Screenshot Tips for VS Code + +On ultrawide monitors, the chat sidebar may be in the far-right corner of the CDP screenshot. Options: +- Use `agent-browser screenshot --full` to capture the entire window +- Use element screenshots: `agent-browser screenshot ".part.auxiliarybar" sidebar.png` +- Use `agent-browser screenshot --annotate` to see labeled element positions +- Maximize the sidebar first: click the "Maximize Secondary Side Bar" button + +> **macOS:** If `agent-browser screenshot` returns "Permission denied", your terminal needs Screen Recording permission. Grant it in **System Settings → Privacy & Security → Screen Recording**. As a fallback, use the `eval` verification snippet to confirm text was entered — this doesn't require screen permissions. + +## Troubleshooting + +### "Connection refused" or "Cannot connect" + +- Make sure Code OSS was launched with `--remote-debugging-port=NNNN` +- If Code OSS was already running, quit and relaunch with the flag +- Check that the port isn't in use by another process: + - macOS / Linux: `lsof -i :9224` + - Windows: `netstat -ano | findstr 9224` + +### Elements not appearing in snapshot + +- VS Code uses multiple webviews. Use `agent-browser tab` to list targets and switch to the right one +- Use `agent-browser snapshot -i -C` to include cursor-interactive elements (divs with onclick handlers) + +### Cannot type in Monaco Editor inputs + +- Use `agent-browser press` for individual keystrokes after focusing the input. Focus the chat input with the keyboard shortcut (macOS: `Ctrl+Cmd+I`, Linux/Windows: `Ctrl+Alt+I`). +- `type @ref`, `keyboard type`, and `keyboard inserttext` work on VS Code Insiders but **silently fail on Code OSS** — they complete without error but no text appears. The `press`-per-key approach works universally. +- See the "Interacting with Monaco Editor" section above for the full compatibility matrix. + +## Cleanup / Disconnect + +> **⚠️ IMPORTANT: Always quit Code OSS when you're done.** Code OSS is a full Electron app that consumes significant memory (often 1–4 GB+). Leaving it running in the background will slow your machine considerably. Don't just disconnect agent-browser — **kill the Code OSS process too.** + +```bash +# 1. Disconnect agent-browser +agent-browser close + +# 2. QUIT Code OSS — do not leave it running! +# macOS: Cmd+Q in the app window, or: +# Find the process +lsof -i :9224 | grep LISTEN +# Kill it (replace with the actual PID) +kill + +# Linux: +# kill $(lsof -t -i :9224) + +# Windows: +# taskkill /F /PID +# Or use Task Manager to end "Code - OSS" +``` + +If you launched with `./scripts/code.sh`, the process name is `Electron` or `Code - OSS`. Verify it's gone: +```bash +# Confirm no process is listening on the debug port +lsof -i :9224 # should return nothing +``` diff --git a/.claude/skills/launch b/.claude/skills/launch new file mode 120000 index 0000000000000..b41e2b420ad03 --- /dev/null +++ b/.claude/skills/launch @@ -0,0 +1 @@ +../../.agents/skills/launch \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d031af36fdd4c..e8dfe47faceaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.19", "@webgpu/types": "^0.1.66", + "agent-browser": "^0.16.3", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", @@ -202,6 +203,37 @@ "node": ">=18" } }, + "node_modules/@appium/logger": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@appium/logger/-/logger-1.7.1.tgz", + "integrity": "sha512-9C2o9X/lBEDBUnKfAi3mRo9oG7Z03nmISLwsGkWxIWjMAvBdJD0RRSJMekWVKzfXN3byrI1WlCXTITzN4LAoLw==", + "dev": true, + "license": "ISC", + "dependencies": { + "console-control-strings": "1.1.0", + "lodash": "4.17.21", + "lru-cache": "10.4.3", + "set-blocking": "2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=8" + } + }, + "node_modules/@appium/logger/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@appium/logger/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@azure-rest/ai-translation-text": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.0-beta.1.tgz", @@ -1284,15 +1316,6 @@ "node": ">=18.0.0" } }, - "node_modules/@isaacs/fs-minipass/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2115,6 +2138,58 @@ "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", "license": "MIT" }, + "node_modules/@promptbook/utils": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", + "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "CC-BY-4.0", + "dependencies": { + "spacetrim": "0.11.59" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2218,6 +2293,13 @@ "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2496,6 +2578,13 @@ "@types/sinon": "*" } }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/svgo": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@types/svgo/-/svgo-1.3.6.tgz", @@ -2534,6 +2623,13 @@ "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", "dev": true }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.7", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", @@ -2553,6 +2649,16 @@ "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg= sha512-c4m/hnOI1j34i8hXlkZzelE6SXfOqaTWhBp0UgBuwmpiafh22OpsE261Rlg//agZtQHIY5cMgbkX8bnthUFrmA==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -3380,16 +3486,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/l10n-dev/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@vscode/native-watchdog": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@vscode/native-watchdog/-/native-watchdog-1.4.6.tgz", @@ -3582,16 +3678,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/test-cli/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@vscode/test-electron": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", @@ -3731,6 +3817,224 @@ "hasInstallScript": true, "license": "MIT" }, + "node_modules/@wdio/config": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.24.0.tgz", + "integrity": "sha512-rcHu0eG16rSEmHL0sEKDcr/vYFmGhQ5GOlmlx54r+1sgh6sf136q+kth4169s16XqviWGW3LjZbUfpTK29pGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.24.0", + "@wdio/utils": "9.24.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@wdio/config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.24.0.tgz", + "integrity": "sha512-ozQKYddBLT4TRvU9J+fGrhVUtx3iDAe+KNCJcTDMFMxNSdDMR2xFQdNp8HLHypspk58oXTYCvz6ZYjySthhqsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/types": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.24.0.tgz", + "integrity": "sha512-PYYunNl8Uq1r8YMJAK6ReRy/V/XIrCSyj5cpCtR5EqCL6heETOORFj7gt4uPnzidfgbtMBcCru0LgjjlMiH1UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/utils": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.24.0.tgz", + "integrity": "sha512-6WhtzC5SNCGRBTkaObX6A07Ofnnyyf+TQH/d/fuhZRqvBknrP4AMMZF+PFxGl1fwdySWdBn+gV2QLE+52Byowg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.24.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^6.1.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/utils/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", @@ -3866,12 +4170,37 @@ "addons/*" ] }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.22", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.22.tgz", + "integrity": "sha512-0KlzbVR6r8irIX2o3zvUlosBDef62VDl47oUfa1U/qgEs67h4/eGBrX/6HWa1RQbt+J6sAeVmtyFKbTHNdF8qQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3917,6 +4246,37 @@ "node": ">= 14" } }, + "node_modules/agent-browser": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.16.3.tgz", + "integrity": "sha512-dsg8PTJNBIQ7/LPp/La42KQwLTzsP8sudbCLpP1atsJXps4Fbuz1CeepUJAGrgxb8koc9y4yKobYVPAsds8hPQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-simctl": "^7.4.0", + "playwright-core": "^1.57.0", + "webdriverio": "^9.15.0", + "ws": "^8.19.0", + "zod": "^3.22.4" + }, + "bin": { + "agent-browser": "bin/agent-browser.js" + } + }, + "node_modules/agent-browser/node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -4053,9 +4413,264 @@ "node": ">=0.10.0" } }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, @@ -4074,6 +4689,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -4307,6 +4932,26 @@ "node": ">=0.10.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -4340,6 +4985,21 @@ "node": ">= 0.10" } }, + "node_modules/asyncbox": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-3.0.0.tgz", + "integrity": "sha512-X7U0nedUMKV3nn9c4R0Zgvdvv6cw97tbDlHSZicq1snGPi/oX9DgGmFSURWtxDdnBWd3V0YviKhqAYAVvoWQ/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bluebird": "^3.5.1", + "lodash": "^4.17.4", + "source-map-support": "^0.x" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4550,6 +5210,16 @@ "node": ">= 0.8" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -4590,6 +5260,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -4972,6 +5649,60 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5423,6 +6154,109 @@ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5469,6 +6303,13 @@ "proto-list": "~1.2.1" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -5578,6 +6419,106 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5682,6 +6623,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-shorthand-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "dev": true, + "license": "MIT" + }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -5695,6 +6643,12 @@ "node": ">=8.0.0" } }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -5729,6 +6683,16 @@ "type": "^1.0.1" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debounce": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", @@ -5874,6 +6838,16 @@ "node": ">=4.0.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -5986,6 +6960,21 @@ "node": ">=0.10.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.6.tgz", @@ -6117,10 +7106,11 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -6168,44 +7158,124 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "node_modules/each-props/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edgedriver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.3.0.tgz", + "integrity": "sha512-ggEQL+oEyIcM4nP2QC3AtCQ04o4kDNefRM3hja0odvlPSnsaxiruMxEZ93v3gDCKWYW6BXUr51PPradb+3nffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^5.3.3", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "which": "^6.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/edgedriver/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "node_modules/edgedriver/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "dependencies": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" } }, - "node_modules/each-props/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/edgedriver/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, + "license": "ISC", "dependencies": { - "isobject": "^3.0.1" + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, "node_modules/editorconfig": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz", @@ -6299,6 +7369,33 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6490,6 +7587,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.36.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", @@ -6710,6 +7829,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -6787,11 +7920,22 @@ "through": "~2.3.1" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } @@ -7308,6 +8452,39 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz", + "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", @@ -7874,6 +9051,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geckodriver": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz", + "integrity": "sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "modern-tar": "^0.7.2" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/geckodriver/node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7917,6 +9129,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -7965,6 +9190,21 @@ "once": "^1.3.1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -8524,6 +9764,13 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true, + "license": "MIT" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -10008,6 +11255,46 @@ "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", "dev": true }, + "node_modules/htmlfy": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -10278,6 +11565,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -10909,6 +12207,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -11491,6 +12799,41 @@ "uc.micro": "^2.0.0" } }, + "node_modules/locate-app": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", + "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@promptbook/utils": "0.69.5", + "type-fest": "4.26.0", + "userhome": "1.0.1" + } + }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11544,6 +12887,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11560,6 +12910,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12069,12 +13440,12 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { @@ -12089,14 +13460,12 @@ "node": ">= 18" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" }, "node_modules/mixin-deep": { "version": "1.3.2", @@ -12333,9 +13702,19 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, - "license": "ISC", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/modern-tar": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.5.tgz", + "integrity": "sha512-YTefgdpKKFgoTDbEUqXqgUJct2OG6/4hs4XWLsxcHkDLj/x/V8WmKIRppPnXP5feQ7d1vuYWSp3qKkxfwaFaxA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18.0.0" } }, "node_modules/morgan": { @@ -12497,6 +13876,16 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -12610,6 +13999,133 @@ "dev": true, "license": "MIT" }, + "node_modules/node-simctl": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-7.7.5.tgz", + "integrity": "sha512-lWflzDW9xLuOOvR6mTJ9efbDtO/iSCH6rEGjxFxTV0vGgz5XjoZlW2BkNCCZib0B6Y23tCOiYhYJaMQYB8FKIQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@appium/logger": "^1.3.0", + "asyncbox": "^3.0.0", + "bluebird": "^3.5.1", + "lodash": "^4.2.1", + "rimraf": "^5.0.0", + "semver": "^7.0.0", + "source-map-support": "^0.x", + "teen_process": "^2.2.0", + "uuid": "^11.0.1", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=8" + } + }, + "node_modules/node-simctl/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-simctl/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-simctl/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-simctl/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-simctl/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-simctl/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/node-simctl/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", @@ -13324,6 +14840,40 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -13395,6 +14945,59 @@ "node": ">=0.10.0" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13864,6 +15467,36 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -13965,6 +15598,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true, + "license": "MIT" + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -14225,6 +15865,39 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -14561,6 +16234,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -14602,6 +16292,13 @@ "node": ">=0.10.0" } }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -14708,20 +16405,60 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + ] + }, + "node_modules/safaridriver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.1.tgz", + "integrity": "sha512-jkg4434cYgtrIF2AeY/X0Wmd2W73cK5qIEFE3hDrrQenJH/2SDJIXGvPAigfvQTcE9+H31zkiNHbUqcihEiMRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4= sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4= sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "node_modules/safe-regex2/node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", "dev": true, - "dependencies": { - "ret": "~0.1.10" + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/safer-buffer": { @@ -14748,9 +16485,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -15436,11 +17173,12 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" }, @@ -15496,6 +17234,23 @@ "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, + "node_modules/spacetrim": { + "version": "0.11.59", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", + "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0" + }, "node_modules/sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", @@ -15561,6 +17316,16 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -15930,6 +17695,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -16168,15 +17946,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -16195,6 +17964,23 @@ "node": ">=22" } }, + "node_modules/teen_process": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-2.3.3.tgz", + "integrity": "sha512-NIdeetf/6gyEqLjnzvfgQe7PfipSceq2xDQM2Py2BkBnIIeWh3HRD3vNhulyO5WppfCv9z4mtsEHyq8kdiULTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bluebird": "^3.7.2", + "lodash": "^4.17.21", + "shell-quote": "^1.8.1", + "source-map-support": "^0.x" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0", + "npm": ">=8" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -17027,6 +18813,13 @@ "requires-port": "^1.0.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -17036,6 +18829,16 @@ "node": ">=0.10.0" } }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -17314,18 +19117,200 @@ "dev": true, "license": "MIT" }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", "integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==", "dev": true }, + "node_modules/webdriver": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.24.0.tgz", + "integrity": "sha512-2R31Ey83NzMsafkl4hdFq6GlIBvOODQMkueLjeRqYAITu3QCYiq9oqBdnWA6CdePuV4dbKlYsKRX0mwMiPclDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.24.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.24.0", + "@wdio/types": "9.24.0", + "@wdio/utils": "9.24.0", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriver/node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/webdriverio": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.24.0.tgz", + "integrity": "sha512-LTJt6Z/iDM0ne/4ytd3BykoPv9CuJ+CAILOzlwFeMGn4Mj02i4Bk2Rg9o/jeJ89f52hnv4OPmNjD0e8nzWAy5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.24.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.24.0", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.24.0", + "@wdio/utils": "9.24.0", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.24.0" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriverio/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriverio/node_modules/serialize-error": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.31.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -17483,6 +19468,28 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -17657,6 +19664,94 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 7567da0e40ba2..8291e70e33499 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.19", "@webgpu/types": "^0.1.66", + "agent-browser": "^0.16.3", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", From 79af825700d599ce667996d752706bbe2a21c487 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:38:04 -0800 Subject: [PATCH 19/21] permissions picker warnings and improved hover (#299331) add warnings when switching, setting based, better picker --- .../actionWidget/browser/actionList.ts | 12 ++- .../actionWidget/browser/actionWidget.css | 25 +++++++ .../browser/widget/input/chatInputPart.ts | 4 +- .../input/permissionPickerActionItem.ts | 75 +++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index a871c7a2b8fb0..71f8ee665a212 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -369,6 +369,12 @@ export interface IActionListOptions { */ readonly minWidth?: number; + /** + * When true, descriptions are rendered as subtext below the title + * instead of inline to the right. + */ + readonly descriptionBelow?: boolean; + /** @@ -383,7 +389,7 @@ export class ActionList extends Disposable { private readonly _list: List>; - private readonly _actionLineHeight = 24; + private readonly _actionLineHeight: number; private readonly _headerLineHeight = 24; private readonly _separatorLineHeight = 8; @@ -431,6 +437,10 @@ export class ActionList extends Disposable { super(); this.domNode = document.createElement('div'); this.domNode.classList.add('actionList'); + if (this._options?.descriptionBelow) { + this.domNode.classList.add('description-below'); + } + this._actionLineHeight = this._options?.descriptionBelow ? 48 : 24; // Initialize collapsed sections if (this._options?.collapsedByDefault) { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 74c89c2c486a9..3c2022bd29dc3 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -217,6 +217,31 @@ font-size: 12px; } +/* Description below mode — shows descriptions as subtext under the title */ +.action-widget .description-below .monaco-list .monaco-list-row.action { + flex-wrap: wrap; + align-content: center; + padding-top: 6px; + padding-right: 2px; + + .title { + line-height: 14px; + } + + .description { + display: block !important; + width: 100%; + margin-left: 0; + padding-left: 18px; + font-size: 11px; + line-height: 14px; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + /* Item toolbar - shows on hover/focus */ .action-widget .monaco-list-row.action .action-list-item-toolbar { display: none; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index db90c77372dcd..45f4a15d64840 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -904,8 +904,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.selectedToolsModel.resetSessionEnablementState(); this._chatSessionIsEmpty = chatSessionIsEmpty; - // Reset permission level to default on new sessions - if (chatSessionIsEmpty) { + // Reset permission level on new sessions, unless global auto-approve is on + if (chatSessionIsEmpty && !this.configurationService.getValue(ChatConfiguration.GlobalAutoApprove)) { this._currentPermissionLevel.set(ChatPermissionLevel.Default, undefined); this.permissionLevelKey.set(ChatPermissionLevel.Default); this.permissionWidget?.refresh(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 5f0cc0988d2a3..26f6954832c0f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -18,8 +18,26 @@ import { ITelemetryService } from '../../../../../../platform/telemetry/common/t import { ChatConfiguration, ChatPermissionLevel } from '../../../common/constants.js'; import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; +import Severity from '../../../../../../base/common/severity.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +// Track whether warnings have been shown this VS Code session +const shownWarnings = new Set(); + +function hasShownElevatedWarning(level: ChatPermissionLevel): boolean { + if (shownWarnings.has(level)) { + return true; + } + // Autopilot is stricter than AutoApprove, so confirming Autopilot + // implies the user already accepted the AutoApprove risks. + if (level === ChatPermissionLevel.AutoApprove && shownWarnings.has(ChatPermissionLevel.Autopilot)) { + return true; + } + return false; +} + export interface IPermissionPickerDelegate { readonly currentPermissionLevel: IObservable; readonly setPermissionLevel: (level: ChatPermissionLevel) => void; @@ -35,6 +53,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService, ) { const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; const isAutopilotEnabled = () => configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; @@ -47,6 +66,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.default', label: localize('permissions.default', "Default Approvals"), + description: localize('permissions.default.subtext', "Copilot uses your configured settings"), icon: ThemeIcon.fromId(Codicon.shield.id), checked: currentLevel === ChatPermissionLevel.Default, tooltip: '', @@ -65,6 +85,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.autoApprove', label: localize('permissions.autoApprove', "Bypass Approvals"), + description: localize('permissions.autoApprove.subtext', "All tool calls are auto-approved"), icon: ThemeIcon.fromId(Codicon.warning.id), checked: currentLevel === ChatPermissionLevel.AutoApprove, enabled: !policyRestricted, @@ -76,6 +97,32 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { position: pickerOptions.hoverPosition }, run: async () => { + if (!hasShownElevatedWarning(ChatPermissionLevel.AutoApprove)) { + const result = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('permissions.autoApprove.warning.title', "Enable Bypass Approvals?"), + buttons: [ + { + label: localize('permissions.autoApprove.warning.confirm', "Enable"), + run: () => true + }, + { + label: localize('permissions.autoApprove.warning.cancel', "Cancel"), + run: () => false + }, + ], + custom: { + icon: Codicon.warning, + markdownDetails: [{ + markdown: new MarkdownString(localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.")), + }], + }, + }); + if (result.result !== true) { + return; + } + shownWarnings.add(ChatPermissionLevel.AutoApprove); + } delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove); if (this.element) { this.renderLabel(this.element); @@ -88,6 +135,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ...action, id: 'chat.permissions.autopilot', label: localize('permissions.autopilot', "Autopilot (Preview)"), + description: localize('permissions.autopilot.subtext', "Copilot handles it from start to finish"), icon: ThemeIcon.fromId(Codicon.rocket.id), checked: currentLevel === ChatPermissionLevel.Autopilot, enabled: !policyRestricted, @@ -99,6 +147,32 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { position: pickerOptions.hoverPosition }, run: async () => { + if (!hasShownElevatedWarning(ChatPermissionLevel.Autopilot)) { + const result = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('permissions.autopilot.warning.title', "Enable Autopilot?"), + buttons: [ + { + label: localize('permissions.autopilot.warning.confirm', "Enable"), + run: () => true + }, + { + label: localize('permissions.autopilot.warning.cancel', "Cancel"), + run: () => false + }, + ], + custom: { + icon: Codicon.rocket, + markdownDetails: [{ + markdown: new MarkdownString(localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.")), + }], + }, + }); + if (result.result !== true) { + return; + } + shownWarnings.add(ChatPermissionLevel.Autopilot); + } delegate.setPermissionLevel(ChatPermissionLevel.Autopilot); if (this.element) { this.renderLabel(this.element); @@ -113,6 +187,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { super(action, { actionProvider, reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, + listOptions: { descriptionBelow: true, minWidth: 232 }, }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); } From a3c86528c3b11f7eb62b2b9b4549dddb50deca6c Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:00:40 -0800 Subject: [PATCH 20/21] add telemetry for chat permissions on request (#299349) add telemetry --- .../contrib/chat/common/chatService/chatServiceTelemetry.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts index 2a0f2cf7cc8af..8dfb8f5333df3 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts @@ -11,7 +11,7 @@ import { ChatRequestModel, IChatRequestVariableData } from '../model/chatModel.j import { ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from '../requestParser/chatParserTypes.js'; import { ChatAgentVoteDirection, ChatCopyKind, IChatSendRequestOptions, IChatUserActionEvent } from './chatService.js'; import { isImageVariableEntry } from '../attachments/chatVariableEntries.js'; -import { ChatAgentLocation } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ILanguageModelsService } from '../languageModels.js'; import { chatSessionResourceToId } from '../model/chatUri.js'; @@ -149,6 +149,7 @@ export type ChatProviderInvokedEvent = { enableCommandDetection: boolean; attachmentKinds: string[]; model: string | undefined; + permissionLevel: ChatPermissionLevel | undefined; }; export type ChatProviderInvokedClassification = { @@ -167,6 +168,7 @@ export type ChatProviderInvokedClassification = { enableCommandDetection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether participation detection was disabled for this invocation.' }; attachmentKinds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The types of variables/attachments that the user included with their query.' }; model: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The model used to generate the response.' }; + permissionLevel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The tool auto-approval permission level selected in the permission picker (default, autoApprove, or autopilot). Undefined when the picker is not applicable (e.g. ask mode or API-driven requests).' }; owner: 'roblourens'; comment: 'Provides insight into the performance of Chat agents.'; }; @@ -303,6 +305,7 @@ export class ChatRequestTelemetry { numCodeBlocks: getCodeBlocks(request.response?.response.toString() ?? '').length, attachmentKinds: this.attachmentKindsForTelemetry(request.variableData), model: this.resolveModelId(this.opts.options?.userSelectedModelId), + permissionLevel: this.opts.options?.modeInfo?.kind === ChatModeKind.Ask ? undefined : this.opts.options?.modeInfo?.permissionLevel, }); } From efd866344598bf2bfc7eb686bbc33a5e798b3311 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:10 -0800 Subject: [PATCH 21/21] Customizations: Adjust search bar height in layout methods for consistency across widgets (#299333) Adjust search bar height in layout methods for consistency across widgets --- .../chat/browser/aiCustomization/aiCustomizationListWidget.ts | 2 +- .../contrib/chat/browser/aiCustomization/mcpListWidget.ts | 2 +- .../browser/aiCustomization/media/aiCustomizationManagement.css | 2 +- .../contrib/chat/browser/aiCustomization/pluginListWidget.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index e890fb82a04c6..8a77a8e52230e 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -1155,7 +1155,7 @@ export class AICustomizationListWidget extends Disposable { */ layout(height: number, width: number): void { const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; - const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 52; const listHeight = height - sectionFooterHeight - searchBarHeight; this.searchInput.layout(); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index e92639f9b953d..e16ecec8d5745 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -777,7 +777,7 @@ export class McpListWidget extends Disposable { */ layout(height: number, width: number): void { const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; - const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 52; const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index d9781fd7664f5..81651ef26dd81 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -166,7 +166,7 @@ align-items: center; gap: 8px; flex-shrink: 0; - margin: 6px 0px; + padding: 6px 0; } .ai-customization-list-widget .list-search-container, diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts index 52e409f290bd5..ff1135b9a58d4 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -795,7 +795,7 @@ export class PluginListWidget extends Disposable { layout(height: number, width: number): void { const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; - const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 52; const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight;