diff --git a/.agents/skills/launch/SKILL.md b/.agents/skills/launch/SKILL.md index f967f6d0c1d..20f207907ac 100644 --- a/.agents/skills/launch/SKILL.md +++ b/.agents/skills/launch/SKILL.md @@ -111,6 +111,30 @@ agent-browser snapshot -i - 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 the Sessions App (Agent Sessions Window) + +The Sessions app is a separate workbench mode launched with the `--sessions` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance. + +```bash +cd # the root of your VS Code checkout +./scripts/code.sh --sessions --remote-debugging-port=9224 +``` + +Wait for the window to fully initialize, then connect: + +```bash +# Wait for Sessions app 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 + +# Verify you're connected to the right target (not about:blank) +agent-browser tab +agent-browser snapshot -i +``` + +**Tips:** +- The `--sessions` flag launches the Agent Sessions workbench instead of the standard VS Code workbench. +- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built. + ## 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. diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 120000 index ff807266877..00000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -../.github/copilot-instructions.md \ No newline at end of file diff --git a/.claude/skills/launch b/.claude/skills/launch deleted file mode 120000 index b41e2b420ad..00000000000 --- a/.claude/skills/launch +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/launch \ No newline at end of file diff --git a/.eslint-plugin-local/code-no-icons-in-localized-strings.ts b/.eslint-plugin-local/code-no-icons-in-localized-strings.ts new file mode 100644 index 00000000000..8f4251dfd41 --- /dev/null +++ b/.eslint-plugin-local/code-no-icons-in-localized-strings.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; + +/** + * Prevents theme icon syntax `$(iconName)` from appearing inside localized + * string arguments. Localizers may translate or corrupt the icon syntax, + * breaking rendering. Icon references should be kept outside the localized + * string - either prepended via concatenation or passed as a placeholder + * argument. + * + * Examples: + * ❌ localize('key', "$(gear) Settings") + * ✅ '$(gear) ' + localize('key', "Settings") + * ✅ localize('key', "Like {0}", '$(gear)') + * + * ❌ nls.localize('key', "$(loading~spin) Loading...") + * ✅ '$(loading~spin) ' + nls.localize('key', "Loading...") + */ +export default new class NoIconsInLocalizedStrings implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + noIconInLocalizedString: 'Theme icon syntax $(…) should not appear inside localized strings. Move it outside the localize call or pass it as a placeholder argument.' + }, + docs: { + description: 'Prevents $(icon) theme icon syntax inside localize() string arguments', + }, + type: 'problem', + schema: false, + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + // Matches $(iconName) or $(iconName~modifier) but not escaped \$(...) + const iconPattern = /(? checkCallExpression(node as TSESTree.CallExpression) + }; + } +}; diff --git a/.eslint-plugin-local/code-no-static-node-module-import.ts b/.eslint-plugin-local/code-no-static-node-module-import.ts new file mode 100644 index 00000000000..674e5f9e6eb --- /dev/null +++ b/.eslint-plugin-local/code-no-static-node-module-import.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TSESTree } from '@typescript-eslint/typescript-estree'; +import * as eslint from 'eslint'; +import { builtinModules } from 'module'; +import { join, normalize, relative } from 'path'; +import minimatch from 'minimatch'; +import { createImportRuleListener } from './utils.ts'; + +const nodeBuiltins = new Set([ + ...builtinModules, + ...builtinModules.map(m => `node:${m}`) +]); + +const REPO_ROOT = normalize(join(import.meta.dirname, '../')); + +export default new class implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + staticImport: 'Static imports of \'{{module}}\' are not allowed here because they are loaded synchronously on startup. Use a dynamic `await import(...)` or `import type` instead.' + }, + docs: { + description: 'Disallow static imports of node_modules packages to prevent synchronous loading on startup. Allows Node.js built-ins, electron, relative imports, and whitelisted file paths.' + }, + schema: { + type: 'array', + items: { + type: 'string' + } + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + const allowedPaths = context.options as string[]; + const filePath = normalize(relative(REPO_ROOT, normalize(context.getFilename()))).replace(/\\/g, '/'); + + // Skip whitelisted files + if (allowedPaths.some(pattern => filePath === pattern || minimatch(filePath, pattern))) { + return {}; + } + + return createImportRuleListener((node, value) => { + // Allow `import type` and `export type` declarations + if (node.parent?.type === TSESTree.AST_NODE_TYPES.ImportDeclaration && node.parent.importKind === 'type') { + return; + } + if (node.parent && 'exportKind' in node.parent && node.parent.exportKind === 'type') { + return; + } + + // Allow relative imports + if (value.startsWith('.')) { + return; + } + + // Allow Node.js built-in modules + if (nodeBuiltins.has(value)) { + return; + } + + // Allow electron + if (value === 'electron') { + return; + } + + context.report({ + loc: node.parent!.loc, + messageId: 'staticImport', + data: { + module: value + } + }); + }); + } +}; diff --git a/.eslint-plugin-local/code-no-telemetry-common-property.ts b/.eslint-plugin-local/code-no-telemetry-common-property.ts new file mode 100644 index 00000000000..2627a09c0a4 --- /dev/null +++ b/.eslint-plugin-local/code-no-telemetry-common-property.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; + +const telemetryMethods = new Set(['publicLog', 'publicLog2', 'publicLogError', 'publicLogError2']); + +/** + * Common telemetry property names that are automatically added to every event. + * Telemetry events must not set these because they would collide with / be + * overwritten by the common properties that the telemetry pipeline injects. + * + * Collected from: + * - src/vs/platform/telemetry/common/commonProperties.ts (resolveCommonProperties) + * - src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts + * - src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts + */ +const commonTelemetryProperties = new Set([ + 'common.machineid', + 'common.sqmid', + 'common.devdeviceid', + 'sessionid', + 'commithash', + 'version', + 'common.releasedate', + 'common.platformversion', + 'common.platform', + 'common.nodeplatform', + 'common.nodearch', + 'common.product', + 'common.msftinternal', + 'timestamp', + 'common.timesincesessionstart', + 'common.sequence', + 'common.snap', + 'common.platformdetail', + 'common.version.shell', + 'common.version.renderer', + 'common.firstsessiondate', + 'common.lastsessiondate', + 'common.isnewsession', + 'common.remoteauthority', + 'common.cli', + 'common.useragent', + 'common.istouchdevice', +]); + +export default new class NoTelemetryCommonProperty implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + noCommonProperty: 'Telemetry events must not contain the common property "{{name}}". Common properties are automatically added by the telemetry pipeline and will be dropped.', + }, + schema: false, + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + /** + * Check whether any property key in an object expression is a reserved common telemetry property. + */ + function checkObjectForCommonProperties(node: ESTree.ObjectExpression) { + for (const prop of node.properties) { + if (prop.type === 'Property') { + let name: string | undefined; + if (prop.key.type === 'Identifier') { + name = prop.key.name; + } else if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') { + name = prop.key.value; + } + if (name && commonTelemetryProperties.has(name.toLowerCase())) { + context.report({ + node: prop.key, + messageId: 'noCommonProperty', + data: { name }, + }); + } + } + } + } + + return { + ['CallExpression[callee.property.type="Identifier"]'](node: ESTree.CallExpression) { + const callee = node.callee; + if (callee.type !== 'MemberExpression') { + return; + } + const prop = callee.property; + if (prop.type !== 'Identifier' || !telemetryMethods.has(prop.name)) { + return; + } + // The data argument is the second argument for publicLog/publicLog2/publicLogError/publicLogError2 + const dataArg = node.arguments[1]; + if (dataArg && dataArg.type === 'ObjectExpression') { + checkObjectForCommonProperties(dataArg); + } + }, + }; + } +}; diff --git a/.eslint-plugin-local/code-translation-remind.ts b/.eslint-plugin-local/code-translation-remind.ts index 42032321167..ed636ec0cb6 100644 --- a/.eslint-plugin-local/code-translation-remind.ts +++ b/.eslint-plugin-local/code-translation-remind.ts @@ -26,18 +26,19 @@ export default new class TranslationRemind implements eslint.Rule.RuleModule { private _checkImport(context: eslint.Rule.RuleContext, node: TSESTree.Node, path: string) { - if (path !== TranslationRemind.NLS_MODULE) { + if (path !== TranslationRemind.NLS_MODULE && !path.endsWith('/nls.js')) { return; } const currentFile = context.getFilename(); const matchService = currentFile.match(/vs\/workbench\/services\/\w+/); const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/); - if (!matchService && !matchPart) { + const matchSessionsPart = currentFile.match(/vs\/sessions\/contrib\/\w+/); + if (!matchService && !matchPart && !matchSessionsPart) { return; } - const resource = matchService ? matchService[0] : matchPart![0]; + const resource = matchService ? matchService[0] : matchPart ? matchPart[0] : matchSessionsPart![0]; let resourceDefined = false; let json; @@ -47,9 +48,10 @@ export default new class TranslationRemind implements eslint.Rule.RuleModule { console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); return; } - const workbenchResources = JSON.parse(json).workbench; + const parsed = JSON.parse(json); + const resources = [...parsed.workbench, ...parsed.sessions]; - workbenchResources.forEach((existingResource: any) => { + resources.forEach((existingResource: any) => { if (existingResource.name === resource) { resourceDefined = true; return; diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index eaf90f0dd1a..3091e0050f0 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -41,8 +41,8 @@ src/vs/platform/secrets/** @TylerLeonhardt src/vs/platform/sharedProcess/** @bpasero src/vs/platform/state/** @bpasero src/vs/platform/storage/** @bpasero -src/vs/platform/terminal/electron-main/** @Tyriar -src/vs/platform/terminal/node/** @Tyriar +src/vs/platform/terminal/electron-main/** @anthonykim1 +src/vs/platform/terminal/node/** @anthonykim1 src/vs/platform/utilityProcess/** @bpasero src/vs/platform/window/** @bpasero src/vs/platform/windows/** @bpasero diff --git a/.github/classifier.json b/.github/classifier.json index 32b68800113..39ebd9e38b2 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -16,7 +16,7 @@ "bracket-pair-guides": {"assign": ["hediet"]}, "breadcrumbs": {"assign": ["jrieken"]}, "callhierarchy": {"assign": ["jrieken"]}, - "chat-terminal": {"assign": ["Tyriar"]}, + "chat-terminal": {"assign": ["meganrogge"]}, "chat-terminal-output-monitor": {"assign": ["meganrogge"]}, "chrome-devtools": {"assign": ["deepak1556"]}, "cloud-changes": {"assign": ["joyceerhl"]}, @@ -228,18 +228,18 @@ "terminal-env-collection": {"assign": ["anthonykim1"]}, "terminal-external": {"assign": ["anthonykim1"]}, "terminal-find": {"assign": ["anthonykim1"]}, - "terminal-inline-chat": {"assign": ["Tyriar", "meganrogge"]}, - "terminal-input": {"assign": ["Tyriar"]}, + "terminal-inline-chat": {"assign": ["meganrogge"]}, + "terminal-input": {"assign": ["anthonykim1"]}, "terminal-layout": {"assign": ["anthonykim1"]}, - "terminal-ligatures": {"assign": ["Tyriar"]}, + "terminal-ligatures": {"assign": ["anthonykim1"]}, "terminal-links": {"assign": ["anthonykim1"]}, "terminal-local-echo": {"assign": ["anthonykim1"]}, - "terminal-parser": {"assign": ["Tyriar"]}, - "terminal-persistence": {"assign": ["Tyriar"]}, + "terminal-parser": {"assign": ["anthonykim1"]}, + "terminal-persistence": {"assign": ["anthonykim1"]}, "terminal-process": {"assign": ["anthonykim1"]}, "terminal-profiles": {"assign": ["meganrogge"]}, "terminal-quick-fix": {"assign": ["meganrogge"]}, - "terminal-rendering": {"assign": ["Tyriar"]}, + "terminal-rendering": {"assign": ["anthonykim1"]}, "terminal-shell-bash": {"assign": ["anthonykim1"]}, "terminal-shell-cmd": {"assign": ["anthonykim1"]}, "terminal-shell-fish": {"assign": ["anthonykim1"]}, @@ -283,7 +283,7 @@ "workbench-auxwindow": {"assign": ["bpasero"]}, "workbench-banner": {"assign": ["lszomoru", "sbatten"]}, "workbench-cli": {"assign": ["bpasero"]}, - "workbench-diagnostics": {"assign": ["Tyriar"]}, + "workbench-diagnostics": {"assign": ["rebornix"]}, "workbench-dnd": {"assign": ["bpasero"]}, "workbench-editor-grid": {"assign": ["benibenj"]}, "workbench-editor-groups": {"assign": ["bpasero"]}, @@ -293,7 +293,7 @@ "workbench-fonts": {"assign": []}, "workbench-history": {"assign": ["bpasero"]}, "workbench-hot-exit": {"assign": ["bpasero"]}, - "workbench-hover": {"assign": ["Tyriar", "benibenj"]}, + "workbench-hover": {"assign": ["benibenj"]}, "workbench-launch": {"assign": []}, "workbench-link": {"assign": []}, "workbench-multiroot": {"assign": ["bpasero"]}, diff --git a/.github/commands.json b/.github/commands.json index 978a0960eab..c52e21eeb8d 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -631,7 +631,7 @@ "addLabel": "capi", "removeLabel": "~capi", "assign": [ - "samvantran", + "rheapatel", "sharonlo" ], "comment": "Thank you for creating this issue! Please provide one or more `requestIds` to help the platform team investigate. You can follow instructions [found here](https://github.com/microsoft/vscode/wiki/Copilot-Issues#language-model-requests-and-responses) to locate the `requestId` value.\n\nHappy Coding!" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8d56465c45a..54502973051 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -145,6 +145,7 @@ function f(x: number, y: string): void { } - You MUST NOT use storage keys of another component only to make changes to that component. You MUST come up with proper API to change another component. - Use `IEditorService` to open editors instead of `IEditorGroupsService.activeGroup.openEditor` to ensure that the editor opening logic is properly followed and to avoid bypassing important features such as `revealIfOpened` or `preserveFocus`. - Avoid using `bind()`, `call()` and `apply()` solely to control `this` or partially apply arguments; prefer arrow functions or closures to capture the necessary context, and use these methods only when required by an API or interoperability. +- Avoid using events to drive control flow between components. Instead, prefer direct method calls or service interactions to ensure clearer dependencies and easier traceability of logic. Events should be reserved for broadcasting state changes or notifications rather than orchestrating behavior across components. ## Learnings - Minimize the amount of assertions in tests. Prefer one snapshot-style `assert.deepStrictEqual` over multiple precise assertions, as they are much more difficult to understand and to update. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 07296619597..f7e3481c75b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,27 +8,3 @@ updates: directory: "/" schedule: interval: "weekly" - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-name: "@vscode/component-explorer" - - dependency-name: "@vscode/component-explorer-cli" - groups: - component-explorer: - patterns: - - "@vscode/component-explorer" - - "@vscode/component-explorer-cli" - - package-ecosystem: "npm" - directory: "/build/vite" - schedule: - interval: "daily" - allow: - - dependency-name: "@vscode/component-explorer" - - dependency-name: "@vscode/component-explorer-vite-plugin" - groups: - component-explorer: - patterns: - - "@vscode/component-explorer" - - "@vscode/component-explorer-vite-plugin" diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json index 5e27e6db893..4457634963e 100644 --- a/.github/hooks/hooks.json +++ b/.github/hooks/hooks.json @@ -4,8 +4,7 @@ "sessionStart": [ { "type": "command", - "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup bash -c 'npm ci && npm run compile' > /tmp/worktree-setup-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi", - "powershell": "if (Test-Path \"$env:USERPROFILE\\.vscode-worktree-setup\") { $log = \"$env:TEMP\\worktree-setup-$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log\"; $dir = $PWD.Path; Start-Job -ScriptBlock { param($d, $l) Set-Location $d; & { npm ci; if ($LASTEXITCODE -eq 0) { npm run compile } } *> $l } -ArgumentList $dir, $log | Out-Null }" + "bash": "" } ], "sessionEnd": [ diff --git a/.github/instructions/agentHostTesting.instructions.md b/.github/instructions/agentHostTesting.instructions.md new file mode 100644 index 00000000000..7ffbbe4ff6e --- /dev/null +++ b/.github/instructions/agentHostTesting.instructions.md @@ -0,0 +1,27 @@ +--- +description: Architecture documentation for VS Code AI Customization view. Use when working in `src/vs/workbench/contrib/chat/browser/aiCustomization` +applyTo: 'src/vs/platform/agentHost/**' +--- + +# Agent Host + +The agent host communicates via the Agent Host Protocol. The specification for this lives in a directory `../agent-host-protocol` as a sibling of the VS Code directory. + +If this directory doesn't exist, you should use the "ask questions" tool to ask the user if they want to clone `git@github.com:microsoft/agent-host-protocol.git` to that directory. After doing so, you should also prompt the user to add `file:////plugins/copilot-plugin` as a plugin in their `chat.pluginLocations` settings. + +## Overall Protocol + +The sessions process is a portable, standalone server that multiple clients can connect to. Clients see a synchronized view of sessions and can send commands that are reflected back as state-changing actions. The protocol is designed around four requirements: + +1. **Synchronized multi-client state** — an immutable, redux-like state tree mutated exclusively by actions flowing through pure reducers. While there is the option to implement functionality via imperative commands, we ALWAYS prefer to model features as pure state and actions. +2. **Lazy loading** — clients subscribe to state by URI and load data on demand. The session list is fetched imperatively. Large content (images, long tool outputs) is stored by reference and fetched separately. +3. **Write-ahead with reconciliation** — clients optimistically apply their own actions locally, then reconcile when the server echoes them back alongside any concurrent actions from other clients or the server itself. +4. **Forward-compatible versioning** — newer clients can connect to older servers. A single protocol version number maps to a capabilities object; clients check capabilities before using features. + +See the agent host protocol documentation for more details. + +## End to End Testing + +You can run `node ./scripts/code-agent-host.js` to start an agent host. If you pass `--enable-mock-agent`, then the `ScriptedMockAgent` will be used. + +By default this will listen on `ws://127.0.0.1:8081`. You can then use the `ahp-websocket` client, when available, to connect to and communicate with it. diff --git a/.github/instructions/oss.instructions.md b/.github/instructions/oss.instructions.md new file mode 100644 index 00000000000..2e73cdbbbc2 --- /dev/null +++ b/.github/instructions/oss.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '{ThirdPartyNotices.txt,cli/ThirdPartyNotices.txt,cglicenses.json,cgmanifest.json}' +--- + +# OSS License Review + +When reviewing changes to these files, verify: + +## ThirdPartyNotices.txt + +- Every new entry has a license type header (e.g., "MIT License", "Apache License 2.0") +- License text is present and non-empty for every entry +- License text matches the declared license type (e.g., MIT-declared entry actually contains MIT license text, not Apache) +- Removed entries are cleanly removed (no leftover fragments) +- Entries are sorted alphabetically by package name + +## cglicenses.json + +- New overrides have a justification comment +- No obviously stale entries for packages no longer in the dependency tree + +## cgmanifest.json + +- Package versions match what's actually installed +- Repository URLs are valid and point to real source repositories +- Newly added license identifiers should use SPDX format where possible +- License identifiers match the corresponding ThirdPartyNotices.txt entries + +## Red Flags + +- Any **newly added** copyleft license (GPL, LGPL, AGPL) — flag immediately (existing copyleft entries like ffmpeg are pre-approved) +- Any "UNKNOWN" or placeholder license text +- License text that appears truncated or corrupted +- A package declared as MIT but with Apache/BSD/other license text (or vice versa) diff --git a/.github/prompts/fix-error.prompt.md b/.github/prompts/fix-error.prompt.md index 1cdc8fa90b4..e833fb07e1e 100644 --- a/.github/prompts/fix-error.prompt.md +++ b/.github/prompts/fix-error.prompt.md @@ -9,9 +9,52 @@ The user has given you a GitHub issue URL for an unhandled error from the VS Cod Follow the `fix-errors` skill guidelines to fix this error. Key principles: -1. **Do NOT fix at the crash site.** Do not add guards, try/catch, or fallback values at the bottom of the stack trace. That only masks the problem. -2. **Trace the data flow upward** through the call stack to find the producer of invalid data. -3. **If the producer is cross-process** (e.g., IPC) and cannot be identified from the stack alone, **enrich the error message** with diagnostic context (data type, truncated value, operation name) so the next telemetry cycle reveals the source. Do NOT silently swallow the error. -4. **If the producer is identifiable**, fix it directly. +1. **Read the error construction code first.** Before proposing any fix, search the codebase for where the error is constructed (the `new Error(...)` or custom error class instantiation). Read the surrounding code to understand: + - What conditions trigger the error (thresholds, validation checks, categorization logic) + - What parameters, classifications, or categories the error encodes + - What the intended meaning of each category is and what action each warrants + - Whether the error is a symptom of invalid data, a threshold-based warning, or a design-time signal + Use this understanding to determine the correct fix strategy. Do NOT assume what the error means from its message alone — the construction code is the source of truth. +2. **Do NOT fix at the crash site.** Do not add guards, try/catch, or fallback values at the bottom of the stack trace. That only masks the problem. +3. **Trace the data flow upward** through the call stack to find the producer of invalid data. +4. **If the producer is cross-process** (e.g., IPC) and cannot be identified from the stack alone, **enrich the error message** with diagnostic context (data type, truncated value, operation name) so the next telemetry cycle reveals the source. Do NOT silently swallow the error. +5. **If the producer is identifiable**, fix it directly. After making changes, check for compilation errors via the build task and run relevant unit tests. + +## Submitting the Fix + +After the fix is validated (compilation clean, tests pass): + +1. **Create a branch**: `git checkout -b /` (e.g., `bryanchen-d/fix-notebook-index-error`). +2. **Commit**: Stage changed files and commit with a message like `fix: (#)`. +3. **Push**: `git push -u origin `. +4. **Create a draft PR** with a description that includes these sections: + - **Summary**: A concise description of what was changed and why. + - **Issue link**: `Fixes #` so GitHub auto-closes the issue when the PR merges. + - **Trigger scenarios**: What user actions or system conditions cause this error to surface. + - **Code flow diagram**: A Mermaid swimlane/sequence diagram showing the call chain from trigger to error. Use participant labels for the key components (e.g., classes, modules, processes). Example: + ```` + ```mermaid + sequenceDiagram + participant A as CallerComponent + participant B as MiddleLayer + participant C as LowLevelUtil + A->>B: someOperation(data) + B->>C: validate(data) + C-->>C: data is invalid + C->>B: throws "error message" + B->>A: unhandled error propagates + ``` + ```` + - **Manual validation steps**: Concrete, step-by-step instructions a reviewer can follow to reproduce the original error and verify the fix. Include specific setup requirements (e.g., file types to open, settings to change, actions to perform). If the error cannot be easily reproduced manually, explain why and describe what alternative validation was performed (e.g., unit tests, code inspection). + - **How the fix works**: A brief explanation of the fix approach, with a note per changed file. +5. **Monitor the PR — BLOCKING**: You MUST NOT complete the task until the monitoring loop below is done. + - Wait 2 minutes after each push, then check for Copilot review comments using `gh pr view --json reviews,comments` and `gh api repos/{owner}/{repo}/pulls/{number}/comments`. + - If there are review comments, evaluate each one: + - If valid, apply the fix in a new commit, push, and **resolve the comment thread** using the GitHub GraphQL API (`resolveReviewThread` mutation with the thread's node ID). + - If not applicable, leave a reply explaining why. + - After addressing comments, update the PR description if the changes affect the summary, diagram, or per-file notes. + - **Re-run tests** after addressing review comments to confirm nothing regressed. + - After each push, repeat the wait-and-check cycle. Continue until **two consecutive checks return zero new comments**. +6. **Verify CI**: After the monitoring loop is done, check that CI checks are passing using `gh pr checks `. If any required checks fail, investigate and fix. Do NOT complete the task with failing CI. diff --git a/.github/skills/accessibility/SKILL.md b/.github/skills/accessibility/SKILL.md index 1d141f0c092..591e85ff812 100644 --- a/.github/skills/accessibility/SKILL.md +++ b/.github/skills/accessibility/SKILL.md @@ -1,8 +1,22 @@ --- name: accessibility -description: Accessibility guidelines for VS Code features — covers accessibility help dialogs, accessible views, verbosity settings, accessibility signals, ARIA alerts/status announcements, keyboard navigation, and ARIA labels/roles. Applies to both new interactive UI surfaces and updates to existing features. Use when creating new UI or updating existing UI features. +description: Primary accessibility skill for VS Code. REQUIRED for new feature and contribution work, and also applies to updates of existing UI. Covers accessibility help dialogs, accessible views, verbosity settings, signals, ARIA announcements, keyboard navigation, and ARIA labels/roles. --- +## When to Use This Skill + +Use this skill for any VS Code feature work that introduces or changes interactive UI. +Use this skill by default for new features and contributions, including when the request does not explicitly mention accessibility. + +Trigger examples: +- "add a new feature" +- "implement a new panel/view/widget" +- "add a new command or workflow" +- "new contribution in workbench/editor/extensions" +- "update existing UI interactions" + +Do not skip this skill just because accessibility is not named in the prompt. + When adding a **new interactive UI surface** to VS Code — a panel, view, widget, editor overlay, dialog, or any rich focusable component the user interacts with — you **must** provide three accessibility components (if they do not already exist for the feature): 1. **An Accessibility Help Dialog** — opened via the accessibility help keybinding when the feature has focus. @@ -47,10 +61,7 @@ An accessibility help dialog tells the user what the feature does, which keyboar The simplest approach is to return an `AccessibleContentProvider` directly from `getProvider()`. This is the most common pattern in the codebase (used by chat, inline chat, quick chat, etc.): ```ts -import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId } from '…/accessibleView.js'; -import { IAccessibleViewImplementation } from '…/accessibleViewRegistry.js'; -import { AccessibilityVerbositySettingId } from '…/accessibilityConfiguration.js'; -import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId, IAccessibleViewContentProvider, IAccessibleViewOptions } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { AccessibilityVerbositySettingId } from '../../../../platform/accessibility/common/accessibilityConfiguration.js'; diff --git a/.github/skills/azure-pipelines/SKILL.md b/.github/skills/azure-pipelines/SKILL.md index 97904019952..8903496983c 100644 --- a/.github/skills/azure-pipelines/SKILL.md +++ b/.github/skills/azure-pipelines/SKILL.md @@ -92,8 +92,8 @@ node .github/skills/azure-pipelines/azure-pipeline.ts queue --parameter "VSCODE_ |--------|-------------| | `--branch ` | Source branch to build (default: current git branch) | | `--definition ` | Pipeline definition ID (default: 111) | -| `--parameter ` | Pipeline parameter in `KEY=value` format (repeatable) | -| `--parameters ` | Space-separated parameters in `KEY=value KEY2=value2` format | +| `--parameter ` | Pipeline parameter in `KEY=value` format (repeatable); **use this when the value contains spaces** | +| `--parameters ` | Space-separated parameters in `KEY=value KEY2=value2` format; values **must not** contain spaces | | `--dry-run` | Print the command without executing | ### Product Build Queue Parameters (`build/azure-pipelines/product-build.yml`) diff --git a/.github/skills/azure-pipelines/azure-pipeline.ts b/.github/skills/azure-pipelines/azure-pipeline.ts index fbb74b5dd4a..3032f01c6fd 100644 --- a/.github/skills/azure-pipelines/azure-pipeline.ts +++ b/.github/skills/azure-pipelines/azure-pipeline.ts @@ -556,7 +556,11 @@ class AzureDevOpsClient { protected runAzCommand(args: string[]): Promise { return new Promise((resolve, reject) => { - const proc = spawn('az', args, { shell: true }); + // Use shell: false so that argument values with spaces are passed verbatim + // to the process without shell word-splitting. On Windows, az is a .cmd + // file and cannot be executed directly, so we must use az.cmd. + const azBin = process.platform === 'win32' ? 'az.cmd' : 'az'; + const proc = spawn(azBin, args, { shell: false }); let stdout = ''; let stderr = ''; @@ -778,8 +782,8 @@ function printQueueUsage(): void { console.log('Options:'); console.log(' --branch Source branch to build (default: current git branch)'); console.log(' --definition Pipeline definition ID (default: 111)'); - console.log(' --parameter Pipeline parameter in "KEY=value" format (repeatable)'); - console.log(' --parameters Space-separated parameter list in "KEY=value KEY2=value2" format'); + console.log(' --parameter Pipeline parameter in "KEY=value" format (repeatable); use this for values with spaces'); + console.log(' --parameters Space-separated parameter list in "KEY=value KEY2=value2" format (values must not contain spaces)'); console.log(' --dry-run Print the command without executing'); console.log(' --help Show this help message'); console.log(''); @@ -887,7 +891,8 @@ async function runQueueCommand(args: string[]): Promise { cmdArgs.push('--parameters', ...parsedArgs.parameters); } cmdArgs.push('--output', 'json'); - console.log(`az ${cmdArgs.join(' ')}`); + const displayArgs = cmdArgs.map(a => a.includes(' ') ? `"${a}"` : a); + console.log(`az ${displayArgs.join(' ')}`); process.exit(0); } @@ -1539,6 +1544,15 @@ async function runAllTests(): Promise { assert.ok(cmd.includes('OTHER=test')); }); + it('queueBuild passes parameter values with spaces verbatim', async () => { + const client = new TestableAzureDevOpsClient(ORGANIZATION, PROJECT); + await client.queueBuild('111', 'main', ['VSCODE_BUILD_TYPE=Product Build']); + + const cmd = client.capturedCommands[0]; + assert.ok(cmd.includes('--parameters')); + assert.deepStrictEqual(cmd[cmd.indexOf('--parameters') + 1], 'VSCODE_BUILD_TYPE=Product Build'); + }); + it('getBuild constructs correct az command', async () => { const client = new TestableAzureDevOpsClient(ORGANIZATION, PROJECT); await client.getBuild('12345'); diff --git a/.github/skills/chat-customizations-editor/SKILL.md b/.github/skills/chat-customizations-editor/SKILL.md new file mode 100644 index 00000000000..8d4ce8edda1 --- /dev/null +++ b/.github/skills/chat-customizations-editor/SKILL.md @@ -0,0 +1,182 @@ +--- +name: chat-customizations-editor +description: Use when working on the Chat Customizations editor — the management UI for agents, skills, instructions, hooks, prompts, MCP servers, and plugins. +--- + +# Chat Customizations Editor + +Split-view management pane for AI customization items across workspace, user, extension, and plugin storage. Supports harness-based filtering (Local, Copilot CLI, Claude). + +## Spec + +**`src/vs/sessions/AI_CUSTOMIZATIONS.md`** — always read before making changes, always update after. + +## Key Folders + +| Folder | What | +|--------|------| +| `src/vs/workbench/contrib/chat/common/` | `ICustomizationHarnessService`, `ISectionOverride`, `IStorageSourceFilter` — shared interfaces and filter helpers | +| `src/vs/workbench/contrib/chat/browser/aiCustomization/` | Management editor, list widgets (prompts, MCP, plugins), harness service registration | +| `src/vs/sessions/contrib/chat/browser/` | Sessions-window overrides (harness service, workspace service) | +| `src/vs/sessions/contrib/sessions/browser/` | Sessions tree view counts and toolbar | + +When changing harness descriptor interfaces or factory functions, verify both core and sessions registrations compile. + +## Key Interfaces + +- **`IHarnessDescriptor`** — drives all UI behavior declaratively (hidden sections, button overrides, file filters, agent gating). See spec for full field reference. +- **`ISectionOverride`** — per-section button customization (command invocation, root file creation, type labels, file extensions). +- **`IStorageSourceFilter`** — controls which storage sources and user roots are visible per harness/type. + +Principle: the UI widgets read everything from the descriptor — no harness-specific conditionals in widget code. + +## Testing + +Component explorer fixtures (see `component-fixtures` skill): `aiCustomizationListWidget.fixture.ts`, `aiCustomizationManagementEditor.fixture.ts` under `src/vs/workbench/test/browser/componentFixtures/`. + +### Screenshotting specific tabs + +The management editor fixture supports a `selectedSection` option to render any tab. Each tab has Dark/Light variants auto-generated by `defineThemedFixtureGroup`. + +**Available fixture IDs** (use with `mcp_component-exp_screenshot`): + +| Fixture ID pattern | Tab shown | +|---|---| +| `chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTab/{Dark,Light}` | Agents | +| `chat/aiCustomizations/aiCustomizationManagementEditor/SkillsTab/{Dark,Light}` | Skills | +| `chat/aiCustomizations/aiCustomizationManagementEditor/InstructionsTab/{Dark,Light}` | Instructions | +| `chat/aiCustomizations/aiCustomizationManagementEditor/HooksTab/{Dark,Light}` | Hooks | +| `chat/aiCustomizations/aiCustomizationManagementEditor/PromptsTab/{Dark,Light}` | Prompts | +| `chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTab/{Dark,Light}` | MCP Servers | +| `chat/aiCustomizations/aiCustomizationManagementEditor/PluginsTab/{Dark,Light}` | Plugins | +| `chat/aiCustomizations/aiCustomizationManagementEditor/LocalHarness/{Dark,Light}` | Default (Agents, Local harness) | +| `chat/aiCustomizations/aiCustomizationManagementEditor/CliHarness/{Dark,Light}` | Default (Agents, CLI harness) | +| `chat/aiCustomizations/aiCustomizationManagementEditor/ClaudeHarness/{Dark,Light}` | Default (Agents, Claude harness) | +| `chat/aiCustomizations/aiCustomizationManagementEditor/Sessions/{Dark,Light}` | Sessions window variant | + +**Adding a new tab fixture:** Add a variant to the `defineThemedFixtureGroup` in `aiCustomizationManagementEditor.fixture.ts`: +```typescript +MyNewTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.MySection, + }), +}), +``` + +The `selectedSection` calls `editor.selectSectionById()` after `setInput`, which navigates to the specified tab and re-layouts. + +### Populating test data + +Each customization type requires its own mock path in `createMockPromptsService`: +- **Agents** — `getCustomAgents()` returns agent objects +- **Skills** — `findAgentSkills()` returns `IAgentSkill[]` +- **Prompts** — `getPromptSlashCommands()` returns `IChatPromptSlashCommand[]` +- **Instructions/Hooks** — `listPromptFiles()` filtered by `PromptsType` +- **MCP Servers** — `mcpWorkspaceServers`/`mcpUserServers` arrays passed to `IMcpWorkbenchService` mock +- **Plugins** — `IPluginMarketplaceService.installedPlugins` and `IAgentPluginService.plugins` observables + +All test data lives in `allFiles` (prompt-based items) and the `mcpWorkspace/UserServers` arrays. Add enough items per category (8+) to invoke scrolling. + +### Exercising built-in grouping + +The list widget regroups items from the default chat extension under a "Built-in" header. Three things must be in place for fixtures to exercise this: +1. Include `BUILTIN_STORAGE` in the harness descriptor's visible sources +2. Mock `IProductService.defaultChatAgent.chatExtensionId` (e.g., `'GitHub.copilot-chat'`) +3. Give mock items extension provenance via `extensionId` / `extensionDisplayName` matching that ID + +Without all three, built-in regrouping silently doesn't run and the fixture only shows flat lists. + +### Editor contribution service mocks + +The management editor embeds a `CodeEditorWidget`. Electron-side editor contributions (e.g., `AgentFeedbackEditorWidgetContribution`) are instantiated automatically and crash if their injected services aren't registered. The fixture must mock at minimum: +- `IAgentFeedbackService` — needs `onDidChangeFeedback`, `onDidChangeNavigation` as `Event.None` +- `ICodeReviewService` — needs `getReviewState()` / `getPRReviewState()` returning idle observables +- `IChatEditingService` — needs `editingSessionsObs` as empty observable +- `IAgentSessionsService` — needs `model.sessions` as empty array + +These are cross-layer imports from `vs/sessions/` — use `// eslint-disable-next-line local/code-import-patterns` on the import lines. + +### CI regression gates + +Key fixtures have `blocksCi: true` in their labels. The `screenshot-test.yml` GitHub Action captures screenshots on every PR to `main` and **fails the CI status check** if any `blocks-ci`-labeled fixture's screenshot changes. This catches layout regressions automatically. + +Currently gated fixtures: `LocalHarness`, `McpServersTab`, `McpServersTabNarrow`, `AgentsTabNarrow`. When adding a new section or layout-critical fixture, add `blocksCi: true`: + +```typescript +MyFixture: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: ctx => renderEditor(ctx, { ... }), +}), +``` + +Don't add `blocksCi` to every fixture — only ones that cover critical layout paths (default view, section with list + footer, narrow viewport). Too many gated fixtures creates noisy CI. + +### Screenshot stability + +Scrollbar fade transitions cause screenshot instability — the scrollbar shifts from `visible` to `invisible fade` class ~2 seconds after a programmatic scroll. After calling `revealLastItem()` or any scroll action, wait for the transition to complete before the fixture's render promise resolves: + +```typescript +await new Promise(resolve => setTimeout(resolve, 2400)); +// Then optionally poll until .scrollbar.vertical loses the 'visible' class +``` + +### Running unit tests + +```bash +./scripts/test.sh --grep "applyStorageSourceFilter|customizationCounts" +npm run compile-check-ts-native && npm run valid-layers-check +``` + +See the `sessions` skill for sessions-window specific guidance. + +## Debugging Layout in the Real Product + +Component fixtures use mock data and a fixed container size. Layout bugs caused by reflow timing, real data shapes, or narrow window sizes often **don't reproduce in fixtures**. When a user reports a broken layout, debug in the live Code OSS product. + +For launching Code OSS with CDP and connecting `agent-browser`, see the **`launch` skill**. Use `--user-data-dir /tmp/code-oss-debug` to avoid colliding with an already-running instance from another worktree. + +### Navigating to the customizations editor + +After connecting, use `snapshot -i` to find the "Open Customizations" button (in the Chat panel header), then click it. To switch sections, use `eval` with a DOM click since sidebar items aren't interactive refs: + +```bash +npx agent-browser eval "const items = [...document.querySelectorAll('.section-list-item')]; \ + items.find(el => el.textContent?.includes('MCP'))?.click();" +``` + +### Inspecting widget layout + +`agent-browser eval` doesn't always print return values. Use `document.title` as a return channel: + +```bash +npx agent-browser eval "const w = document.querySelector('.mcp-list-widget'); \ + const lc = w?.querySelector('.mcp-list-container'); \ + const rows = lc?.querySelectorAll('.monaco-list-row'); \ + document.title = 'DBG:rows=' + (rows?.length ?? -1) \ + + ',listH=' + (lc?.offsetHeight ?? -1) \ + + ',seStH=' + (lc?.querySelector('.monaco-scrollable-element')?.style?.height ?? '') \ + + ',wH=' + (w?.offsetHeight ?? -1);" +npx agent-browser eval "document.title" 2>&1 +``` + +Key diagnostics: +- **`rows`** — fewer than expected means `list.layout()` never received the correct viewport height. +- **`seStH`** — empty means the list was never properly laid out. +- **`listH` vs `wH`** — list container height should be widget height minus search bar minus footer. + +### Common layout issues + +| Symptom | Root cause | Fix pattern | +|---------|-----------|-------------| +| List shows 0-1 rows in a tall container | `layout()` bailed out because `offsetHeight` returned 0 during `display:none → visible` transition | Defer layout via `DOM.getWindow(this.element).requestAnimationFrame(...)` | +| Badge or row content clips at right edge | Widget container missing `overflow: hidden` | Add `overflow: hidden` to the widget's CSS class | +| Items visible in fixture but not in product | Fixture uses many mock items; real product has few | Add fixture variants with fewer items or narrower dimensions (`width`/`height` options) | + +### Fixture vs real product gaps + +Fixtures render at a fixed size (default 900×600) with many mock items. They won't catch: +- **Reflow timing** — the real product's `display:none → visible` transition may not have reflowed before `layout()` fires +- **Narrow windows** — add narrow fixture variants (e.g., `width: 550, height: 400`) +- **Real data counts** — a user with 1 MCP server sees very different layout than a fixture with 12 diff --git a/.github/skills/fix-ci-failures/SKILL.md b/.github/skills/fix-ci-failures/SKILL.md new file mode 100644 index 00000000000..4e05478ad78 --- /dev/null +++ b/.github/skills/fix-ci-failures/SKILL.md @@ -0,0 +1,269 @@ +--- +name: fix-ci-failures +description: Investigate and fix CI failures on a pull request. Use when CI checks fail on a PR branch — covers finding the PR, identifying failed checks, downloading logs and artifacts, extracting the failure cause, and iterating on a fix. Requires the `gh` CLI. +--- + +# Investigating and Fixing CI Failures + +This skill guides you through diagnosing and fixing CI failures on a PR using the `gh` CLI. The user has the PR branch checked out locally. + +## Workflow Overview + +1. Identify the current branch and its PR +2. Check CI status and find failed checks +3. Download logs for failed jobs +4. Extract and understand the failure +5. Fix the issue and push + +--- + +## Step 1: Identify the Branch and PR + +```bash +# Get the current branch name +git branch --show-current + +# Find the PR for this branch +gh pr view --json number,title,url,statusCheckRollup +``` + +If no PR is found, the user may need to specify the PR number. + +--- + +## Step 2: Check CI Status + +```bash +# List all checks and their status (pass/fail/pending) +gh pr checks --json name,state,link,bucket + +# Filter to only failed checks +gh pr checks --json name,state,link,bucket --jq '.[] | select(.bucket == "fail")' +``` + +The `link` field contains the URL to the GitHub Actions job. Extract the **run ID** from the URL — it's the number after `/runs/`: +``` +https://github.com/microsoft/vscode/actions/runs//job/ +``` + +If checks are still `IN_PROGRESS`, wait for them to complete before downloading logs: +```bash +gh pr checks --watch --fail-fast +``` + +--- + +## Step 3: Get Failed Job Details + +```bash +# List failed jobs in a run (use the run ID from the check link) +gh run view --json jobs --jq '.jobs[] | select(.conclusion == "failure") | {name: .name, id: .databaseId}' +``` + +--- + +## Step 4: Download Failure Logs + +There are two approaches depending on the type of failure. + +### Option A: View Failed Step Logs Directly + +Best for build/compile/lint failures where the error is in the step output: + +```bash +# View only the failed step logs (most useful — shows just the errors) +gh run view --job --log-failed +``` + +> **Important**: `--log-failed` requires the **entire run** to complete, not just the failed job. If other jobs are still running, this command will block or error. Use **Option C** below to get logs for a completed job while the run is still in progress. + +The output can be large. Pipe through `tail` or `grep` to focus: +```bash +# Last 100 lines of failed output +gh run view --job --log-failed | tail -100 + +# Search for common error patterns +gh run view --job --log-failed | grep -E "Error|FAIL|error TS|AssertionError|failing" +``` + +### Option B: Download Artifacts + +Best for integration test failures where detailed logs (terminal logs, ext host logs, crash dumps) are uploaded as artifacts: + +```bash +# List available artifacts for a run +gh run download --pattern '*' --dir /dev/null 2>&1 || gh run view --json jobs --jq '.jobs[].name' + +# Download log artifacts for a specific failed job +# Artifact naming convention: logs---- +# Examples: logs-linux-x64-electron-1, logs-linux-x64-remote-1 +gh run download -n "logs-linux-x64-electron-1" -D /tmp/ci-logs + +# Download crash dumps if available +gh run download -n "crash-dump-linux-x64-electron-1" -D /tmp/ci-crashes +``` + +> **Tip**: Use the test runner name from the failed check (e.g., "Linux / Electron" → `electron`, "Linux / Remote" → `remote`) and platform map ("Windows" → `windows-x64`, "Linux" → `linux-x64`, "macOS" → `macos-arm64`) to construct the artifact name. + +> **Warning**: Log artifacts may be empty if the test runner crashed before producing output (e.g., Electron download failure). In that case, fall back to **Option C**. + +### Option C: Download Per-Job Logs via API (works while run is in progress) + +When the run is still in progress but the failed job has completed, use the GitHub API to download that job's step logs directly: + +```bash +# Save the full job log to a temp file (can be very large — 30k+ lines) +gh api repos/microsoft/vscode/actions/jobs//logs > "$TMPDIR/ci-job-log.txt" +``` + +Then search the saved file. **Start with `##[error]`** — this is the GitHub Actions error annotation that marks the exact line where the step failed: + +```bash +# Step 1: Find the error annotation (fastest path to the failure) +grep -n '##\[error\]' "$TMPDIR/ci-job-log.txt" + +# Step 2: Read context around the error (e.g., if error is on line 34371, read 200 lines before it) +sed -n '34171,34371p' "$TMPDIR/ci-job-log.txt" +``` + +If `##[error]` doesn't reveal enough, use broader patterns: +```bash +# Find test failures, exceptions, and crash indicators +grep -n -E 'HTTPError|ECONNRESET|ETIMEDOUT|502|exit code|Process completed|node:internal|triggerUncaughtException' "$TMPDIR/ci-job-log.txt" | head -20 +``` + +> **Why save to a file?** The API response for a full job log can be 30k+ lines. Tool output gets truncated, so always redirect to a file first, then search. + +### VS Code Log Artifacts Structure + +Downloaded log artifacts typically contain: +``` +logs-linux-x64-electron-1/ + main.log # Main process log + terminal.log # Terminal/pty host log (key for run_in_terminal issues) + window1/ + renderer.log # Renderer process log + exthost/ + exthost.log # Extension host log (key for extension test failures) +``` + +Key files to examine first: +- **Test assertion failures**: Check `exthost.log` for the extension host output and stack traces +- **Terminal/sandbox issues**: Check `terminal.log` for rewriter pipeline, shell integration, and strategy logs +- **Crash/hang**: Check `main.log` and look for crash dumps artifacts + +--- + +## Step 5: Extract the Failure + +### For Test Failures + +Look for the test runner output in the failed step log: +```bash +# Find failing test names and assertion messages +gh run view --job --log-failed | grep -A 5 "failing\|AssertionError\|Expected\|Unexpected" +``` + +Common patterns in VS Code CI: +- **`AssertionError [ERR_ASSERTION]`**: Test assertion failed — check expected vs actual values +- **`Extension host test runner exit code: 1`**: Integration test suite had failures +- **`Command produced no output`**: Shell integration may not have captured command output (see terminal.log) +- **`Error: Timeout`**: Test timed out — could be a hang or slow CI machine + +### For Build Failures + +```bash +# Find TypeScript compilation errors +gh run view --job --log-failed | grep "error TS" + +# Find hygiene/lint errors +gh run view --job --log-failed | grep -E "eslint|stylelint|hygiene" +``` + +--- + +## Step 6: Determine if Failures are Related to the PR + +Before fixing, determine if the failure is caused by the PR changes or is a pre-existing/infrastructure issue: + +1. **Check if the failing test is in code you changed** — if the test is in a completely unrelated area, it may be a flake +2. **Check the test name** — does it relate to the feature area you modified? +3. **Look at the failure output** — does it reference code paths your PR touches? +4. **Check if the same tests fail on main** — if identical failures exist on recent main commits, it's a pre-existing issue +5. **Look for infrastructure failures** — network timeouts, npm registry errors, and machine-level issues are not caused by code changes + +```bash +# Check recent runs on main for the same workflow +gh run list --branch main --workflow pr-linux-test.yml --limit 5 --json databaseId,conclusion,displayTitle +``` + +### Recognizing Infrastructure / Flaky Failures + +Not all CI failures are caused by code changes. Common infrastructure failures: + +**Network / Registry issues**: +- `npm ERR! network`, `ETIMEDOUT`, `ECONNRESET`, `EAI_AGAIN` — npm registry unreachable +- `error: RPC failed; curl 56`, `fetch-pack: unexpected disconnect` — git network failure +- `Error: unable to get local issuer certificate` — TLS/certificate issues +- `rate limit exceeded` — GitHub API rate limiting +- `HTTPError: Request failed with status code 502` on `electron/electron/releases` — Electron CDN download failure (common in the `node.js integration tests` step, which downloads Electron at runtime) + +**Machine / Environment issues**: +- `No space left on device` — CI disk full +- `ENOMEM`, `JavaScript heap out of memory` — CI machine ran out of memory +- `The runner has received a shutdown signal` — CI preemption / timeout +- `Error: The operation was canceled` — GitHub Actions cancelled the job +- `Xvfb failed to start` — display server for headless Linux tests failed + +**Test flakes** (not infrastructure, but not your fault either): +- Timeouts on tests that normally pass — slow CI machine +- Race conditions in async tests +- Shell integration not reporting exit codes (see terminal.log for `exitCode: undefined`) + +**What to do with infrastructure failures**: +1. **Don't change code** — the failure isn't caused by your PR +2. **Re-run the failed jobs** via the GitHub UI or: + ```bash + gh run rerun --failed + ``` +3. If failures persist across re-runs, check if main is also broken: + ```bash + gh run list --branch main --limit 10 --json databaseId,conclusion,displayTitle + ``` +4. If main is broken too, wait for it to be fixed — your PR is not the cause + +--- + +## Step 7: Fix and Iterate + +1. Make the fix locally +2. Verify compilation: check the `VS Code - Build` task or run `npm run compile-check-ts-native` +3. Run relevant unit tests locally: `./scripts/test.sh --grep ""` +4. Commit and push: + ```bash + git add -A + git commit -m "fix: " + git push + ``` +5. Watch CI again: + ```bash + gh pr checks --watch --fail-fast + ``` + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Find PR for branch | `gh pr view --json number,url` | +| List all checks | `gh pr checks --json name,state,bucket` | +| List failed checks only | `gh pr checks --json name,state,link,bucket --jq '.[] \| select(.bucket == "fail")'` | +| Watch checks until done | `gh pr checks --watch --fail-fast` | +| Failed jobs in a run | `gh run view --json jobs --jq '.jobs[] \| select(.conclusion == "failure") \| {name, id: .databaseId}'` | +| View failed step logs | `gh run view --job --log-failed` (requires full run to complete) | +| Download job log via API | `gh api repos/microsoft/vscode/actions/jobs//logs > "$TMPDIR/ci-job-log.txt"` (works while run is in progress) | +| Find error line in log | `grep -n '##\[error\]' "$TMPDIR/ci-job-log.txt"` | +| Download log artifacts | `gh run download -n "" -D /tmp/ci-logs` | +| Re-run failed jobs | `gh run rerun --failed` | +| Recent main runs | `gh run list --branch main --workflow .yml --limit 5` | diff --git a/.github/skills/fix-errors/SKILL.md b/.github/skills/fix-errors/SKILL.md index 7a03d11ee0c..f2afd203619 100644 --- a/.github/skills/fix-errors/SKILL.md +++ b/.github/skills/fix-errors/SKILL.md @@ -62,6 +62,34 @@ throw new Error(`[UriError]: Scheme contains illegal characters. scheme:"${ret.s **Right fix (when producer is known)**: Fix the code that sends malformed data. For example, if an authentication provider passes a stringified URI instead of a `UriComponents` object to a logger creation call, fix that call site to pass the proper object. +## Understanding error construction before fixing + +Before proposing any fix, **always find and read the code that constructs the error**. Search the codebase for the error class name or a unique substring of the error message. The construction code reveals: + +- **What conditions trigger the error** — thresholds, validation checks, state assertions +- **What classifications or categories the error encodes** — the error may have subtypes that require different fix strategies +- **What the error's parameters mean** — numeric values, ratios, or flags embedded in the message often encode diagnostic context +- **Whether the error is actionable** — some errors are threshold-based warnings where the threshold may be legitimately exceeded by design + +Use this understanding to determine the correct fix strategy. The construction code is the source of truth — do NOT assume what the error means from its message alone. + +### Example: Listener leak errors + +Searching for `ListenerLeakError` leads to `src/vs/base/common/event.ts`, where the construction code reveals: + +```typescript +const kind = topCount / listenerCount > 0.3 ? 'dominated' : 'popular'; +const error = new ListenerLeakError(kind, message, topStack); +``` + +Reading this code tells you: +- The error has two categories based on a ratio +- **Dominated** (ratio > 30%): one code path accounts for most listeners → that code path is the problem, fix its disposal +- **Popular** (ratio ≤ 30%): many diverse code paths each contribute a few listeners → the identified stack trace is NOT the root cause; it's just the most identical stack among many. Investigate the emitter and its aggregate subscribers instead +- For popular leaks: do NOT remove caching/pooling/reuse patterns that appear in the top stack — they exist to solve other problems. If the aggregate count is by design (e.g., many menus subscribing to a shared context key service), close the issue as "not planned" + +This analysis came from reading the construction code, not from memorized rules about listener leaks. + ## Guidelines - Prefer enriching error messages over adding try/catch guards diff --git a/.github/skills/unit-tests/SKILL.md b/.github/skills/unit-tests/SKILL.md new file mode 100644 index 00000000000..f2a8b66c5a3 --- /dev/null +++ b/.github/skills/unit-tests/SKILL.md @@ -0,0 +1,87 @@ +--- +name: unit-tests +description: Use when running unit tests in the VS Code repo. Covers the runTests tool, scripts/test.sh (macOS/Linux) and scripts/test.bat (Windows), and their supported arguments for filtering, globbing, and debugging tests. +--- + +# Running Unit Tests + +## Preferred: Use the `runTests` tool + +If the `runTests` tool is available, **prefer it** over running shell commands. It provides structured output with detailed pass/fail information and supports filtering by file and test name. + +- Pass absolute paths to test files via the `files` parameter. +- Pass test names via the `testNames` parameter to filter which tests run. +- Set `mode="coverage"` to collect coverage. + +Example (conceptual): run tests in `src/vs/editor/test/common/model.test.ts` with test name filter `"should split lines"`. + +## Fallback: Shell scripts + +When the `runTests` tool is not available (e.g. in CLI environments), use the platform-appropriate script from the repo root: + +- **macOS / Linux:** `./scripts/test.sh [options]` +- **Windows:** `.\scripts\test.bat [options]` + +These scripts download Electron if needed and launch the Mocha test runner. + +### Commonly used options + +#### `--run ` - Run tests from a specific file + +Accepts a **source file path** (starting with `src/`). The runner strips the `src/` prefix and the `.ts`/`.js` extension automatically to resolve the compiled module. + +```bash +./scripts/test.sh --run src/vs/editor/test/common/model.test.ts +``` + +Multiple files can be specified by repeating `--run`: + +```bash +./scripts/test.sh --run src/vs/editor/test/common/model.test.ts --run src/vs/editor/test/common/range.test.ts +``` + +#### `--grep ` (aliases: `-g`, `-f`) - Filter tests by name + +Runs only tests whose full title matches the pattern (passed to Mocha's `--grep`). + +```bash +./scripts/test.sh --grep "should split lines" +``` + +Combine with `--run` to filter tests within a specific file: + +```bash +./scripts/test.sh --run src/vs/editor/test/common/model.test.ts --grep "should split lines" +``` + +#### `--runGlob ` (aliases: `--glob`, `--runGrep`) - Run tests matching a glob + +Runs all test files matching a glob pattern against the compiled output directory. Useful for running all tests under a feature area. + +```bash +./scripts/test.sh --runGlob "**/editor/test/**/*.test.js" +``` + +Note: the glob runs against compiled `.js` files in the output directory, not source `.ts` files. + +#### `--coverage` - Generate a coverage report + +```bash +./scripts/test.sh --run src/vs/editor/test/common/model.test.ts --coverage +``` + +#### `--timeout ` - Set test timeout + +Override the default Mocha timeout for long-running tests. + +```bash +./scripts/test.sh --run src/vs/editor/test/common/model.test.ts --timeout 10000 +``` + +### Integration tests + +Integration tests (files ending in `.integrationTest.ts` or located in `extensions/`) are **not run** by `scripts/test.sh`. Use `scripts/test-integration.sh` (or `scripts/test-integration.bat`) instead. + +### Compilation requirement + +Tests run against compiled JavaScript output. Ensure the `VS Code - Build` watch task is running or that compilation has completed before running tests. Test failures caused by stale output are a common pitfall. diff --git a/.github/skills/update-screenshots/SKILL.md b/.github/skills/update-screenshots/SKILL.md index 46172cfee2d..294125273ef 100644 --- a/.github/skills/update-screenshots/SKILL.md +++ b/.github/skills/update-screenshots/SKILL.md @@ -72,7 +72,16 @@ git add test/componentFixtures/.screenshots/baseline/ git commit -m "update screenshot baselines from CI" ``` -### 7. Verify +### 7. Push LFS objects before pushing + +Screenshot baselines are stored in Git LFS. The `git lfs pre-push` hook is not active in this repo (husky overwrites it), so LFS objects are NOT automatically uploaded on `git push`. You must push them manually before pushing the branch, otherwise the push will fail with `GH008: Your push referenced unknown Git LFS objects`. + +```bash +git lfs push --all origin +git push +``` + +### 8. Verify Confirm the baselines are updated by listing the files: diff --git a/.github/workflows/api-proposal-version-check.yml b/.github/workflows/api-proposal-version-check.yml index 23f6e052f9f..ee082dee49f 100644 --- a/.github/workflows/api-proposal-version-check.yml +++ b/.github/workflows/api-proposal-version-check.yml @@ -23,16 +23,19 @@ jobs: check-version-changes: name: Check API Proposal Version Changes # Run on PR events, or on issue_comment if it's on a PR and contains the override command - if: false # temporarily disabled - # github.event_name == 'pull_request' || - # (github.event_name == 'issue_comment' && - # github.event.issue.pull_request && - # contains(github.event.comment.body, '/api-proposal-change-required')) + if: | + github.event_name == 'pull_request' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/api-proposal-change-required') && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR')) runs-on: ubuntu-latest steps: - name: Get PR info id: pr_info - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | let prNumber, headSha, baseSha; @@ -59,7 +62,7 @@ jobs: - name: Check for override comment id: check_override - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const prNumber = ${{ steps.pr_info.outputs.number }}; @@ -71,25 +74,50 @@ jobs: // Only accept overrides from trusted users (repo members/collaborators) const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; - const overrideComment = comments.find(comment => - comment.body.includes('/api-proposal-change-required') && - trustedAssociations.includes(comment.author_association) - ); + let overrideComment = null; + const untrustedOverrides = []; + + comments.forEach((comment, index) => { + const hasOverrideText = comment.body.includes('/api-proposal-change-required'); + const isTrusted = trustedAssociations.includes(comment.author_association); + console.log(`Comment ${index + 1}:`); + console.log(` Author: ${comment.user.login}`); + console.log(` Author association: ${comment.author_association}`); + console.log(` Created at: ${comment.created_at}`); + console.log(` Contains override command: ${hasOverrideText}`); + console.log(` Author is trusted: ${isTrusted}`); + console.log(` Would be valid override: ${hasOverrideText && isTrusted}`); + + if (hasOverrideText) { + if (isTrusted && !overrideComment) { + overrideComment = comment; + } else if (!isTrusted) { + untrustedOverrides.push(comment); + } + } + }); if (overrideComment) { - console.log(`Override comment found by ${overrideComment.user.login} (${overrideComment.author_association})`); + console.log(`✅ Override comment FOUND`); + console.log(` Comment ID: ${overrideComment.id}`); + console.log(` Author: ${overrideComment.user.login}`); + console.log(` Association: ${overrideComment.author_association}`); + console.log(` Created at: ${overrideComment.created_at}`); core.setOutput('override_found', 'true'); core.setOutput('override_user', overrideComment.user.login); } else { - // Check if there's an override from an untrusted user - const untrustedOverride = comments.find(comment => - comment.body.includes('/api-proposal-change-required') && - !trustedAssociations.includes(comment.author_association) - ); - if (untrustedOverride) { - console.log(`Override comment by ${untrustedOverride.user.login} ignored (${untrustedOverride.author_association} is not trusted)`); + if (untrustedOverrides.length > 0) { + console.log(`⚠️ Found ${untrustedOverrides.length} override comment(s) from UNTRUSTED user(s):`); + untrustedOverrides.forEach((comment, index) => { + console.log(` Untrusted override ${index + 1}:`); + console.log(` Author: ${comment.user.login}`); + console.log(` Association: ${comment.author_association}`); + console.log(` Created at: ${comment.created_at}`); + console.log(` Comment ID: ${comment.id}`); + }); + console.log(` Trusted associations are: ${trustedAssociations.join(', ')}`); } - console.log('No valid override comment found'); + console.log('❌ No valid override comment found'); core.setOutput('override_found', 'false'); } @@ -102,7 +130,7 @@ jobs: (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const headSha = '${{ steps.pr_info.outputs.head_sha }}'; @@ -213,7 +241,7 @@ jobs: - name: Post warning comment if: steps.check_override.outputs.override_found != 'true' && steps.version_check.outputs.version_changed == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const prNumber = ${{ steps.pr_info.outputs.number }}; diff --git a/.github/workflows/no-engineering-system-changes.yml b/.github/workflows/no-engineering-system-changes.yml index 45d1ae55f62..be9cf34d077 100644 --- a/.github/workflows/no-engineering-system-changes.yml +++ b/.github/workflows/no-engineering-system-changes.yml @@ -21,22 +21,52 @@ jobs: echo "engineering_systems_modified=false" >> $GITHUB_OUTPUT echo "No engineering systems were modified in this PR" fi + - name: Allow automated distro updates + id: distro_exception + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login == 'vs-code-engineering[bot]' }} + run: | + # Allow the vs-code-engineering bot ONLY when package.json is the + # sole changed file and the diff exclusively touches the "distro" field. + ONLY_PKG=$(jq -e '. == ["package.json"]' "$HOME/files.json" > /dev/null 2>&1 && echo true || echo false) + if [[ "$ONLY_PKG" != "true" ]]; then + echo "Bot modified files beyond package.json — not allowed" + echo "allowed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + DIFF=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }}) || { + echo "Failed to fetch PR diff — not allowed" + echo "allowed=false" >> $GITHUB_OUTPUT + exit 0 + } + CHANGED_LINES=$(echo "$DIFF" | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | wc -l) + DISTRO_LINES=$(echo "$DIFF" | grep -cE '^[+-][[:space:]]*"distro"[[:space:]]*:' || true) + + if [[ "$CHANGED_LINES" -eq 2 && "$DISTRO_LINES" -eq 2 ]]; then + echo "Distro-only update by bot — allowing" + echo "allowed=true" >> $GITHUB_OUTPUT + else + echo "Bot changed more than the distro field — not allowed" + echo "allowed=false" >> $GITHUB_OUTPUT + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Prevent Copilot from modifying engineering systems - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login == 'Copilot' }} + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.distro_exception.outputs.allowed != 'true' && github.event.pull_request.user.login == 'Copilot' }} run: | echo "Copilot is not allowed to modify .github/workflows, build folder files, or package.json files." echo "If you need to update engineering systems, please do so manually or through authorized means." exit 1 - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 id: get_permissions - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.distro_exception.outputs.allowed != 'true' && github.event.pull_request.user.login != 'Copilot' }} with: route: GET /repos/microsoft/vscode/collaborators/${{ github.event.pull_request.user.login }}/permission env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set control output variable id: control - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.distro_exception.outputs.allowed != 'true' && github.event.pull_request.user.login != 'Copilot' }} run: | echo "user: ${{ github.event.pull_request.user.login }}" echo "role: ${{ fromJson(steps.get_permissions.outputs.data).permission }}" @@ -44,7 +74,7 @@ jobs: echo "should_run: ${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) }}" echo "should_run=${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) && github.event.pull_request.user.login != 'dependabot[bot]' }}" >> $GITHUB_OUTPUT - name: Check for engineering system changes - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.control.outputs.should_run == 'true' }} + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.distro_exception.outputs.allowed != 'true' && steps.control.outputs.should_run == 'true' }} run: | echo "Changes to .github/workflows/, build/ folder files, or package.json files aren't allowed in PRs." exit 1 diff --git a/.github/workflows/no-package-lock-changes.yml b/.github/workflows/no-package-lock-changes.yml deleted file mode 100644 index 04ea8a43a80..00000000000 --- a/.github/workflows/no-package-lock-changes.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Prevent package-lock.json changes in PRs - -on: pull_request -permissions: {} - -jobs: - main: - name: Prevent package-lock.json changes in PRs - runs-on: ubuntu-latest - steps: - - name: Get file changes - uses: trilom/file-changes-action@ce38c8ce2459ca3c303415eec8cb0409857b4272 - id: file_changes - - name: Check if lockfiles were modified - id: lockfile_check - run: | - if cat $HOME/files.json | jq -e 'any(test("package-lock\\.json$|Cargo\\.lock$"))' > /dev/null; then - echo "lockfiles_modified=true" >> $GITHUB_OUTPUT - echo "Lockfiles were modified in this PR" - else - echo "lockfiles_modified=false" >> $GITHUB_OUTPUT - echo "No lockfiles were modified in this PR" - fi - - name: Prevent Copilot from modifying lockfiles - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && github.event.pull_request.user.login == 'Copilot' }} - run: | - echo "Copilot is not allowed to modify package-lock.json or Cargo.lock files." - echo "If you need to update dependencies, please do so manually or through authorized means." - exit 1 - - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 - id: get_permissions - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} - with: - route: GET /repos/microsoft/vscode/collaborators/{username}/permission - username: ${{ github.event.pull_request.user.login }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Set control output variable - id: control - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} - run: | - echo "user: ${{ github.event.pull_request.user.login }}" - echo "role: ${{ fromJson(steps.get_permissions.outputs.data).permission }}" - echo "is dependabot: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}" - echo "should_run: ${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) }}" - echo "should_run=${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) && github.event.pull_request.user.login != 'dependabot[bot]' }}" >> $GITHUB_OUTPUT - - name: Check for lockfile changes - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && steps.control.outputs.should_run == 'true' }} - run: | - echo "Changes to package-lock.json/Cargo.lock files aren't allowed in PRs." - exit 1 diff --git a/.github/workflows/pr-linux-cli-test.yml b/.github/workflows/pr-linux-cli-test.yml index 003e1344fb6..70874649dd5 100644 --- a/.github/workflows/pr-linux-cli-test.yml +++ b/.github/workflows/pr-linux-cli-test.yml @@ -11,7 +11,7 @@ on: jobs: linux-cli-test: name: ${{ inputs.job_name }} - runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + runs-on: ubuntu-22.04 env: RUSTUP_TOOLCHAIN: ${{ inputs.rustup_toolchain }} steps: diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index 7922ec107f9..c2d20a7d21d 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -17,7 +17,7 @@ on: jobs: linux-test: name: ${{ inputs.job_name }} - runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + runs-on: ubuntu-22.04 env: ARTIFACT_NAME: ${{ (inputs.electron_tests && 'electron') || (inputs.browser_tests && 'browser') || (inputs.remote_tests && 'remote') || 'unknown' }} NPM_ARCH: x64 @@ -42,7 +42,9 @@ jobs: libxkbfile-dev \ libkrb5-dev \ libgbm1 \ - rpm + rpm \ + bubblewrap \ + socat sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb sudo chmod +x /etc/init.d/xvfb sudo update-rc.d xvfb defaults diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 68e65fd1298..952938c0df4 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -10,7 +10,7 @@ permissions: {} jobs: compile: name: Compile - runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + runs-on: ubuntu-22.04 steps: - name: Checkout microsoft/vscode uses: actions/checkout@v6 @@ -86,7 +86,7 @@ jobs: linux: name: Linux - runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + runs-on: ubuntu-22.04 env: NPM_ARCH: x64 VSCODE_ARCH: x64 diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index 2bde317b480..c9ac6ef0374 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -17,7 +17,7 @@ on: jobs: windows-test: name: ${{ inputs.job_name }} - runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-windows-2022-x64 ] + runs-on: windows-2022 env: ARTIFACT_NAME: ${{ (inputs.electron_tests && 'electron') || (inputs.browser_tests && 'browser') || (inputs.remote_tests && 'remote') || 'unknown' }} NPM_ARCH: x64 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bb87ac077bc..c470f35e069 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,7 +10,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: {} +permissions: + contents: read env: VSCODE_QUALITY: 'oss' @@ -18,7 +19,7 @@ env: jobs: compile: name: Compile & Hygiene - runs-on: [ self-hosted, 1ES.Pool=1es-vscode-oss-ubuntu-22.04-x64 ] + runs-on: ubuntu-22.04 steps: - name: Checkout microsoft/vscode uses: actions/checkout@v6 diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index c3107065279..01f186a1c81 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -18,7 +18,6 @@ concurrency: jobs: screenshots: - if: false # temporarily disabled name: Checking Component Screenshots runs-on: ubuntu-latest steps: @@ -55,12 +54,12 @@ jobs: run: npx playwright install chromium - name: Capture screenshots - run: npx component-explorer screenshot --project ./test/componentFixtures/component-explorer.json + run: ./node_modules/.bin/component-explorer screenshot --project ./test/componentFixtures/component-explorer.json - name: Compare screenshots id: compare run: | - npx component-explorer screenshot:compare \ + ./node_modules/.bin/component-explorer screenshot:compare \ --project ./test/componentFixtures \ --report ./test/componentFixtures/.screenshots/report continue-on-error: true @@ -93,19 +92,33 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | REPORT="test/componentFixtures/.screenshots/report/report.json" + STATE="success" if [ -f "$REPORT" ]; then CHANGED=$(node -e "const r = require('./$REPORT'); console.log(r.summary.added + r.summary.removed + r.summary.changed)") TITLE="⚠ ${CHANGED} screenshots changed" + BLOCKS_CI=$(node -e " + const r = require('./$REPORT'); + const blocking = Object.entries(r.fixtures).filter(([, f]) => + f.status !== 'unchanged' && (f.labels || []).includes('blocks-ci') + ); + if (blocking.length > 0) { + console.log(blocking.map(([name]) => name).join(', ')); + } + ") + if [ -n "$BLOCKS_CI" ]; then + STATE="failure" + TITLE="❌ ${CHANGED} screenshots changed (blocks CI: ${BLOCKS_CI})" + fi else TITLE="✅ Screenshots match" fi SHA="${{ github.event.pull_request.head.sha || github.sha }}" - DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json" + DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json&search=changed" gh api "repos/${{ github.repository }}/statuses/$SHA" \ --input - < Daniel Imms Raymond Zhao Tyler Leonhardt Tyler Leonhardt João Moreno João Moreno diff --git a/.npmrc b/.npmrc index a275846ab5c..e846219142b 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="39.8.0" -ms_build_id="13470701" +target="39.8.3" +ms_build_id="13658728" runtime="electron" ignore-scripts=false build_from_source="true" diff --git a/.nvmrc b/.nvmrc index 85e502778f6..32a2d7bd80d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.22.0 +22.22.1 diff --git a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts index 8927b0b7064..f21e36604fb 100644 --- a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts +++ b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts @@ -112,7 +112,7 @@ export class NpmUpToDateFeature extends vscode.Disposable { } try { const script = path.join(workspaceRoot, 'build', 'npm', 'installStateHash.ts'); - const output = cp.execFileSync(process.execPath, [script], { + const output = cp.execFileSync(process.execPath, [script, '--ignore-node-version'], { cwd: workspaceRoot, timeout: 10_000, encoding: 'utf8', diff --git a/.vscode/launch.json b/.vscode/launch.json index 47d901042e3..24c1abde456 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,6 +52,15 @@ "${workspaceFolder}/out/**/*.js" ] }, + { + "type": "node", + "request": "attach", + "name": "Attach to Agent Host Process", + "port": 5878, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + }, { "type": "node", "request": "attach", @@ -651,7 +660,7 @@ "name": "Component Explorer (Edge)", "type": "msedge", "request": "launch", - "url": "http://localhost:5337/___explorer", + "url": "${taskVar:componentExplorerUrl}", "preLaunchTask": "Launch Component Explorer", "presentation": { "group": "1_component_explorer", @@ -662,7 +671,7 @@ "name": "Component Explorer (Chrome)", "type": "chrome", "request": "launch", - "url": "http://localhost:5337/___explorer", + "url": "${taskVar:componentExplorerUrl}", "preLaunchTask": "Launch Component Explorer", "presentation": { "group": "1_component_explorer", @@ -701,6 +710,7 @@ "Attach to Main Process", "Attach to Extension Host", "Attach to Shared Process", + "Attach to Agent Host Process" ], "preLaunchTask": "Ensure Prelaunch Dependencies", "presentation": { diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 40235fad54e..c039ae27ce8 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,82 +7,32 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.111.0\"" + "value": "$MILESTONE=milestone:\"1.113.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" }, { "kind": 1, "language": "markdown", - "value": "# Preparation" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Open Pull Requests on the Milestone" + "value": "## Prep: Open PRs and Issues" }, { "kind": 2, "language": "github-issues", - "value": "org:microsoft $MILESTONE is:pr is:open" + "value": "org:microsoft $MILESTONE is:issue is:open -label:iteration-plan -label:endgame-plan -label:testplan-item\norg:microsoft $MILESTONE is:pr is:open" }, { "kind": 1, "language": "markdown", - "value": "## Unverified Older Insiders-Released Issues" + "value": "## Verification: Missing Steps" }, { "kind": 2, "language": "github-issues", - "value": "org:microsoft -$MILESTONE is:issue is:closed reason:completed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan -label:error-telemetry" + "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:verification-steps-needed -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified -label:unreleased -label:*not-reproducible" }, { "kind": 1, "language": "markdown", - "value": "## Unverified Older Insiders-Released Feature Requests" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft -$MILESTONE is:issue is:closed reason:completed label:feature-request label:insiders-released -label:on-testplan -label:verified -label:*duplicate -label:error-telemetry" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Open Issues on the Milestone" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:open -label:iteration-plan -label:endgame-plan -label:testplan-item" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Feature Requests Missing Labels" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed label:feature-request -label:verification-needed -label:on-testplan -label:verified -label:*duplicate" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Open Test Plan Items without milestone" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:open label:testplan-item no:milestone" - }, - { - "kind": 1, - "language": "markdown", - "value": "# Testing" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Test Plan Items" + "value": "## Testing & Verification" }, { "kind": 2, @@ -92,66 +42,11 @@ { "kind": 1, "language": "markdown", - "value": "## Verification Needed" + "value": "These are bugs we created and closed after running the TPI tool. They need to be `verified` manually" }, { "kind": 2, "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed label:verification-needed -label:verified -label:on-testplan" - }, - { - "kind": 1, - "language": "markdown", - "value": "# Verification" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Verifiable Fixes" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified -label:unreleased -label:*not-reproducible -label:*out-of-scope" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Verifiable Fixes Missing Steps" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug label:verification-steps-needed -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified -label:unreleased -label:*not-reproducible" - }, - { - "kind": 1, - "language": "markdown", - "value": "## Unreleased Fixes" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified label:unreleased -label:*not-reproducible" - }, - { - "kind": 1, - "language": "markdown", - "value": "## All Unverified Fixes" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified -label:*not-reproducible" - }, - { - "kind": 1, - "language": "markdown", - "value": "# Candidates" - }, - { - "kind": 2, - "language": "github-issues", - "value": "org:microsoft $MILESTONE is:issue is:open label:candidate" + "value": "org:microsoft $MILESTONE is:issue is:closed label:bug reason:completed -label:verified created:>=$TPI_CREATION" } ] \ No newline at end of file diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index f44d9c4a45b..8ee3e6cbe3b 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,12 +7,12 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.111.0\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.113.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, "language": "github-issues", - "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintangent -author:jukasper -author:zhichli" + "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintangent -author:jukasper -author:zhichli" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index c4bc569e9da..e5c0bd60fbb 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce repo:microsoft/vscode-copilot-issues repo:microsoft/vscode-extension-samples\n\n// current milestone name\n$MILESTONE=milestone:\"March 2026\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce repo:microsoft/vscode-copilot-issues repo:microsoft/vscode-extension-samples\n\n// current milestone name\n$MILESTONE=milestone:\"1.114.0\"\n" }, { "kind": 1, diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index 1c7e9dc1843..84e36a975d5 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -32,7 +32,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos $milestone is:closed reason:completed -assignee:@me label:bug -label:verified -label:*duplicate -author:@me -assignee:@me label:bug -label:verified -author:@me -author:aeschli -author:alexdima -author:alexr00 -author:bpasero -author:chrisdias -author:chrmarti -author:connor4312 -author:dbaeumer -author:deepak1556 -author:eamodio -author:egamma -author:gregvanl -author:isidorn -author:JacksonKearl -author:joaomoreno -author:jrieken -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:RMacfarlane -author:roblourens -author:sana-ajani -author:sandy081 -author:sbatten -author:Tyriar -author:weinand -author:rzhao271 -author:kieferrm -author:TylerLeonhardt -author:bamurtaugh -author:hediet -author:joyceerhl -author:rchiodo" + "value": "$repos $milestone is:closed reason:completed -assignee:@me label:bug -label:verified -label:*duplicate -author:@me -assignee:@me label:bug -label:verified -author:@me -author:aeschli -author:alexdima -author:alexr00 -author:bpasero -author:chrisdias -author:chrmarti -author:connor4312 -author:dbaeumer -author:deepak1556 -author:eamodio -author:egamma -author:gregvanl -author:isidorn -author:JacksonKearl -author:joaomoreno -author:jrieken -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:RMacfarlane -author:roblourens -author:sana-ajani -author:sandy081 -author:sbatten -author:weinand -author:rzhao271 -author:kieferrm -author:TylerLeonhardt -author:bamurtaugh -author:hediet -author:joyceerhl -author:rchiodo" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index bd45f6441fa..99f937bdf9d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,6 @@ "chat.tools.edits.autoApprove": { ".github/skills/azure-pipelines/azure-pipeline.ts": false }, - "chat.viewSessions.enabled": true, "chat.editing.explainChanges.enabled": true, // --- Editor --- "editor.insertSpaces": false, @@ -72,6 +71,7 @@ "extensions/terminal-suggest/src/completions/upstream/**": true, "test/smoke/out/**": true, "test/automation/out/**": true, + "src/vs/platform/agentHost/common/state/protocol/**": true, "test/integration/browser/out/**": true, // "src/vs/sessions/**": true }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e6bf967ddf0..e0b2a048d9a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -235,20 +235,29 @@ "command": ".\\scripts\\code.bat" }, "args": [ - "--sessions" + "--sessions", + "--user-data-dir=${userHome}/.vscode-oss-sessions-dev", + "--extensions-dir=${userHome}/.vscode-oss-sessions-dev/extensions" ], "problemMatcher": [] }, { - "label": "Run and Compile Dev Sessions", - "type": "shell", - "command": "npm run transpile-client && ./scripts/code.sh", - "windows": { - "command": "npm run transpile-client && .\\scripts\\code.bat" - }, - "args": [ - "--sessions" - ], + "label": "Transpile Client", + "type": "npm", + "script": "transpile-client", + "problemMatcher": [] + }, + { + "label": "Run and Compile Sessions - OSS", + "dependsOn": ["Transpile Client", "Run Dev Sessions"], + "dependsOrder": "sequence", + "inSessions": true, + "problemMatcher": [] + }, + { + "label": "Run and Compile Code - OSS", + "dependsOn": ["Transpile Client", "Run Dev"], + "dependsOrder": "sequence", "inSessions": true, "problemMatcher": [] }, @@ -386,8 +395,9 @@ { "label": "Launch Component Explorer", "type": "shell", - "command": "npx component-explorer serve -c ./test/componentFixtures/component-explorer.json -vv", + "command": "npx component-explorer serve -c ./test/componentFixtures/component-explorer.json -vv --kill-if-running", "isBackground": true, + "inSessions": true, "problemMatcher": { "owner": "component-explorer", "fileLocation": "absolute", @@ -400,9 +410,32 @@ "background": { "activeOnStart": true, "beginsPattern": ".*Setting up sessions.*", - "endsPattern": "Redirection server listening on.*" + "endsPattern": " current: (?.*) \\(current\\)" } } + }, + { + "label": "Install & Watch", + "type": "shell", + "command": "npm ci && npm run watch", + "windows": { + "command": "cmd /c \"npm ci && npm run watch\"" + }, + "inSessions": true, + "runOptions": { + "runOn": "worktreeCreated" + } + }, + { + "label": "Echo E2E Status", + "type": "shell", + "command": "pwsh", + "args": [ + "-NoProfile", + "-Command", + "Write-Output \"134 passed, 0 failed, 1 skipped, 135 total\"; Start-Sleep -Seconds 2; Write-Output \"[PASS] E2E Tests\"; Write-Output \"Watching for changes...\"" + ], + "isBackground": false } ] } diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 0a15b3ff5fc..c4883fd0ad0 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -524,6 +524,580 @@ Title to copyright in this work will at all times remain with copyright holders. --------------------------------------------------------- +dompurify 3.2.7 - Apache 2.0 +https://github.com/cure53/DOMPurify + +DOMPurify +Copyright 2025 Dr.-Ing. Mario Heiderich, Cure53 + +DOMPurify is free software; you can redistribute it and/or modify it under the +terms of either: + +a) the Apache License Version 2.0, or +b) the Mozilla Public License Version 2.0 + +----------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +----------------------------------------------------------------------------- +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. +--------------------------------------------------------- + +--------------------------------------------------------- + dotenv-org/dotenv-vscode 0.26.0 - MIT License https://github.com/dotenv-org/dotenv-vscode @@ -2277,7 +2851,7 @@ written authorization of the copyright holder. --------------------------------------------------------- -vscode-codicons 0.0.41 - MIT and Creative Commons Attribution 4.0 +vscode-codicons 0.0.46-0 - MIT and Creative Commons Attribution 4.0 https://github.com/microsoft/vscode-codicons Attribution 4.0 International diff --git a/build/.moduleignore b/build/.moduleignore index ed36151130c..faa4973e2dc 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -188,3 +188,28 @@ zone.js/dist/** @xterm/xterm-addon-*/fixtures/** @xterm/xterm-addon-*/out/** @xterm/xterm-addon-*/out-test/** + +# @github/copilot - strip unneeded binaries and files +@github/copilot/sdk/index.js +@github/copilot/prebuilds/** +@github/copilot/clipboard/** +@github/copilot/ripgrep/** +@github/copilot/**/keytar.node + +# @github/copilot platform binaries - not needed +@github/copilot-darwin-arm64/** +@github/copilot-darwin-x64/** +@github/copilot-linux-arm64/** +@github/copilot-linux-x64/** +@github/copilot-win32-arm64/** +@github/copilot-win32-x64/** + +# @github/copilot-sdk - strip the nested @github/copilot CLI runtime +# The SDK only needs its own dist/ files; the CLI is resolved via cliPath at runtime +@github/copilot-sdk/node_modules/@github/copilot/** +@github/copilot-sdk/node_modules/@github/copilot-darwin-arm64/** +@github/copilot-sdk/node_modules/@github/copilot-darwin-x64/** +@github/copilot-sdk/node_modules/@github/copilot-linux-arm64/** +@github/copilot-sdk/node_modules/@github/copilot-linux-x64/** +@github/copilot-sdk/node_modules/@github/copilot-win32-arm64/** +@github/copilot-sdk/node_modules/@github/copilot-win32-x64/** diff --git a/build/.npmrc b/build/.npmrc index 551822f79cd..f1c087f86b5 100644 --- a/build/.npmrc +++ b/build/.npmrc @@ -4,3 +4,4 @@ build_from_source="true" legacy-peer-deps="true" force_process_config="true" timeout=180000 +min-release-age="1" diff --git a/build/azure-pipelines/common/releaseBuild.ts b/build/azure-pipelines/common/releaseBuild.ts index 3cd8082308e..708978a130c 100644 --- a/build/azure-pipelines/common/releaseBuild.ts +++ b/build/azure-pipelines/common/releaseBuild.ts @@ -66,15 +66,8 @@ async function main(force: boolean): Promise { console.log(`Releasing build ${commit}...`); - let rolloutDurationMs = undefined; - - // If the build is insiders or exploration, start a rollout of 4 hours - if (quality === 'insider') { - rolloutDurationMs = 4 * 60 * 60 * 1000; // 4 hours - } - const scripts = client.database('builds').container(quality).scripts; - await retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit, rolloutDurationMs])); + await retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); } const [, , force] = process.argv; diff --git a/build/azure-pipelines/common/sanity-tests.yml b/build/azure-pipelines/common/sanity-tests.yml index ce6d95dd7e5..fdf6b2cd3dd 100644 --- a/build/azure-pipelines/common/sanity-tests.yml +++ b/build/azure-pipelines/common/sanity-tests.yml @@ -33,9 +33,9 @@ jobs: outputs: - output: pipelineArtifact targetPath: $(SCREENSHOTS_DIR) - artifactName: screenshots-${{ parameters.name }} + artifactName: screenshots-${{ parameters.name }}-$(System.JobAttempt) displayName: Publish Screenshots - condition: succeededOrFailed() + condition: and(succeededOrFailed(), eq(variables.HAS_SCREENSHOTS, 'true')) continueOnError: true sbomEnabled: false variables: @@ -171,6 +171,25 @@ jobs: condition: and(succeeded(), ne(variables.DOCKER_CACHE_HIT, 'true')) displayName: Save Docker Image + - ${{ if eq(parameters.os, 'windows') }}: + - script: | + @echo off + dir /b "$(SCREENSHOTS_DIR)" 2>nul | findstr . >nul + if %errorlevel%==0 ( + echo ##vso[task.setvariable variable=HAS_SCREENSHOTS]true + ) + exit /b 0 + displayName: Check Screenshots + condition: succeededOrFailed() + + - ${{ else }}: + - bash: | + if [ -n "$(ls -A "$(SCREENSHOTS_DIR)" 2>/dev/null)" ]; then + echo "##vso[task.setvariable variable=HAS_SCREENSHOTS]true" + fi + displayName: Check Screenshots + condition: succeededOrFailed() + - task: PublishTestResults@2 inputs: testResultsFormat: JUnit diff --git a/build/azure-pipelines/product-npm-package-validate.yml b/build/azure-pipelines/dependencies-check.yml similarity index 80% rename from build/azure-pipelines/product-npm-package-validate.yml rename to build/azure-pipelines/dependencies-check.yml index 37483396b23..2c0d32b751a 100644 --- a/build/azure-pipelines/product-npm-package-validate.yml +++ b/build/azure-pipelines/dependencies-check.yml @@ -1,8 +1,16 @@ trigger: none -pr: - branches: - include: ["main"] +pr: none + +parameters: + - name: GITHUB_APP_ID + type: string + - name: GITHUB_APP_INSTALLATION_ID + type: string + - name: GITHUB_APP_PRIVATE_KEY + type: string + - name: GITHUB_CHECK_RUN_ID + type: string variables: - name: NPM_REGISTRY @@ -12,11 +20,11 @@ variables: jobs: - job: ValidateNpmPackages - displayName: Valiate NPM packages against Terrapin + displayName: Validate package-lock.json, Cargo.lock changes via Azure DevOps pipeline pool: name: 1es-ubuntu-22.04-x64 os: linux - timeoutInMinutes: 40000 + timeoutInMinutes: 1300 variables: VSCODE_ARCH: x64 steps: @@ -75,10 +83,10 @@ jobs: - script: | set -e - for attempt in {1..12}; do + for attempt in {1..120}; do if [ $attempt -gt 1 ]; then - echo "Attempt $attempt: Waiting for 30 minutes before retrying..." - sleep 1800 + echo "Attempt $attempt: Waiting for 10 minutes before retrying..." + sleep 600 fi echo "Attempt $attempt: Running npm ci" @@ -94,7 +102,7 @@ jobs: fi done - echo "npm i failed after 12 attempts" + echo "giving up after 120 attempts (20 hours)" exit 1 env: npm_command: 'install --ignore-scripts' @@ -102,7 +110,7 @@ jobs: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install dependencies with retries - timeoutInMinutes: 400 + timeoutInMinutes: 1300 condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) - script: | @@ -114,3 +122,13 @@ jobs: - script: .github/workflows/check-clean-git-state.sh displayName: Check clean git state condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) + + - script: node build/azure-pipelines/update-dependencies-check.ts + displayName: Update GitHub check run + condition: always() + env: + GITHUB_APP_ID: ${{ parameters.GITHUB_APP_ID }} + GITHUB_APP_INSTALLATION_ID: ${{ parameters.GITHUB_APP_INSTALLATION_ID }} + GITHUB_APP_PRIVATE_KEY: ${{ parameters.GITHUB_APP_PRIVATE_KEY }} + CHECK_RUN_ID: ${{ parameters.GITHUB_CHECK_RUN_ID }} + AGENT_JOBSTATUS: $(Agent.JobStatus) diff --git a/build/azure-pipelines/github-check-run.js b/build/azure-pipelines/github-check-run.js new file mode 100644 index 00000000000..e6c2a8a892d --- /dev/null +++ b/build/azure-pipelines/github-check-run.js @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +const crypto = require('crypto'); +const https = require('https'); + +/** + * @param {string} appId + * @param {string} privateKey + * @returns {string} + */ +function createJwt(appId, privateKey) { + const now = Math.floor(Date.now() / 1000); + const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url'); + const payload = Buffer.from(JSON.stringify({ iat: now - 60, exp: now + 600, iss: appId })).toString('base64url'); + const signature = crypto.sign('sha256', Buffer.from(`${header}.${payload}`), privateKey).toString('base64url'); + return `${header}.${payload}.${signature}`; +} + +/** + * @param {import('https').RequestOptions} options + * @param {object} [body] + * @returns {Promise} + */ +function request(options, body) { + return new Promise((resolve, reject) => { + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + resolve(JSON.parse(data)); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }); + req.on('error', reject); + if (body) { + req.write(JSON.stringify(body)); + } + req.end(); + }); +} + +/** + * @param {string} jwt + * @param {string} installationId + * @returns {Promise} + */ +async function getInstallationToken(jwt, installationId) { + /** @type {{ token: string }} */ + const result = await request({ + hostname: 'api.github.com', + path: `/app/installations/${encodeURIComponent(installationId)}/access_tokens`, + method: 'POST', + headers: { + 'Authorization': `Bearer ${jwt}`, + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'VSCode-ADO-Pipeline', + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + return result.token; +} + +/** + * @param {string} token + * @param {string} checkRunId + * @param {string} conclusion + * @param {string} detailsUrl + */ +function updateCheckRun(token, checkRunId, conclusion, detailsUrl) { + return request({ + hostname: 'api.github.com', + path: `/repos/microsoft/vscode/check-runs/${encodeURIComponent(checkRunId)}`, + method: 'PATCH', + headers: { + 'Authorization': `token ${token}`, + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'VSCode-ADO-Pipeline', + 'X-GitHub-Api-Version': '2022-11-28' + } + }, { + status: 'completed', + conclusion, + completed_at: new Date().toISOString(), + details_url: detailsUrl + }); +} + +async function main() { + const appId = process.env.GITHUB_APP_ID; + const privateKey = process.env.GITHUB_APP_PRIVATE_KEY; + const installationId = process.env.GITHUB_APP_INSTALLATION_ID; + const checkRunId = process.env.CHECK_RUN_ID; + const jobStatus = process.env.AGENT_JOBSTATUS; + const detailsUrl = `${process.env.SYSTEM_COLLECTIONURI}${process.env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${process.env.BUILD_BUILDID}`; + + if (!appId || !privateKey || !installationId || !checkRunId) { + throw new Error('Missing required environment variables'); + } + + const jwt = createJwt(appId, privateKey); + const token = await getInstallationToken(jwt, installationId); + + /** @type {string} */ + let conclusion; + switch (jobStatus) { + case 'Succeeded': + case 'SucceededWithIssues': + conclusion = 'success'; + break; + case 'Canceled': + conclusion = 'cancelled'; + break; + default: + conclusion = 'failure'; + break; + } + + await updateCheckRun(token, checkRunId, conclusion, detailsUrl); + console.log(`Updated check run ${checkRunId} with conclusion: ${conclusion}`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/linux/product-build-linux-node-modules.yml b/build/azure-pipelines/linux/product-build-linux-node-modules.yml index 290a3fe1b29..ad0e1498160 100644 --- a/build/azure-pipelines/linux/product-build-linux-node-modules.yml +++ b/build/azure-pipelines/linux/product-build-linux-node-modules.yml @@ -41,7 +41,9 @@ jobs: libxkbfile-dev \ libkrb5-dev \ libgbm1 \ - rpm + rpm \ + bubblewrap \ + socat sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb sudo chmod +x /etc/init.d/xvfb sudo update-rc.d xvfb defaults diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index 82e1a19107d..4b5a5d08cd0 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -48,7 +48,9 @@ steps: libxkbfile-dev \ libkrb5-dev \ libgbm1 \ - rpm + rpm \ + bubblewrap \ + socat sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb sudo chmod +x /etc/init.d/xvfb sudo update-rc.d xvfb defaults @@ -345,7 +347,7 @@ steps: - script: | set -e npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-snap" - sudo -E docker run -e VSCODE_ARCH -e VSCODE_QUALITY -v $(pwd):/work -w /work vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64 /bin/bash -c "./build/azure-pipelines/linux/build-snap.sh" + sudo -E docker run -e VSCODE_ARCH -e VSCODE_QUALITY -v $(pwd):/work -w /work vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64@sha256:ab4a88c4d85e0d7a85acabba59543f7143f575bab2c0b2b07f5b77d4a7e491ff /bin/bash -c "./build/azure-pipelines/linux/build-snap.sh" SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)" SNAP_EXTRACTED_PATH=$(find $SNAP_ROOT -maxdepth 1 -type d -name 'code-*') diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index e016db50686..fa1cc1a7699 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -122,7 +122,7 @@ variables: - name: VSCODE_BUILD_STAGE_WEB value: ${{ eq(parameters.VSCODE_BUILD_WEB, true) }} - name: VSCODE_CIBUILD - value: ${{ or(in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), eq(parameters.VSCODE_BUILD_TYPE, 'CI')) }} + value: ${{ or(and(in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'))), eq(parameters.VSCODE_BUILD_TYPE, 'CI')) }} - name: VSCODE_PUBLISH value: ${{ and(eq(parameters.VSCODE_PUBLISH, true), eq(variables.VSCODE_CIBUILD, false)) }} - name: VSCODE_SCHEDULEDBUILD @@ -710,7 +710,7 @@ extends: baseImage: ubuntu:24.04 arch: arm64 - - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: + - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false), or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'))) }}: - stage: ApproveRelease dependsOn: [] # run in parallel to compile stage pool: @@ -739,6 +739,50 @@ extends: parameters: VSCODE_RELEASE: ${{ parameters.VSCODE_RELEASE }} + - ${{ if and(in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}: + - stage: TriggerStableBuild + displayName: Trigger Stable Build + dependsOn: [] + pool: + name: 1es-ubuntu-22.04-x64 + os: linux + jobs: + - job: TriggerStableBuild + displayName: Trigger Stable Build + steps: + - checkout: none + - script: | + set -e + node -e ' + async function main() { + const body = JSON.stringify({ + definition: { id: Number(process.env.DEFINITION_ID) }, + sourceBranch: process.env.SOURCE_BRANCH, + sourceVersion: process.env.SOURCE_VERSION, + templateParameters: { VSCODE_QUALITY: "stable", VSCODE_RELEASE: "false" } + }); + console.log(`Triggering stable build on ${process.env.SOURCE_BRANCH} @ ${process.env.SOURCE_VERSION}...`); + const response = await fetch(process.env.BUILDS_API_URL, { + method: "POST", + headers: { "Authorization": `Bearer ${process.env.SYSTEM_ACCESSTOKEN}`, "Content-Type": "application/json" }, + body + }); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}: ${await response.text()}`); + } + const build = await response.json(); + console.log(`Build queued successfully — ID: ${build.id}, URL: ${build._links.web.href}`); + } + main().catch(err => { console.error(err); process.exit(1); }); + ' + displayName: Queue stable build + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + DEFINITION_ID: $(System.DefinitionId) + SOURCE_BRANCH: $(Build.SourceBranch) + SOURCE_VERSION: $(Build.SourceVersion) + BUILDS_API_URL: $(System.TeamFoundationCollectionUri)$(System.TeamProject)/_apis/build/builds?api-version=7.0 + - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - stage: node_modules dependsOn: [] diff --git a/build/azure-pipelines/product-sanity-tests.yml b/build/azure-pipelines/product-sanity-tests.yml index ade0b96878b..8f555f30a1f 100644 --- a/build/azure-pipelines/product-sanity-tests.yml +++ b/build/azure-pipelines/product-sanity-tests.yml @@ -15,7 +15,6 @@ parameters: - name: buildCommit displayName: Published Build Commit type: string - default: '' - name: npmRegistry displayName: Custom NPM Registry URL @@ -28,17 +27,9 @@ variables: - name: Codeql.SkipTaskAutoInjection value: true - name: BUILD_COMMIT - ${{ if ne(parameters.buildCommit, '') }}: - value: ${{ parameters.buildCommit }} - ${{ else }}: - value: $(resources.pipeline.vscode.sourceCommit) + value: ${{ parameters.buildCommit }} - name: BUILD_QUALITY - ${{ if ne(parameters.buildCommit, '') }}: - value: ${{ parameters.buildQuality }} - ${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') }}: - value: stable - ${{ else }}: - value: insider + value: ${{ parameters.buildQuality }} - name: NPM_REGISTRY value: ${{ parameters.npmRegistry }} @@ -50,17 +41,6 @@ resources: type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release - pipelines: - - pipeline: vscode - # allow-any-unicode-next-line - source: '⭐️ VS Code' - trigger: - stages: - - Publish - branches: - include: - - main - - release/* extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines diff --git a/build/azure-pipelines/update-dependencies-check.ts b/build/azure-pipelines/update-dependencies-check.ts new file mode 100644 index 00000000000..5923770fc2e --- /dev/null +++ b/build/azure-pipelines/update-dependencies-check.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import crypto from 'crypto'; +import https from 'https'; + +function createJwt(appId: string, privateKey: string): string { + const now = Math.floor(Date.now() / 1000); + const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url'); + const payload = Buffer.from(JSON.stringify({ iat: now - 60, exp: now + 600, iss: appId })).toString('base64url'); + const signature = crypto.sign('sha256', Buffer.from(`${header}.${payload}`), privateKey).toString('base64url'); + return `${header}.${payload}.${signature}`; +} + +function request(options: https.RequestOptions, body?: object): Promise> { + return new Promise((resolve, reject) => { + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + resolve(JSON.parse(data)); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }); + req.on('error', reject); + if (body) { + req.write(JSON.stringify(body)); + } + req.end(); + }); +} + +async function getInstallationToken(jwt: string, installationId: string): Promise { + const result = await request({ + hostname: 'api.github.com', + path: `/app/installations/${encodeURIComponent(installationId)}/access_tokens`, + method: 'POST', + headers: { + 'Authorization': `Bearer ${jwt}`, + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'VSCode-ADO-Pipeline', + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + return result.token as string; +} + +function updateCheckRun(token: string, checkRunId: string, conclusion: string, detailsUrl: string) { + return request({ + hostname: 'api.github.com', + path: `/repos/microsoft/vscode/check-runs/${encodeURIComponent(checkRunId)}`, + method: 'PATCH', + headers: { + 'Authorization': `token ${token}`, + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'VSCode-ADO-Pipeline', + 'X-GitHub-Api-Version': '2022-11-28' + } + }, { + status: 'completed', + conclusion, + completed_at: new Date().toISOString(), + details_url: detailsUrl + }); +} + +async function main() { + const appId = process.env.GITHUB_APP_ID; + const privateKey = process.env.GITHUB_APP_PRIVATE_KEY; + const installationId = process.env.GITHUB_APP_INSTALLATION_ID; + const checkRunId = process.env.CHECK_RUN_ID; + const jobStatus = process.env.AGENT_JOBSTATUS; + const detailsUrl = `${process.env.SYSTEM_COLLECTIONURI}${process.env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${process.env.BUILD_BUILDID}`; + + if (!appId || !privateKey || !installationId || !checkRunId) { + throw new Error('Missing required environment variables'); + } + + const jwt = createJwt(appId, privateKey); + const token = await getInstallationToken(jwt, installationId); + + let conclusion: string; + switch (jobStatus) { + case 'Succeeded': + case 'SucceededWithIssues': + conclusion = 'success'; + break; + case 'Canceled': + conclusion = 'cancelled'; + break; + default: + conclusion = 'failure'; + break; + } + + await updateCheckRun(token, checkRunId, conclusion, detailsUrl); + console.log(`Updated check run ${checkRunId} with conclusion: ${conclusion}`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/win32/product-build-win32-cli.yml b/build/azure-pipelines/win32/product-build-win32-cli.yml index 78461a959ed..20e49d34866 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli.yml @@ -120,6 +120,9 @@ jobs: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Codesign + - powershell: Remove-Item -Path "$(Build.ArtifactStagingDirectory)/sign/CodeSignSummary*.md" -Force -ErrorAction SilentlyContinue + displayName: Remove CodeSignSummary + - task: ArchiveFiles@2 displayName: Archive signed CLI inputs: diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index e3356effa95..2580588a743 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -100,7 +100,11 @@ steps: env: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - - powershell: npm run compile + - template: ../common/install-builtin-extensions.yml@self + + - powershell: npm run gulp core-ci + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile - powershell: npm run gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" diff --git a/build/buildfile.ts b/build/buildfile.ts index 47b0476892c..80c97ff1daa 100644 --- a/build/buildfile.ts +++ b/build/buildfile.ts @@ -24,6 +24,7 @@ export const workbenchDesktop = [ createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), createModuleDescription('vs/platform/files/node/watcher/watcherMain'), createModuleDescription('vs/platform/terminal/node/ptyHostMain'), + createModuleDescription('vs/platform/agentHost/node/agentHostMain'), createModuleDescription('vs/workbench/api/node/extensionHostProcess'), createModuleDescription('vs/workbench/workbench.desktop.main'), createModuleDescription('vs/sessions/sessions.desktop.main') @@ -53,7 +54,8 @@ export const codeServer = [ // 'vs/server/node/server.cli' is not included here because it gets inlined via ./src/server-cli.js createModuleDescription('vs/workbench/api/node/extensionHostProcess'), createModuleDescription('vs/platform/files/node/watcher/watcherMain'), - createModuleDescription('vs/platform/terminal/node/ptyHostMain') + createModuleDescription('vs/platform/terminal/node/ptyHostMain'), + createModuleDescription('vs/platform/agentHost/node/agentHostMain') ]; export const entrypoint = createModuleDescription; diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 5d8343f42a9..3a0e930f3f4 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -d70954386008ad2c65d9849bb89955ab3c7dd08763256ae0d91d8604e8894d64 *chromedriver-v39.8.0-darwin-arm64.zip -2f6b654337133c13440aafdaf9e8b15f5ebb244e7d49f20977f03438e9bb8adb *chromedriver-v39.8.0-darwin-x64.zip -ef8681bb6b6af42cdf0e14c9ce188f035e01620781308c06cd3c6b922aaea2e6 *chromedriver-v39.8.0-linux-arm64.zip -c03fea6ac2b743d771407dc5f58809f44d2a885b1830b847957823cac2e7b222 *chromedriver-v39.8.0-linux-armv7l.zip -4bb7c6d9b3a7bfdd89edd0db98e63599ebf6dacdb888d5985bbb73f6153acc0c *chromedriver-v39.8.0-linux-x64.zip -aad1f6f970b5636d637c1c242766fbaa5bebe2707a605a38aadc7b40724b3d11 *chromedriver-v39.8.0-mas-arm64.zip -e89ebebe3a135d3ce40168152a0aabfd055b9fa6b118262a6df18405fd2ea433 *chromedriver-v39.8.0-mas-x64.zip -232e1a0460f6a59056499cccfff3265bf92eae22f20f02f2419e5e49552aaed7 *chromedriver-v39.8.0-win32-arm64.zip -ab92f46cc55da7c719175b50203c734781828389b8b3a1a535204bf0dc7d1296 *chromedriver-v39.8.0-win32-ia32.zip -a40eb521063e4ea6791ed4005815fa8ac259c1febc850246a83a47ce120121ce *chromedriver-v39.8.0-win32-x64.zip -d6a33b4c3c0de845ea23d1e2614c6c6d3bbe35b771bb63ae521c4db11373b021 *electron-api.json -5425323fdb23167870075e944ec6cf3ae383fbe45ad141d08b1d9689030ccd05 *electron-v39.8.0-darwin-arm64-dsym-snapshot.zip -aa32ab00ee58d8827cd53ca561b8c26b7cb7e2ad8cb0801acdda117ee728388e *electron-v39.8.0-darwin-arm64-dsym.zip -f94e589804a3394a4735543b888927be873f8f402899d0debe32a9dc570d6285 *electron-v39.8.0-darwin-arm64-symbols.zip -681d82c2ec6677ff0bf12f5bb1808b5a51dcbf10894bd0298641015119a3e04d *electron-v39.8.0-darwin-arm64.zip -a95e83b5cde762a37e64229e5669b0c19b95aac148689d96ca344535109eb983 *electron-v39.8.0-darwin-x64-dsym-snapshot.zip -8c989d8ca835ecdd93d49d9627f5548272c0ed03e263392b21ed287960b29e41 *electron-v39.8.0-darwin-x64-dsym.zip -b4b6fda9c5b9063a104318645aa29ef4738dd099da2b722e3e9b6dde5e098418 *electron-v39.8.0-darwin-x64-symbols.zip -ec53f2ba79498410323bb96a19ce98741bf28666cc9d83e07d11dadcc5506f38 *electron-v39.8.0-darwin-x64.zip -9141e64f9d4ea7f0e6a43ae364c8232a0dac79ecec44de2d4a0e5d688fbb742c *electron-v39.8.0-linux-arm64-debug.zip -5fac949d5331abaff0643dbcda7cc187e548cd4bf9d198c1ffc361383bfaa79f *electron-v39.8.0-linux-arm64-symbols.zip -c9db883fa671237fbc16256cf89aba55b9fcfbd9825fec32a6d57724a6446fe1 *electron-v39.8.0-linux-arm64.zip -b26ac10e84f6b7d338c13a38547aa66b5e9afbe2f1355b183ebc2ff8f428cfa9 *electron-v39.8.0-linux-armv7l-debug.zip -16c47c008a8783f6c8d6387fe01ea15425161befbf4211e4667bbdd6bb806ef0 *electron-v39.8.0-linux-armv7l-symbols.zip -b1b37fd450a5081a876c2b00b6ca007d454747a7d1d8f04feb16119d6ace94c6 *electron-v39.8.0-linux-armv7l.zip -1e8039cdf60b27785771c9e3f3c4c39fad37602bb0e6b75a30f83c57fdbef069 *electron-v39.8.0-linux-x64-debug.zip -ff9ca169c6e79649dd4c5a49a82a8d4b1761b62fbe14c15c61bf534381a9f653 *electron-v39.8.0-linux-x64-symbols.zip -854076cc4c63d6d6c320df1ca3f4bd7084ef9f9bb47c7b75d80feb2c2ed920b4 *electron-v39.8.0-linux-x64.zip -91bc313cbd009435552d8d5efff5d6ed0ff15465743c2629dac1cfe99ac34e4d *electron-v39.8.0-mas-arm64-dsym-snapshot.zip -974f10f80ec6c65f8d9f2ac1ccd8c4395bb34e24e2b09dc0ff80bd351099692e *electron-v39.8.0-mas-arm64-dsym.zip -b3878bc9198cff324b7c829ce2fbea7a4ee505f2f99b0bb3c11ac5e60651be59 *electron-v39.8.0-mas-arm64-symbols.zip -48dac99c757a850b0db7b38c1b95e08270f690a7ea1b58872e45308c2f7c8c93 *electron-v39.8.0-mas-arm64.zip -1a6e4df1092f89ed46833938d6dd1b3036640037bd09f0630a369ae386a7c872 *electron-v39.8.0-mas-x64-dsym-snapshot.zip -81425eb867527341af64c00726bd462957fec4d5f073922df891d830addbc5bc *electron-v39.8.0-mas-x64-dsym.zip -748ce154e894a27b117b46354cc288dc9442fade844c637b59fe1c1f3f7c625d *electron-v39.8.0-mas-x64-symbols.zip -91f8f7d4eb1a42ac4fa0eaa93034c8e6155ccb50718f9f55541ce2be4a4ed6d0 *electron-v39.8.0-mas-x64.zip -b775b7584afb84e52b0a770e1e63a2f17384b66eeebe845e0c5c82beacaf7e93 *electron-v39.8.0-win32-arm64-pdb.zip -ac62373d11ed682b4fcdae27de2bd72ebf7d46d3b569f5fcf242de01786d0948 *electron-v39.8.0-win32-arm64-symbols.zip -b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.0-win32-arm64-toolchain-profile.zip -08b79fa5deabbcace447f1e15eb99b3b117b42a84b71ad5b0f52d2da68a34192 *electron-v39.8.0-win32-arm64.zip -f4fb798d76a0c2f80717ef1607571537dbbb07f1cc5f177048bcfd17046c2255 *electron-v39.8.0-win32-ia32-pdb.zip -37c1d2988793604294724b648589fca6459472021189abab1550d5e1eecff1a7 *electron-v39.8.0-win32-ia32-symbols.zip -b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.0-win32-ia32-toolchain-profile.zip -59b70a12abedb550795614bc74c5803787e824da3529a631fdb5c2b5aad00196 *electron-v39.8.0-win32-ia32.zip -0357c6fb0d7198c45cba0e8c939473ea1d971e1efe801bc84e2c559141b368e7 *electron-v39.8.0-win32-x64-pdb.zip -8e6f4e8516d15aecde5244beac315067c13513c7074383086523eef2638a5e8d *electron-v39.8.0-win32-x64-symbols.zip -b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.0-win32-x64-toolchain-profile.zip -9edc111b22aee1a0efb5103d6d3b48645af57b48214eeb48f75f9edfc3e271d6 *electron-v39.8.0-win32-x64.zip -b6eca0e05fcff2464382278dff52367f6f21eb1a580dd8a0a954fc16397ab085 *electron.d.ts -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.8.0-darwin-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.8.0-darwin-x64.zip -52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.8.0-linux-arm64.zip -622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.8.0-linux-armv7l.zip -ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.8.0-linux-x64.zip -27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.8.0-mas-arm64.zip -321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.8.0-mas-x64.zip -3ba7c7507181e0d4836f70f3d8800b4e9ba379e1086e9e89fda7ff9b3b9ad2cb *ffmpeg-v39.8.0-win32-arm64.zip -f37e7d51b8403e2ed8ca192bc6ae759cf63d80010e747b15eeb7120b575578b2 *ffmpeg-v39.8.0-win32-ia32.zip -b252e232438010f9683e8fd10c3bf0631df78e42a6ae11d6cb7aa7e6ac11185f *ffmpeg-v39.8.0-win32-x64.zip -365735192f58a7f7660100227ec348ba3df604415ff5264b54d93cb6cf5f6f6f *hunspell_dictionaries.zip -6384ee31daa39de4dd4bd3aa225cdb14cdddb7f463a2c1663b38a79e122a13e2 *libcxx-objects-v39.8.0-linux-arm64.zip -9748b3272e52a8274fe651def2d6ae2dad7a3771b520dd105f46f4020ba9d63b *libcxx-objects-v39.8.0-linux-armv7l.zip -74d47a155ecc6c2054418c7c3e0540f32b983ebdc65e8b4ea5d3e257d29b3f4f *libcxx-objects-v39.8.0-linux-x64.zip -c0755fbb84011664bd36459fc6e06a603078dccd3b7b260f6ed6ad1d409f79f7 *libcxx_headers.zip -3ea41e9bd56e8f52ab8562c1406ba9416abe3993640935e981cbbd77c0f2654b *libcxxabi_headers.zip -befcd6067f35d911a6a87b927e79dc531cb7bea39e85f86a65e9ab82ef0cece1 *mksnapshot-v39.8.0-darwin-arm64.zip -f0e692655298ffed60630c3e6490ced69e9d8726e85bcaecfa34485f3a991469 *mksnapshot-v39.8.0-darwin-x64.zip -d5d0901cd1eafdf921d2a0d1565829cf60f454a71ce74fa60db98780fd8a1a96 *mksnapshot-v39.8.0-linux-arm64-x64.zip -1bc0a3294d258a59846aa5c5359cd8b0f43831ebd7c3e1dde9a6cfaa39d845bf *mksnapshot-v39.8.0-linux-armv7l-x64.zip -4e414dbe75f460cb34508608db984aa6f4d274f333fa327a3d631da4a516da8f *mksnapshot-v39.8.0-linux-x64.zip -c51c86e3a11ad75fb4f7559798f6d64ec7def19583c96ce08de7ee5796568841 *mksnapshot-v39.8.0-mas-arm64.zip -6544d1e93adea1e9a694f9b9f539d96f84df647d9c9319b29d4fc88751ff9075 *mksnapshot-v39.8.0-mas-x64.zip -372b4685c53f19ccc72c33d78c1283d9389c72f42cd48224439fe4f89199caa0 *mksnapshot-v39.8.0-win32-arm64-x64.zip -199e9244f4522a4a02aece09a6a33887b24d7ec837640d39c930170e4b3caa57 *mksnapshot-v39.8.0-win32-ia32.zip -970e979e7a8b70f300f7854cb571756d9049bc42b44a6153a9ce3a18e1a83243 *mksnapshot-v39.8.0-win32-x64.zip +0ab48e3e8888b5c33950be0c36da939aa989df7609d3c32140c5e5371ea53abb *chromedriver-v39.8.3-darwin-arm64.zip +b7103565ffb4068dc705c50ce3039ed3178cac350301abf82545de54ac3bc849 *chromedriver-v39.8.3-darwin-x64.zip +e7e43ee7a3d14482ce488d0b0bc338a026a00ee544e5a3d55aed220af6b5da0e *chromedriver-v39.8.3-linux-arm64.zip +060223baebe6d8f9e8c7367bf561dd558fca03509edcc3bce660c42f96ad73ea *chromedriver-v39.8.3-linux-armv7l.zip +854a6f921684e59866aed9db0e9f61d28f756f70b7898f947359b4d04dba76db *chromedriver-v39.8.3-linux-x64.zip +f70ea58bc5e4e51eec51f65e153cfd36eea568ecd571c2815a4c05a457b6923d *chromedriver-v39.8.3-mas-arm64.zip +8e3e1450bc544bff712ffab0ba365d1ed2c9b79116b4ec4750a46c8607242ed4 *chromedriver-v39.8.3-mas-x64.zip +c07e35a2a5a673c8902452571f3436ca8b774fa4628ad9e42f179d3c935f4ed7 *chromedriver-v39.8.3-win32-arm64.zip +d0361344208d8bdf58500d08ae1bb723b9ccdc66fc736c2fc6c9f011bcc6e47d *chromedriver-v39.8.3-win32-ia32.zip +e2e91fd7d97e3e9806d22c4990253cbd5e466cdfa1a8e4c86c72431f7d3a8d0f *chromedriver-v39.8.3-win32-x64.zip +f004c879e159edf3eb403bd43bc76c3069b0b375c6dfae5b249b96d543e51e26 *electron-api.json +21a5324aaed782fead97b2e50f833373148392d4c13ec818f80f142e800c6158 *electron-v39.8.3-darwin-arm64-dsym-snapshot.zip +bb9c14900f48aabb7d272149ba4b60813b366f1e67f95b510da73355e15ba78c *electron-v39.8.3-darwin-arm64-dsym.zip +8a42b50a0841e7bfefc49e704f5cfdb3cbb7b9a507ac74b9982004a9350a202d *electron-v39.8.3-darwin-arm64-symbols.zip +e1b9e03a56fc27ad567c8d2bb32a21e0e2afe6a095f71c26df5b8b8ed8dd8d4c *electron-v39.8.3-darwin-arm64.zip +5b474116e398286a80def6509fa255481ab88fbb52b1770dfd5d39ddff124c6b *electron-v39.8.3-darwin-x64-dsym-snapshot.zip +14648a98eef5a28c1158f0580a813617d9ce6d77a8b7881c389acfff34d328cd *electron-v39.8.3-darwin-x64-dsym.zip +231e13b26c39cceecec359e74c00e4d6a13de3ae9fb6459f18846f91f214074f *electron-v39.8.3-darwin-x64-symbols.zip +22cf6f6147d5d632e2a8ad5207504a18db94a8c96e3f4f65f792822eaed7bf1c *electron-v39.8.3-darwin-x64.zip +fdf25df8857e1ef2cdb0a5be71b78dfb9923a6061cf11336577c6a4368ecfdcd *electron-v39.8.3-linux-arm64-debug.zip +731bf3f908a1efe871e862852087b67027c791427284f057d42376634d4d53d3 *electron-v39.8.3-linux-arm64-symbols.zip +e1a0e6939fe2d10c1f807888f74dbbb9f28a2cfc25e28bb8168f5513513fc124 *electron-v39.8.3-linux-arm64.zip +65893fd03097eadb0c89eb95ded97e97a9910bbc53634f12170cfb40b9165832 *electron-v39.8.3-linux-armv7l-debug.zip +997acce3540d16f9e0551cde811021999a4276c970bfe42ed77c3fd769ba6d05 *electron-v39.8.3-linux-armv7l-symbols.zip +5d5825966a3b2678c50121c81ed3fb8c39d35c3798dd0413a19afaac04109ef9 *electron-v39.8.3-linux-armv7l.zip +52b44ef60f73ef7b7c8461f520a1048da3601d9cc869262ec63f507cd6591e78 *electron-v39.8.3-linux-x64-debug.zip +c4e1fa21d21724ab7f5bcdb6c1bfc03dca837ebcca00d6af56944041499d35a9 *electron-v39.8.3-linux-x64-symbols.zip +5866d6c6f8fcf15967279107d2387edfa4589c5a00ad52d4b770d7504106a734 *electron-v39.8.3-linux-x64.zip +3ff4c9fb99f40dda465486fa6fa23eaedd89b87dcd9cce402a171accfcacc9bd *electron-v39.8.3-mas-arm64-dsym-snapshot.zip +9401101eabaf5e55063b9fad94bf3ac2fe9d743ff88ec638a3c6c665b2266564 *electron-v39.8.3-mas-arm64-dsym.zip +0905e57da501b64436dff51b1378bae311cb1276372dc39dedb7aef44f1b947a *electron-v39.8.3-mas-arm64-symbols.zip +1af2cdee3405c0b8e1c8145a65891b249ac737dd35d959cebd6833970ad5eb08 *electron-v39.8.3-mas-arm64.zip +9e8c6b7b880ac726cda52aaf2b02bc9f0750559be85ef1583789d52b617914b9 *electron-v39.8.3-mas-x64-dsym-snapshot.zip +483ce280606a61c7ed4394e99008ad6fc7e2ce9c35149c5ad745bce9ee78a7a2 *electron-v39.8.3-mas-x64-dsym.zip +1e16529ab1ee8404b1623df611077abcbbbabc1f825a57e93e2ef2b1f7ba788a *electron-v39.8.3-mas-x64-symbols.zip +19df399b352db2c3b3f26f830700b17adf697b70e4d361b1e0f20790e6e703b6 *electron-v39.8.3-mas-x64.zip +bdfd01e7c55ea4beb90afd4285ab3639e3a66808ec993389e9eaf62c3edcb5e9 *electron-v39.8.3-win32-arm64-pdb.zip +7329264c9d308a78081509cb4173f0bd931522655d2f434ad858555e735e5721 *electron-v39.8.3-win32-arm64-symbols.zip +b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.3-win32-arm64-toolchain-profile.zip +699933ff8c4d7216fc0318f239a5f593f06487c0dc9c3722b8744f6a44fca94e *electron-v39.8.3-win32-arm64.zip +1ffab5a8419a1a93476e2edc09754a52bbe9f3d39915e097f2a1ee50ffdbbd13 *electron-v39.8.3-win32-ia32-pdb.zip +6445047728d64a09db80205c24135f140ba60c25433d833f581c57092638b875 *electron-v39.8.3-win32-ia32-symbols.zip +b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.3-win32-ia32-toolchain-profile.zip +b80bb96a4eda2c2b6bd223d2d8b6abfc39abdebac0b36cf74cd70661d43258a5 *electron-v39.8.3-win32-ia32.zip +c8e3cab205bdfe42f916a4428fe0a5e88b6f90e8482e297dadfe1234420abb8f *electron-v39.8.3-win32-x64-pdb.zip +eda4a2f01e388eccfc2ecc7587b0987d123ae01e5b832b73a0a76bb62680bd7c *electron-v39.8.3-win32-x64-symbols.zip +b701e63ca5d443d9bd1b653ea0e2b7479f0d834a3d1bd9f10a3b745d29607154 *electron-v39.8.3-win32-x64-toolchain-profile.zip +12eabd7c5f08823525034c1ff3ab286f271af802928e0f224b458235e2689c5a *electron-v39.8.3-win32-x64.zip +d5345fc0cb336a425f7a25885f67969452746cbf30cc1e95449f7a68221aab07 *electron.d.ts +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.8.3-darwin-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.8.3-darwin-x64.zip +52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.8.3-linux-arm64.zip +622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.8.3-linux-armv7l.zip +ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.8.3-linux-x64.zip +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.8.3-mas-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.8.3-mas-x64.zip +06d402e51bf66fd1a0eddc7e8329b31eb8a1bc6c829e5bc13694708a010feb07 *ffmpeg-v39.8.3-win32-arm64.zip +4b1c2ddedebbf32b7792fb788ddce577f2dc6f8ecccd913e72e842068b2f81e5 *ffmpeg-v39.8.3-win32-ia32.zip +0cf0f521a452d6fdfe1313b81284d646e991954e91c356d805f5c066f2ecf278 *ffmpeg-v39.8.3-win32-x64.zip +bb2d6ec64c43e5a41da5bc55a3daa2b7ef8a0dee722dc73f4fa31bcae5487cb2 *hunspell_dictionaries.zip +7fd663a5eeaf4d0a93785ea4ea715d21464c70c1341b7d8629c96a7bfe24044a *libcxx-objects-v39.8.3-linux-arm64.zip +bdd7e9f732b97b6be2c8293d9391bce3a5cd60feae1c762a0dda0790493da7a2 *libcxx-objects-v39.8.3-linux-armv7l.zip +0de009f84fed7c1bba087ff161674177ca91951fca2f4c60850be0bffb42dfdf *libcxx-objects-v39.8.3-linux-x64.zip +8ae7fd5c3bdc332f9f49830a9316e470d43f17e6ad6adbd05ac629d03d1718c2 *libcxx_headers.zip +9b988e2bb379c6d3094872f600944ad3284510cf225f86368c4f43270b89673c *libcxxabi_headers.zip +d504296ed183e3f460028a73b4a5e2bcd99bc4a3c74b8dc73ba987719c005458 *mksnapshot-v39.8.3-darwin-arm64.zip +b18b8a0e902cf86961d53486826fb07feb3ac98e018b2849cf2bb13150077b13 *mksnapshot-v39.8.3-darwin-x64.zip +7d9dc2ceb3f88d8d532af5b90387479ade571a0370489429571871d386c12322 *mksnapshot-v39.8.3-linux-arm64-x64.zip +84114ba67259f52ae462210544e815b602d70b71162f7f70982a7c36db54b4fa *mksnapshot-v39.8.3-linux-armv7l-x64.zip +be08392f0964d2166bb84212d225c5380fde9b12e622599cb040f45524ff7882 *mksnapshot-v39.8.3-linux-x64.zip +ffebb01a6fe568ec51391f9585e8abf1a93a566a7c991b2abacc33cc2e94d705 *mksnapshot-v39.8.3-mas-arm64.zip +858a8ee80b6f826b1d24a9458b8acb0fcc9805ee3c309652d60ed5c07b578113 *mksnapshot-v39.8.3-mas-x64.zip +dbc82c573f1ba098b6d321ad79c6580f27066d94e13fd93c0b7650a54150eb5c *mksnapshot-v39.8.3-win32-arm64-x64.zip +c8f7c43741b20da99558db596d32c86ebff3483aaf6b3e2539cd85a471b92043 *mksnapshot-v39.8.3-win32-ia32.zip +000941eeb8e1169d581120df7d42aa0b76e33de99a06528a9c4767bcffb74cbf *mksnapshot-v39.8.3-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index c5cb12c7972..5b1c61efa07 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640 node-v22.22.0-darwin-arm64.tar.gz -5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce node-v22.22.0-darwin-x64.tar.gz -25ba95dfb96871fa2ef977f11f95ea90818c8fa15c0f2110771db08d4ba423be node-v22.22.0-linux-arm64.tar.gz -a92684d8720589f19776fb186c5a3a4d273c13436fc8c44b61dd3eeef81f0d3a node-v22.22.0-linux-armv7l.tar.gz -c33c39ed9c80deddde77c960d00119918b9e352426fd604ba41638d6526a4744 node-v22.22.0-linux-x64.tar.gz -fd44256121597d6a3707f4c7730b4e3733eacb5a95cc78a099f601d7e7f8290d win-arm64/node.exe -bae898add4643fcf890a83ad8ae56e20dce7e781cab161a53991ceba70c99ffb win-x64/node.exe +679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2 node-v22.22.1-darwin-arm64.tar.gz +07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a node-v22.22.1-darwin-x64.tar.gz +1d1690e9aba47e887a275abc6d8f7317e571a0700deaef493f768377e99155f5 node-v22.22.1-linux-arm64.tar.gz +2b592d21609ef299d1e3918bb806ed62ba715d4109b0f8ec11b132af9fa42d70 node-v22.22.1-linux-armv7l.tar.gz +07c8aafa60644fb81adefa1ee7da860eb1920851ffdc9a37020ab0be47fbc10e node-v22.22.1-linux-x64.tar.gz +993b56091266aec4a41653ea3e70b5b18fadc78952030ca0329309240030859c win-arm64/node.exe +923a41f268ab49ede2e3363fbdd9e790609e385c6f3ca880b4ee9a56a8133e5a win-x64/node.exe diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 9e90e31491f..46544b0c4d9 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -10,6 +10,30 @@ import { makeUniversalApp } from 'vscode-universal-bundler'; const root = path.dirname(path.dirname(import.meta.dirname)); +const nodeModulesBases = [ + path.join('Contents', 'Resources', 'app', 'node_modules'), + path.join('Contents', 'Resources', 'app', 'node_modules.asar.unpacked'), +]; + +/** + * Ensures a directory exists in both the x64 and arm64 app bundles by copying + * it from whichever build has it to the one that does not. This is needed for + * platform-specific native module directories that npm only installs for the + * host architecture. + */ +function crossCopyPlatformDir(x64AppPath: string, arm64AppPath: string, relativePath: string): void { + const inX64 = path.join(x64AppPath, relativePath); + const inArm64 = path.join(arm64AppPath, relativePath); + + if (fs.existsSync(inX64) && !fs.existsSync(inArm64)) { + fs.mkdirSync(inArm64, { recursive: true }); + fs.cpSync(inX64, inArm64, { recursive: true }); + } else if (fs.existsSync(inArm64) && !fs.existsSync(inX64)) { + fs.mkdirSync(inX64, { recursive: true }); + fs.cpSync(inArm64, inX64, { recursive: true }); + } +} + async function main(buildDir?: string) { const arch = process.env['VSCODE_ARCH']; @@ -25,10 +49,33 @@ async function main(buildDir?: string) { const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + // Copilot SDK ships platform-specific native binaries that npm only installs + // for the host architecture. The universal app merger requires both builds to + // have identical file trees, so we cross-copy each missing directory from the + // other build. The binaries are then excluded from comparison (filesToSkip) + // and the x64 binary is tagged as arch-specific (x64ArchFiles) so the merger + // keeps both. + for (const plat of ['darwin-x64', 'darwin-arm64']) { + for (const base of nodeModulesBases) { + // @github/copilot-{platform} packages (e.g. copilot-darwin-x64) + crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', `copilot-${plat}`)); + // @github/copilot/prebuilds/{platform} (pty.node, spawn-helper) + crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', 'copilot', 'prebuilds', plat)); + } + } + const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', - '**/policies/{*.mobileconfig,**/*.plist}' + '**/policies/{*.mobileconfig,**/*.plist}', + '**/node_modules/@github/copilot-darwin-x64/**', + '**/node_modules/@github/copilot-darwin-arm64/**', + '**/node_modules.asar.unpacked/@github/copilot-darwin-x64/**', + '**/node_modules.asar.unpacked/@github/copilot-darwin-arm64/**', + '**/node_modules/@github/copilot/prebuilds/darwin-x64/**', + '**/node_modules/@github/copilot/prebuilds/darwin-arm64/**', + '**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-x64/**', + '**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-arm64/**', ]; await makeUniversalApp({ @@ -38,7 +85,7 @@ async function main(buildDir?: string) { outAppPath, force: true, mergeASARs: true, - x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node}', + x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/copilot,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules.asar.unpacked/@github/copilot-darwin-*/copilot,**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-*/*}', filesToSkipComparison: (file: string) => { for (const expected of filesToSkip) { if (minimatch(file, expected)) { diff --git a/build/darwin/verify-macho.ts b/build/darwin/verify-macho.ts index 7770b9c36cd..bec37b2dd8e 100644 --- a/build/darwin/verify-macho.ts +++ b/build/darwin/verify-macho.ts @@ -26,6 +26,14 @@ const FILES_TO_SKIP = [ // MSAL runtime files are only present in ARM64 builds '**/extensions/microsoft-authentication/dist/libmsalruntime.dylib', '**/extensions/microsoft-authentication/dist/msal-node-runtime.node', + // Copilot SDK: universal app has both x64 and arm64 platform packages + '**/node_modules/@github/copilot-darwin-x64/**', + '**/node_modules/@github/copilot-darwin-arm64/**', + '**/node_modules.asar.unpacked/@github/copilot-darwin-x64/**', + '**/node_modules.asar.unpacked/@github/copilot-darwin-arm64/**', + // Copilot prebuilds: single-arch binaries in per-platform directories + '**/node_modules/@github/copilot/prebuilds/darwin-*/**', + '**/node_modules.asar.unpacked/@github/copilot/prebuilds/darwin-*/**', ]; function isFileSkipped(file: string): boolean { diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index 8f9ac9b2b21..e0137816c8c 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -309,13 +309,6 @@ async function buildWebExtensions(isWatch: boolean): Promise { { ignore: ['**/node_modules'] } ); - // Find all webpack configs, excluding those that will be esbuilt - const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p))); - const webpackConfigLocations = (await nodeUtil.promisify(glob)( - path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), - { ignore: ['**/node_modules'] } - )).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath))); - const promises: Promise[] = []; // Esbuild for extensions @@ -330,10 +323,5 @@ async function buildWebExtensions(isWatch: boolean): Promise { ); } - // Run webpack for remaining extensions - if (webpackConfigLocations.length > 0) { - promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })))); - } - await Promise.all(promises); } diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 0ad08ab6fbe..f3ba46d9f89 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -34,6 +34,7 @@ import * as cp from 'child_process'; import log from 'fancy-log'; import buildfile from './buildfile.ts'; import { fetchUrls, fetchGithub } from './lib/fetch.ts'; +import { getCopilotExcludeFilter, copyCopilotNativeDeps } from './lib/copilot.ts'; import jsonEditor from 'gulp-json-editor'; @@ -343,6 +344,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN .pipe(filter(['**', '!**/package-lock.json', '!**/*.{js,css}.map'])) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) + .pipe(filter(getCopilotExcludeFilter(platform, arch))) .pipe(jsFilter) .pipe(util.stripSourceMappingURL()) .pipe(jsFilter.restore); @@ -461,6 +463,13 @@ function patchWin32DependenciesTask(destinationFolderName: string) { }; } +function copyCopilotNativeDepsTaskREH(platform: string, arch: string, destinationFolderName: string) { + return async () => { + const nodeModulesDir = path.join(BUILD_ROOT, destinationFolderName, 'node_modules'); + copyCopilotNativeDeps(platform, arch, nodeModulesDir); + }; +} + /** * @param product The parsed product.json file contents */ @@ -509,7 +518,8 @@ function tweakProductForServerWeb(product: typeof import('../product.json')) { compileNativeExtensionsBuildTask, gulp.task(`node-${platform}-${arch}`) as task.Task, util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), - packageTask(type, platform, arch, sourceFolderName, destinationFolderName) + packageTask(type, platform, arch, sourceFolderName, destinationFolderName), + copyCopilotNativeDepsTaskREH(platform, arch, destinationFolderName) ]; if (platform === 'win32') { diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index e33ae905ad1..336a8947fbb 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -31,6 +31,7 @@ import minimist from 'minimist'; import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; +import { getCopilotExcludeFilter, copyCopilotNativeDeps } from './lib/copilot.ts'; import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; @@ -43,8 +44,6 @@ const glob = promisify(globCallback); const rcedit = promisify(rceditCallback); const root = path.dirname(import.meta.dirname); const commit = getVersion(root); -const useVersionedUpdate = process.platform === 'win32' && (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; -const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; // Build const vscodeEntryPoints = [ @@ -102,6 +101,7 @@ const vscodeResourceIncludes = [ // Sessions 'out-build/vs/sessions/contrib/chat/browser/media/*.svg', 'out-build/vs/sessions/prompts/*.prompt.md', + 'out-build/vs/sessions/skills/**/SKILL.md', // Extensions 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', @@ -325,6 +325,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const task = () => { const out = sourceFolderName; + const versionedResourcesFolder = util.getVersionedResourcesFolder(platform, commit!); const checksums = computeChecksums(out, [ 'vs/base/parts/sandbox/electron-browser/preload.js', @@ -437,12 +438,14 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(filter(depFilterPattern)) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) + .pipe(filter(getCopilotExcludeFilter(platform, arch))) .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) .pipe(jsFilter.restore) .pipe(createAsar(path.join(process.cwd(), 'node_modules'), [ '**/*.node', '**/@vscode/ripgrep/bin/*', + '**/@github/copilot-*/**', '**/node-pty/build/Release/*', '**/node-pty/build/Release/conpty/*', '**/node-pty/lib/worker/conoutSocketWorker.js', @@ -532,6 +535,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d darwinMiniAppName: embedded.nameShort, darwinMiniAppBundleIdentifier: embedded.darwinBundleIdentifier, darwinMiniAppIcon: 'resources/darwin/sessions.icns', + darwinMiniAppAssetsCar: 'resources/darwin/sessions.car', darwinMiniAppBundleURLTypes: [{ role: 'Viewer', name: embedded.nameLong, @@ -568,7 +572,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d if (platform === 'win32') { result = es.merge(result, gulp.src('resources/win32/bin/code.js', { base: 'resources/win32', allowEmpty: true })); - if (useVersionedUpdate) { + if (versionedResourcesFolder) { result = es.merge(result, gulp.src('resources/win32/versioned/bin/code.cmd', { base: 'resources/win32/versioned' }) .pipe(replace('@@NAME@@', product.nameShort)) .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder)) @@ -646,6 +650,7 @@ function patchWin32DependenciesTask(destinationFolderName: string) { const cwd = path.join(path.dirname(root), destinationFolderName); return async () => { + const versionedResourcesFolder = util.getVersionedResourcesFolder('win32', commit!); const deps = (await Promise.all([ glob('**/*.node', { cwd, ignore: 'extensions/node_modules/@parcel/watcher/**' }), glob('**/rg.exe', { cwd }), @@ -677,6 +682,21 @@ function patchWin32DependenciesTask(destinationFolderName: string) { }; } +function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFolderName: string) { + const outputDir = path.join(path.dirname(root), destinationFolderName); + + return async () => { + // On Windows with win32VersionedUpdate, app resources live under a + // commit-hash prefix: {output}/{commitHash}/resources/app/ + const versionedResourcesFolder = util.getVersionedResourcesFolder(platform, commit!); + const appBase = platform === 'darwin' + ? path.join(outputDir, `${product.nameLong}.app`, 'Contents', 'Resources', 'app') + : path.join(outputDir, versionedResourcesFolder, 'resources', 'app'); + + copyCopilotNativeDeps(platform, arch, path.join(appBase, 'node_modules')); + }; +} + const buildRoot = path.dirname(root); const BUILD_TARGETS = [ @@ -701,7 +721,8 @@ BUILD_TARGETS.forEach(buildTarget => { const packageTasks: task.Task[] = [ compileNativeExtensionsBuildTask, util.rimraf(path.join(buildRoot, destinationFolderName)), - packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) + packageTask(platform, arch, sourceFolderName, destinationFolderName, opts), + copyCopilotNativeDepsTask(platform, arch, destinationFolderName) ]; if (platform === 'win32') { diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 0f81323c98d..c393e8247f1 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -122,6 +122,7 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { definitions['ProxyExeBasename'] = embedded.nameShort; definitions['ProxyAppUserId'] = embedded.win32AppUserModelId; definitions['ProxyNameLong'] = embedded.nameLong; + definitions['ProxyExeUrlProtocol'] = embedded.urlProtocol; } if (quality === 'stable' || quality === 'insider') { diff --git a/build/lib/copilot.ts b/build/lib/copilot.ts new file mode 100644 index 00000000000..f182c9829a9 --- /dev/null +++ b/build/lib/copilot.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * The platforms that @github/copilot ships platform-specific packages for. + * These are the `@github/copilot-{platform}` optional dependency packages. + */ +export const copilotPlatforms = [ + 'darwin-arm64', 'darwin-x64', + 'linux-arm64', 'linux-x64', + 'win32-arm64', 'win32-x64', +]; + +/** + * Converts VS Code build platform/arch to the values that Node.js reports + * at runtime via `process.platform` and `process.arch`. + * + * The copilot SDK's `loadNativeModule` looks up native binaries under + * `prebuilds/${process.platform}-${process.arch}/`, so the directory names + * must match these runtime values exactly. + */ +function toNodePlatformArch(platform: string, arch: string): { nodePlatform: string; nodeArch: string } { + // alpine is musl-linux; Node still reports process.platform === 'linux' + let nodePlatform = platform === 'alpine' ? 'linux' : platform; + let nodeArch = arch; + + if (arch === 'armhf') { + // VS Code build uses 'armhf'; Node reports process.arch === 'arm' + nodeArch = 'arm'; + } else if (arch === 'alpine') { + // Legacy: { platform: 'linux', arch: 'alpine' } means alpine-x64 + nodePlatform = 'linux'; + nodeArch = 'x64'; + } + + return { nodePlatform, nodeArch }; +} + +/** + * Returns a glob filter that strips @github/copilot platform packages + * for architectures other than the build target. + * + * For platforms the copilot SDK doesn't natively support (e.g. alpine, armhf), + * ALL platform packages are stripped - that's fine because the SDK doesn't ship + * binaries for those platforms anyway, and we replace them with VS Code's own. + */ +export function getCopilotExcludeFilter(platform: string, arch: string): string[] { + const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); + const targetPlatformArch = `${nodePlatform}-${nodeArch}`; + const nonTargetPlatforms = copilotPlatforms.filter(p => p !== targetPlatformArch); + + // Strip wrong-architecture @github/copilot-{platform} packages. + // All copilot prebuilds are stripped by .moduleignore; VS Code's own + // node-pty is copied into the prebuilds location by a post-packaging task. + const excludes = nonTargetPlatforms.map(p => `!**/node_modules/@github/copilot-${p}/**`); + + return ['**', ...excludes]; +} + +/** + * Copies VS Code's own node-pty binaries into the copilot SDK's + * expected locations so the copilot CLI subprocess can find them at runtime. + * The copilot-bundled prebuilds are stripped by .moduleignore; + * this replaces them with the same binaries VS Code already ships, avoiding + * new system dependency requirements. + * + * This works even for platforms the copilot SDK doesn't natively support + * (e.g. alpine, armhf) because the SDK's native module loader simply + * looks for `prebuilds/{process.platform}-{process.arch}/pty.node` - it + * doesn't validate the platform against a supported list. + * + * Failures are logged but do not throw, to avoid breaking the build on + * platforms where something unexpected happens. + * + * @param nodeModulesDir Absolute path to the node_modules directory that + * contains both the source binaries (node-pty) and the copilot SDK + * target directories. + */ +export function copyCopilotNativeDeps(platform: string, arch: string, nodeModulesDir: string): void { + const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); + const platformArch = `${nodePlatform}-${nodeArch}`; + + const copilotBase = path.join(nodeModulesDir, '@github', 'copilot'); + if (!fs.existsSync(copilotBase)) { + console.warn(`[copyCopilotNativeDeps] @github/copilot not found at ${copilotBase}, skipping`); + return; + } + + const nodePtySource = path.join(nodeModulesDir, 'node-pty', 'build', 'Release'); + if (!fs.existsSync(nodePtySource)) { + console.warn(`[copyCopilotNativeDeps] node-pty source not found at ${nodePtySource}, skipping`); + return; + } + + try { + // Copy node-pty (pty.node + spawn-helper on Unix, conpty.node + conpty/ on Windows) + // into copilot prebuilds so the SDK finds them via loadNativeModule. + const copilotPrebuildsDir = path.join(copilotBase, 'prebuilds', platformArch); + fs.mkdirSync(copilotPrebuildsDir, { recursive: true }); + fs.cpSync(nodePtySource, copilotPrebuildsDir, { recursive: true }); + console.log(`[copyCopilotNativeDeps] Copied node-pty from ${nodePtySource} to ${copilotPrebuildsDir}`); + } catch (err) { + console.warn(`[copyCopilotNativeDeps] Failed to copy node-pty for ${platformArch}: ${err}`); + } +} diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 5710f4d6919..c5a74b53e04 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -20,10 +20,8 @@ import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; -import webpack from 'webpack'; import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; -import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; @@ -32,8 +30,8 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const root = path.dirname(path.dirname(import.meta.dirname)); -const commit = getVersion(root); -const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +// const commit = getVersion(root); +// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -65,32 +63,24 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { +function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb ? 'esbuild.browser.mts' : 'esbuild.mts'; - const webpackConfigFileName = forWeb - ? `extension-browser.webpack.config.js` - : `extension.webpack.config.js`; - const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); - const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step + // Esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), ...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)), ); isBundled = true; - } else if (hasWebpack) { - input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); - isBundled = true; } else { input = fromLocalNormal(extensionPath); } @@ -122,132 +112,6 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean) return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } -function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { - const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); - const webpack = require('webpack'); - const webpackGulp = require('webpack-stream'); - const result = es.through(); - - const packagedDependencies: string[] = []; - const stripOutSourceMaps: string[] = []; - const packageJsonConfig = require(path.join(extensionPath, 'package.json')); - if (packageJsonConfig.dependencies) { - const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); - const webpackRootConfig = webpackConfig.default; - for (const key in webpackRootConfig.externals) { - if (key in packageJsonConfig.dependencies) { - packagedDependencies.push(key); - } - } - - if (webpackConfig.StripOutSourceMaps) { - for (const filePath of webpackConfig.StripOutSourceMaps) { - stripOutSourceMaps.push(filePath); - } - } - } - - // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar - // to vsce.PackageManager.Yarn. - // A static analysis showed there are no webpack externals that are dependencies of the current - // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list - // as a temporary workaround. - vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { - const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ - path: filePath, - stat: fs.statSync(filePath), - base: extensionPath, - contents: fs.createReadStream(filePath) - })); - - // check for a webpack configuration files, then invoke webpack - // and merge its output with the files stream. - const webpackConfigLocations = (glob.sync( - path.join(extensionPath, '**', webpackConfigFileName), - { ignore: ['**/node_modules'] } - ) as string[]); - const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { - - const webpackDone = (err: Error | undefined, stats: any) => { - fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); - if (err) { - result.emit('error', err); - } - const { compilation } = stats; - if (compilation.errors.length > 0) { - result.emit('error', compilation.errors.join('\n')); - } - if (compilation.warnings.length > 0) { - result.emit('error', compilation.warnings.join('\n')); - } - }; - - const exportedConfig = require(webpackConfigPath).default; - return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { - const webpackConfig = { - ...config, - ...{ mode: 'production' } - }; - if (disableMangle) { - if (Array.isArray(config.module.rules)) { - for (const rule of config.module.rules) { - if (Array.isArray(rule.use)) { - for (const use of rule.use) { - if (String(use.loader).endsWith('mangle-loader.js')) { - use.options.disabled = true; - } - } - } - } - } - } - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data: File) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - if (path.extname(data.basename) === '.js') { - if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); - } else { - const contents = (data.contents as Buffer).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); - } - } - - this.emit('data', data); - })); - }); - }); - - es.merge(...webpackStreams, es.readArray(files)) - // .pipe(es.through(function (data) { - // // debug - // console.log('out', data.path, data.contents.length); - // this.emit('data', data); - // })) - .pipe(result); - - }).catch(err => { - console.error(extensionPath); - console.error(packagedDependencies); - result.emit('error', err); - }); - - return result.pipe(createStatsStream(path.basename(extensionPath))); -} function fromLocalNormal(extensionPath: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -274,6 +138,14 @@ function fromLocalNormal(extensionPath: string): Stream { function fromLocalEsbuild(extensionPath: string, esbuildConfigFileName: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); const result = es.through(); + const extensionName = path.basename(extensionPath); + + // Extensions built with esbuild can still externalize runtime dependencies. + // Ensure those externals are included in the packaged built-in extension. + const packagedDependenciesByExtension: Record = { + 'git': ['@vscode/fs-copyfile'] + }; + const packagedDependencies = packagedDependenciesByExtension[extensionName] ?? []; const esbuildScript = path.join(extensionPath, esbuildConfigFileName); @@ -299,6 +171,25 @@ function fromLocalEsbuild(extensionPath: string, esbuildConfigFileName: string): // After esbuild completes, collect all files using vsce return vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None }); }).then(fileNames => { + if (packagedDependencies.length > 0) { + const packagedDependencyFileNames = packagedDependencies.flatMap(dependency => + glob.sync(path.join(extensionPath, 'node_modules', dependency, '**'), { nodir: true, dot: true }) + .map(filePath => path.relative(extensionPath, filePath)) + .filter(filePath => { + // Exclude non-.node files from build directories to avoid timestamp-sensitive + // artifacts (e.g. Makefile) that break macOS universal builds due to SHA mismatches. + const parts = filePath.split(path.sep); + const buildIndex = parts.indexOf('build'); + if (buildIndex !== -1) { + return filePath.endsWith('.node'); + } + return true; + }) + ); + + fileNames = Array.from(new Set([...fileNames, ...packagedDependencyFileNames])); + } + const files = fileNames .map(fileName => path.join(extensionPath, fileName)) .map(filePath => new File({ @@ -311,6 +202,7 @@ function fromLocalEsbuild(extensionPath: string, esbuildConfigFileName: string): es.readArray(files).pipe(result); }).catch(err => { console.error(extensionPath); + console.error(packagedDependencies); result.emit('error', err); }); @@ -405,6 +297,7 @@ export function fromGithub({ name, version, repo, sha256, metadata }: IExtension * platform that is being built. */ const nativeExtensions = [ + 'git', 'microsoft-authentication', ]; @@ -649,70 +542,6 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); -export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { - const webpack = require('webpack') as typeof import('webpack'); - - const webpackConfigs: webpack.Configuration[] = []; - - for (const { configPath, outputRoot } of webpackConfigLocations) { - const configOrFnOrArray = require(configPath).default; - function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { - for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { - const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; - if (outputRoot) { - config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!)); - } - webpackConfigs.push(config); - } - } - addConfig(configOrFnOrArray); - } - - function reporter(fullStats: any) { - if (Array.isArray(fullStats.children)) { - for (const stats of fullStats.children) { - const outputPath = stats.outputPath; - if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); - const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); - } - if (Array.isArray(stats.errors)) { - stats.errors.forEach((error: any) => { - fancyLog.error(error); - }); - } - if (Array.isArray(stats.warnings)) { - stats.warnings.forEach((warning: any) => { - fancyLog.warn(warning); - }); - } - } - } - } - return new Promise((resolve, reject) => { - if (isWatch) { - webpack(webpackConfigs).watch({}, (err, stats) => { - if (err) { - reject(); - } else { - reporter(stats?.toJson()); - } - }); - } else { - webpack(webpackConfigs).run((err, stats) => { - if (err) { - fancyLog.error(err); - reject(); - } else { - reporter(stats?.toJson()); - resolve(); - } - }); - } - }); -} - export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise { function reporter(stdError: string, script: string) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 921137824ee..d2b50232093 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -170,6 +170,10 @@ "name": "vs/workbench/contrib/inlineChat", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/imageCarousel", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/chat", "project": "vscode-workbench" @@ -561,6 +565,132 @@ { "name": "vs/workbench/contrib/list", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/browserView", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/dropOrPasteInto", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/editTelemetry", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/inlineCompletions", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/mcp", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/meteredConnection", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/processExplorer", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/remoteCodingAgents", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/telemetry", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/welcomeAgentSessions", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/accounts", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/chat", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/request", + "project": "vscode-workbench" + } + ], + "sessions": [ + { + "name": "vs/sessions", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/accountMenu", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/agentFeedback", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/aiCustomizationTreeView", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/applyCommitsToParentRepo", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/changes", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/chat", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/copilotChatSessions", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/codeReview", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/fileTreeView", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/files", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/git", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/logs", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/remoteAgentHost", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/sessions", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/terminal", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/welcome", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/chatDebug", + "project": "vscode-sessions" } ] } diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 8ebcb1f177b..61ed524f35b 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -391,7 +391,8 @@ const editorProject: string = 'vscode-editor', workbenchProject: string = 'vscode-workbench', extensionsProject: string = 'vscode-extensions', setupProject: string = 'vscode-setup', - serverProject: string = 'vscode-server'; + serverProject: string = 'vscode-server', + sessionsProject: string = 'vscode-sessions'; export function getResource(sourceFile: string): Resource { let resource: string; @@ -416,6 +417,11 @@ export function getResource(sourceFile: string): Resource { return { name: resource, project: workbenchProject }; } else if (/^vs\/workbench/.test(sourceFile)) { return { name: 'vs/workbench', project: workbenchProject }; + } else if (/^vs\/sessions\/contrib/.test(sourceFile)) { + resource = sourceFile.split('/', 4).join('/'); + return { name: resource, project: sessionsProject }; + } else if (/^vs\/sessions/.test(sourceFile)) { + return { name: 'vs/sessions', project: sessionsProject }; } throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); @@ -737,6 +743,10 @@ export function prepareI18nPackFiles(resultingTranslationPaths: TranslationPath[ if (EXTERNAL_EXTENSIONS.find(e => e === resource)) { project = extensionsProject; } + // vscode-setup has its own import path via prepareIslFiles + if (project === setupProject) { + return; + } const contents = xlf.contents!.toString(); log(`Found ${project}: ${resource}`); const parsePromise = getL10nFilesFromXlf(contents); diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 5b0fc9b6ad5..f0b99e4e493 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -646,6 +646,12 @@ "--vscode-searchEditor-findMatchBorder", "--vscode-searchEditor-textInputBorder", "--vscode-selection-background", + "--vscode-sessionsAuxiliaryBar-background", + "--vscode-sessionsChatBar-background", + "--vscode-sessionsPanel-background", + "--vscode-sessionsSidebar-background", + "--vscode-sessionsSidebarHeader-background", + "--vscode-sessionsSidebarHeader-foreground", "--vscode-sessionsUpdateButton-downloadedBackground", "--vscode-sessionsUpdateButton-downloadingBackground", "--vscode-settings-checkboxBackground", diff --git a/build/lib/test/i18n.test.ts b/build/lib/test/i18n.test.ts index 7d5bb0433fe..6c9409bcb4a 100644 --- a/build/lib/test/i18n.test.ts +++ b/build/lib/test/i18n.test.ts @@ -31,7 +31,8 @@ suite('XLF Parser Tests', () => { test('JSON file source path to Transifex resource match', () => { const editorProject: string = 'vscode-editor', - workbenchProject: string = 'vscode-workbench'; + workbenchProject: string = 'vscode-workbench', + sessionsProject: string = 'vscode-sessions'; const platform: i18n.Resource = { name: 'vs/platform', project: editorProject }, editorContrib = { name: 'vs/editor/contrib', project: editorProject }, @@ -40,7 +41,9 @@ suite('XLF Parser Tests', () => { code = { name: 'vs/code', project: workbenchProject }, workbenchParts = { name: 'vs/workbench/contrib/html', project: workbenchProject }, workbenchServices = { name: 'vs/workbench/services/textfile', project: workbenchProject }, - workbench = { name: 'vs/workbench', project: workbenchProject }; + workbench = { name: 'vs/workbench', project: workbenchProject }, + sessionsContrib = { name: 'vs/sessions/contrib/chat', project: sessionsProject }, + sessions = { name: 'vs/sessions', project: sessionsProject }; assert.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); assert.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); @@ -50,5 +53,7 @@ suite('XLF Parser Tests', () => { assert.deepStrictEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); assert.deepStrictEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); assert.deepStrictEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); + assert.deepStrictEqual(i18n.getResource('vs/sessions/contrib/chat/browser/chatWidget'), sessionsContrib); + assert.deepStrictEqual(i18n.getResource('vs/sessions/browser/layoutActions'), sessions); }); }); diff --git a/build/lib/util.ts b/build/lib/util.ts index e4d01e143c9..4203e6e6530 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -381,6 +381,12 @@ export function getElectronVersion(): Record { return { electronVersion, msBuildId }; } +export function getVersionedResourcesFolder(platform: string, commit: string): string { + const productJson = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); + const useVersionedUpdate = platform === 'win32' && productJson.win32VersionedUpdate; + return useVersionedUpdate ? commit.substring(0, 10) : ''; +} + export class VinylStat implements fs.Stats { readonly dev: number; diff --git a/build/next/index.ts b/build/next/index.ts index a5bb0796da0..565bafc72ec 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -101,6 +101,7 @@ const desktopEntryPoints = [ 'vs/workbench/contrib/debug/node/telemetryApp', 'vs/platform/files/node/watcher/watcherMain', 'vs/platform/terminal/node/ptyHostMain', + 'vs/platform/agentHost/node/agentHostMain', 'vs/workbench/api/node/extensionHostProcess', ]; @@ -128,6 +129,7 @@ const serverEntryPoints = [ 'vs/workbench/api/node/extensionHostProcess', 'vs/platform/files/node/watcher/watcherMain', 'vs/platform/terminal/node/ptyHostMain', + 'vs/platform/agentHost/node/agentHostMain', ]; // Bootstrap files per target @@ -280,8 +282,9 @@ const desktopResourcePatterns = [ 'vs/workbench/browser/parts/editor/media/*.png', 'vs/workbench/contrib/debug/browser/media/*.png', - // Sessions - built-in prompts + // Sessions - built-in prompts and skills 'vs/sessions/prompts/*.prompt.md', + 'vs/sessions/skills/**/SKILL.md', ]; // Resources for server target (minimal - no UI) diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 3142db6e89d..8c499c6740f 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -1069,9 +1069,9 @@ } }, "node_modules/tar": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", - "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/build/npm/installStateHash.ts b/build/npm/installStateHash.ts index f52c0a4696d..0b3d9898015 100644 --- a/build/npm/installStateHash.ts +++ b/build/npm/installStateHash.ts @@ -87,7 +87,7 @@ function hashContent(content: string): string { return hash.digest('hex'); } -export function computeState(): PostinstallState { +export function computeState(options?: { ignoreNodeVersion?: boolean }): PostinstallState { const fileHashes: Record = {}; for (const filePath of collectInputFiles()) { const key = path.relative(root, filePath); @@ -97,7 +97,7 @@ export function computeState(): PostinstallState { // file may not be readable } } - return { nodeVersion: process.versions.node, fileHashes }; + return { nodeVersion: options?.ignoreNodeVersion ? '' : process.versions.node, fileHashes }; } export function computeContents(): Record { @@ -141,18 +141,23 @@ export function readSavedContents(): Record | undefined { // When run directly, output state as JSON for tooling (e.g. the vscode-extras extension). if (import.meta.filename === process.argv[1]) { - if (process.argv[2] === '--normalize-file') { - const filePath = process.argv[3]; + const args = new Set(process.argv.slice(2)); + + if (args.has('--normalize-file')) { + const filePath = process.argv[process.argv.indexOf('--normalize-file') + 1]; if (!filePath) { process.exit(1); } process.stdout.write(normalizeFileContent(filePath)); } else { + const ignoreNodeVersion = args.has('--ignore-node-version'); + const current = computeState({ ignoreNodeVersion }); + const saved = readSavedState(); console.log(JSON.stringify({ root, stateContentsFile, - current: computeState(), - saved: readSavedState(), + current, + saved: saved && ignoreNodeVersion ? { nodeVersion: '', fileHashes: saved.fileHashes } : saved, files: [...collectInputFiles(), stateFile], })); } diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index ae2651cd188..db659fa78a4 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -182,6 +182,32 @@ function clearInheritedNpmrcConfig(dir: string, env: NodeJS.ProcessEnv): void { } } +function ensureAgentHarnessLink(sourceRelativePath: string, linkPath: string): 'existing' | 'junction' | 'symlink' | 'hard link' { + if (fs.existsSync(linkPath)) { + return 'existing'; + } + + const sourcePath = path.resolve(path.dirname(linkPath), sourceRelativePath); + const isDirectory = fs.statSync(sourcePath).isDirectory(); + + try { + if (process.platform === 'win32' && isDirectory) { + fs.symlinkSync(sourcePath, linkPath, 'junction'); + return 'junction'; + } + + fs.symlinkSync(sourceRelativePath, linkPath, isDirectory ? 'dir' : 'file'); + return 'symlink'; + } catch (error) { + if (process.platform === 'win32' && !isDirectory && (error as NodeJS.ErrnoException).code === 'EPERM') { + fs.linkSync(sourcePath, linkPath); + return 'hard link'; + } + + throw error; + } +} + async function runWithConcurrency(tasks: (() => Promise)[], concurrency: number): Promise { const errors: Error[] = []; let index = 0; @@ -288,6 +314,37 @@ async function main() { fs.writeFileSync(stateFile, JSON.stringify(_state)); fs.writeFileSync(stateContentsFile, JSON.stringify(computeContents())); + + // Symlink .claude/ files to their canonical locations to test Claude agent harness + const claudeDir = path.join(root, '.claude'); + fs.mkdirSync(claudeDir, { recursive: true }); + + const claudeMdLink = path.join(claudeDir, 'CLAUDE.md'); + const claudeMdLinkType = ensureAgentHarnessLink(path.join('..', '.github', 'copilot-instructions.md'), claudeMdLink); + if (claudeMdLinkType !== 'existing') { + log('.', `Created ${claudeMdLinkType} .claude/CLAUDE.md -> .github/copilot-instructions.md`); + } + + const claudeSkillsLink = path.join(claudeDir, 'skills'); + const claudeSkillsLinkType = ensureAgentHarnessLink(path.join('..', '.agents', 'skills'), claudeSkillsLink); + if (claudeSkillsLinkType !== 'existing') { + log('.', `Created ${claudeSkillsLinkType} .claude/skills -> .agents/skills`); + } + + // Temporary: patch @github/copilot-sdk session.js to fix ESM import + // (missing .js extension on vscode-jsonrpc/node). Fixed upstream in v0.1.32. + // TODO: Remove once @github/copilot-sdk is updated to >=0.1.32 + for (const dir of ['', 'remote']) { + const sessionFile = path.join(root, dir, 'node_modules', '@github', 'copilot-sdk', 'dist', 'session.js'); + if (fs.existsSync(sessionFile)) { + const content = fs.readFileSync(sessionFile, 'utf8'); + const patched = content.replace(/from "vscode-jsonrpc\/node"/g, 'from "vscode-jsonrpc/node.js"'); + if (content !== patched) { + fs.writeFileSync(sessionFile, patched); + log(dir || '.', 'Patched @github/copilot-sdk session.js (vscode-jsonrpc ESM import fix)'); + } + } + } } main().catch(err => { diff --git a/build/npm/preinstall.ts b/build/npm/preinstall.ts index dd53ff44671..82d1919e454 100644 --- a/build/npm/preinstall.ts +++ b/build/npm/preinstall.ts @@ -29,10 +29,10 @@ if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { const requiredMinor = parseInt(requiredVersionMatch[2]); const requiredPatch = parseInt(requiredVersionMatch[3]); - if (majorNodeVersion < requiredMajor || - (majorNodeVersion === requiredMajor && minorNodeVersion < requiredMinor) || - (majorNodeVersion === requiredMajor && minorNodeVersion === requiredMinor && patchNodeVersion < requiredPatch)) { - console.error(`\x1b[1;31m*** Please use Node.js v${requiredVersion} or later for development. Currently using v${process.versions.node}.\x1b[0;0m`); + if (majorNodeVersion !== requiredMajor || + minorNodeVersion < requiredMinor || + (minorNodeVersion === requiredMinor && patchNodeVersion < requiredPatch)) { + console.error(`\x1b[1;31m*** Please use Node.js v${requiredVersion} or newer with the same major version (${requiredMajor}) as specified in .nvmrc. Currently using v${process.versions.node}.\x1b[0;0m`); throw new Error(); } } diff --git a/build/npm/update-localization-extension.ts b/build/npm/update-localization-extension.ts index cb7981b9388..45371dd9cd0 100644 --- a/build/npm/update-localization-extension.ts +++ b/build/npm/update-localization-extension.ts @@ -120,7 +120,7 @@ function update(options: Options) { }); }); } -if (path.basename(process.argv[1]) === 'update-localization-extension.js') { +if (path.basename(process.argv[1]) === 'update-localization-extension.ts') { const options = minimist(process.argv.slice(2), { string: ['location', 'externalExtensionsLocation'] }) as Options; diff --git a/build/package-lock.json b/build/package-lock.json index 644e16f901b..cc1acf90b97 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -3494,22 +3494,9 @@ "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.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "dev": true, "funding": [ { @@ -3519,8 +3506,25 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz", + "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5100,6 +5104,22 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6145,9 +6165,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "dev": true, "funding": [ { @@ -6556,9 +6576,9 @@ "license": "MIT" }, "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, "license": "MIT" }, diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index b9b854eacf2..8d0937b8faf 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,13 +8,166 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "^0.1.1-20", - "@vscode/component-explorer-vite-plugin": "^0.1.1-19", + "@vscode/component-explorer": "^0.1.1-24", + "@vscode/component-explorer-vite-plugin": "^0.1.1-24", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" } }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.2.0.tgz", + "integrity": "sha512-9UAZqn8ywdR70n3GwVle4N8ALosQs4z50N7XMXrSTUVOmVpaBC5kE3TRTT7qQdi3OaQV24mjGuJZsHUmhD+ZXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^1.0.3", + "@octokit/graphql": "^4.3.1", + "@octokit/rest": "^16.43.1" + } + }, + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -49,6 +202,65 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@hediet/semver": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@hediet/semver/-/semver-0.2.2.tgz", + "integrity": "sha512-sdH+TwXwaYOgnKij3QQbJERl2HkJ+l8idWINwHBI+8nXl1yuTCMerDLDPC48t1wbr849qBTpJTV1EJXlh7OGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.0.4", + "@actions/github": "^2.2.0", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "eslint": "^7.1.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -66,6 +278,322 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/core/node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@octokit/core/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", + "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.1" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", + "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.1", + "deprecation": "^2.3.1" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest": { + "version": "16.43.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", + "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^2.4.0", + "@octokit/plugin-paginate-rest": "^1.1.1", + "@octokit/plugin-request-log": "^1.0.0", + "@octokit/plugin-rest-endpoint-methods": "2.4.0", + "@octokit/request": "^5.2.0", + "@octokit/request-error": "^1.0.2", + "atob-lite": "^2.0.0", + "before-after-hook": "^2.0.0", + "btoa-lite": "^1.0.0", + "deprecation": "^2.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", + "octokit-pagination-methods": "^1.1.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "os-name": "^3.1.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, "node_modules/@oxc-project/runtime": { "version": "0.101.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", @@ -675,6 +1203,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -682,24 +1217,187 @@ "dev": true, "license": "MIT" }, - "node_modules/@vscode/component-explorer": { - "version": "0.1.1-20", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-20.tgz", - "integrity": "sha512-HvMWH+wK0SWC+eKZ2cL2LSsWnXiQjyQRURUgW2FBd8SM1G99+kKce0ESTYSr4b0tNJ1/FONE0ixADFlSRduzTg==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "dev": true, "license": "MIT", "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^3.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vscode/component-explorer": { + "version": "0.1.1-27", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-27.tgz", + "integrity": "sha512-FAxC9WlYOaRx6XfU3/ceI2bCOPQp45CpQehUyhG3AbDxLuM8Kv2VyJYJiSsQ0Z2a8cah/3CB729oIiINO19mdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hediet/semver": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@vscode/component-explorer-vite-plugin": { - "version": "0.1.1-19", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-19.tgz", - "integrity": "sha512-V0wMhLvHMbeUHOzwGrBPMwwvcbGhXXaQTCGc9hNfF4fjUutOtQFu5o+9XKDG1hIcKgk5qyvcRoXjVazBcg19lA==", + "version": "0.1.1-27", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-27.tgz", + "integrity": "sha512-wX2Z9e5E3ZdiPRTRIUYOBaReYOfGXd+iseNPAcdfx8gNKJiXrceco4gdKCQv3+WEyvLI3uT/oGdP9ecDcR6mbw==", "dev": true, "license": "MIT", "dependencies": { + "@hediet/semver": "^0.2.2", "tinyglobby": "^0.2.0" }, "peerDependencies": { @@ -717,6 +1415,242 @@ "rollup": "^3.0.0 || ^4.0.0" } }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/atob-lite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -727,6 +1661,415 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/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.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -745,6 +2088,48 @@ } } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -760,6 +2145,203 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -767,6 +2349,73 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -1028,6 +2677,49 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1041,6 +2733,39 @@ "loose-envify": "cli.js" } }, + "node_modules/macos-release": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1060,6 +2785,156 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/octokit-pagination-methods": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1109,6 +2984,47 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1136,6 +3052,56 @@ "react": "^18.3.1" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rolldown": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", @@ -1223,6 +3189,67 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1233,6 +3260,125 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1250,6 +3396,13 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1258,6 +3411,111 @@ "license": "0BSD", "optional": true }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "name": "rolldown-vite", "version": "7.3.1", @@ -1334,6 +3592,73 @@ "optional": true } } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/windows-release": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" } } } diff --git a/build/vite/package.json b/build/vite/package.json index 67e2f227e40..05a96fa1ffd 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/component-explorer": "^0.1.1-20", - "@vscode/component-explorer-vite-plugin": "^0.1.1-19", + "@vscode/component-explorer": "^0.1.1-24", + "@vscode/component-explorer-vite-plugin": "^0.1.1-24", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" diff --git a/build/win32/code.iss b/build/win32/code.iss index 935f17dbe41..53016d814ae 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -86,7 +86,7 @@ Type: files; Name: "{app}\updating_version" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not ShouldUseWindows11ContextMenu +Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent @@ -1284,15 +1284,24 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}Contex Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not ShouldUseWindows11ContextMenu Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Flags: uninsdeletekey; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Flags: uninsdeletekey; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Flags: uninsdeletekey; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Check: ShouldInstallLegacyFolderContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Check: ShouldInstallLegacyFolderContextMenu + +; URL Protocol handler for proxy executable +#ifdef ProxyExeBasename +#ifdef ProxyExeUrlProtocol +Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}"; ValueType: string; ValueName: ""; ValueData: "URL:{#ProxyExeUrlProtocol}"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ProxyExeBasename}.exe"" --open-url -- ""%1"""; Flags: uninsdeletekey +#endif +#endif ; Environment #if "user" == InstallTarget @@ -1527,6 +1536,68 @@ begin Result := IsWindows11OrLater() and not IsWindows10ContextMenuForced(); end; +function HasLegacyFileContextMenu(): Boolean; +begin + Result := RegKeyExists({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}\command'); +end; + +function HasLegacyFolderContextMenu(): Boolean; +begin + Result := RegKeyExists({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}\command'); +end; + +function ShouldRepairFolderContextMenu(): Boolean; +begin + // Repair folder context menu during updates if: + // 1. This is a background update (not a fresh install or manual re-install) + // 2. Windows 11+ with forced classic context menu + // 3. Legacy file context menu exists (user previously selected it) + // 4. Legacy folder context menu is MISSING + Result := IsBackgroundUpdate() + and IsWindows11OrLater() + and IsWindows10ContextMenuForced() + and HasLegacyFileContextMenu() + and not HasLegacyFolderContextMenu(); +end; + +function ShouldInstallLegacyFolderContextMenu(): Boolean; +begin + Result := (WizardIsTaskSelected('addcontextmenufolders') and not ShouldUseWindows11ContextMenu()) or ShouldRepairFolderContextMenu(); +end; + +function BoolToStr(Value: Boolean): String; +begin + if Value then + Result := 'true' + else + Result := 'false'; +end; + +procedure LogContextMenuInstallState(); +begin + Log( + 'Context menu state: ' + + 'isBackgroundUpdate=' + BoolToStr(IsBackgroundUpdate()) + + ', isWindows11OrLater=' + BoolToStr(IsWindows11OrLater()) + + ', isWindows10ContextMenuForced=' + BoolToStr(IsWindows10ContextMenuForced()) + + ', shouldUseWindows11ContextMenu=' + BoolToStr(ShouldUseWindows11ContextMenu()) + + ', hasLegacyFileContextMenu=' + BoolToStr(HasLegacyFileContextMenu()) + + ', hasLegacyFolderContextMenu=' + BoolToStr(HasLegacyFolderContextMenu()) + + ', shouldRepairFolderContextMenu=' + BoolToStr(ShouldRepairFolderContextMenu()) + + ', shouldInstallLegacyFolderContextMenu=' + BoolToStr(ShouldInstallLegacyFolderContextMenu()) + + ', addcontextmenufiles=' + BoolToStr(WizardIsTaskSelected('addcontextmenufiles')) + + ', addcontextmenufolders=' + BoolToStr(WizardIsTaskSelected('addcontextmenufolders')) + ); +end; + +procedure DeleteLegacyContextMenuRegistryKeys(); +begin + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\Drive\shell\{#RegValueName}'); +end; + function GetAppMutex(Value: string): string; begin if IsBackgroundUpdate() then @@ -1604,14 +1675,6 @@ begin Result := ExpandConstant('{#ApplicationName}.cmd'); end; -function BoolToStr(Value: Boolean): String; -begin - if Value then - Result := 'true' - else - Result := 'false'; -end; - function QualityIsInsiders(): boolean; begin if '{#Quality}' = 'insider' then @@ -1634,30 +1697,43 @@ end; function AppxPackageInstalled(const name: String; var ResultCode: Integer): Boolean; begin AppxPackageFullname := ''; + ResultCode := -1; try Log('Get-AppxPackage for package with name: ' + name); ExecAndLogOutput('powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Get-AppxPackage -Name ''' + name + ''' | Select-Object -ExpandProperty PackageFullName'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, @ExecAndGetFirstLineLog); except Log(GetExceptionMessage); + ResultCode := -1; end; if (AppxPackageFullname <> '') then Result := True else - Result := False + Result := False; + + Log('Get-AppxPackage result: name=' + name + ', installed=' + BoolToStr(Result) + ', resultCode=' + IntToStr(ResultCode) + ', packageFullName=' + AppxPackageFullname); end; procedure AddAppxPackage(); var AddAppxPackageResultCode: Integer; + IsCurrentAppxInstalled: Boolean; begin - if not SessionEndFileExists() and not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin + if SessionEndFileExists() then begin + Log('Skipping Add-AppxPackage because session end was detected.'); + exit; + end; + + IsCurrentAppxInstalled := AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode); + if not IsCurrentAppxInstalled then begin Log('Installing appx ' + AppxPackageFullname + ' ...'); #if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); #else ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); #endif - Log('Add-AppxPackage complete.'); + Log('Add-AppxPackage complete with result code ' + IntToStr(AddAppxPackageResultCode) + '.'); + end else begin + Log('Skipping Add-AppxPackage because package is already installed.'); end; end; @@ -1670,6 +1746,7 @@ begin if QualityIsInsiders() and not SessionEndFileExists() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin Log('Deleting old appx ' + AppxPackageFullname + ' installation...'); ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); + Log('Remove-AppxPackage for old appx completed with result code ' + IntToStr(RemoveAppxPackageResultCode) + '.'); DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_{#Arch}.appx')); DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_command.dll')); end; @@ -1680,7 +1757,9 @@ begin #else ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('$packages = Get-AppxPackage ''' + ExpandConstant('{#AppxPackageName}') + '''; foreach ($package in $packages) { Remove-AppxProvisionedPackage -PackageName $package.PackageFullName -Online }; foreach ($package in $packages) { Remove-AppxPackage -Package $package.PackageFullName -AllUsers }'), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); #endif - Log('Remove-AppxPackage for current appx installation complete.'); + Log('Remove-AppxPackage for current appx installation complete with result code ' + IntToStr(RemoveAppxPackageResultCode) + '.'); + end else if not SessionEndFileExists() then begin + Log('Skipping Remove-AppxPackage for current appx because package is not installed.'); end; end; #endif @@ -1692,6 +1771,8 @@ var begin if CurStep = ssPostInstall then begin + LogContextMenuInstallState(); + #ifdef AppxPackageName // Remove the appx package when user has forced Windows 10 context menus via // registry. This handles the case where the user previously had the appx @@ -1701,10 +1782,7 @@ begin end; // Remove the old context menu registry keys if ShouldUseWindows11ContextMenu() then begin - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\Drive\shell\{#RegValueName}'); + DeleteLegacyContextMenuRegistryKeys(); end; #endif @@ -1817,6 +1895,7 @@ begin if not CurUninstallStep = usUninstall then begin exit; end; + #ifdef AppxPackageName RemoveAppxPackage(); #endif diff --git a/cglicenses.json b/cglicenses.json index 48d2c3b093c..2793b7fe2d6 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -209,21 +209,6 @@ "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ] }, - { - // Reason: Missing license file - "name": "readable-web-to-node-stream", - "fullLicenseText": [ - "(The MIT License)", - "", - "Copyright (c) 2019 Borewit", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] - }, { // Reason: The substack org has been deleted on GH "name": "concat-map", @@ -750,5 +735,64 @@ // Reason: mono-repo "name": "@jridgewell/trace-mapping", "fullLicenseTextUri": "https://raw.githubusercontent.com/jridgewell/sourcemaps/refs/heads/main/packages/trace-mapping/LICENSE" + }, + { + // Reason: License text from https://github.com/github/copilot-cli/blob/master/LICENSE.md + // does not include a copyright statement. + "name": "@github/copilot", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + "name": "@github/copilot-darwin-arm64", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + "name": "@github/copilot-darwin-x64", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + "name": "@github/copilot-linux-arm64", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + "name": "@github/copilot-linux-x64", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + "name": "@github/copilot-win32-arm64", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + "name": "@github/copilot-win32-x64", + "prependLicenseText": [ + "Copyright (c) GitHub, Inc." + ] + }, + { + // Reason: NPM package does not include repository URL + "name": "@vscode/fs-copyfile", + "fullLicenseText": [ + "Copyright (c) Microsoft Corporation.", + "", + "MIT License", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] } ] diff --git a/cgmanifest.json b/cgmanifest.json index 1b1e1711ccf..a617dd1c78c 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -516,12 +516,12 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "6add85e4c46b8be383c8b637102d6b6fd206adce", - "tag": "22.22.0" + "commitHash": "b4acf0c9393e4b31c4937564f059c672967161d8", + "tag": "22.22.1" } }, "isOnlyProductionDependency": true, - "version": "22.22.0" + "version": "22.22.1" }, { "component": { @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "69c8cbf259da0f84e9c1db04958516a68f7170aa", - "tag": "39.8.0" + "commitHash": "e6928c13198c854aa014c319d72eea599e2e0ee7", + "tag": "39.8.3" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "39.8.0" + "version": "39.8.3" }, { "component": { @@ -606,7 +606,7 @@ } }, "license": "MIT and Creative Commons Attribution 4.0", - "version": "0.0.41" + "version": "0.0.46-0" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index afe353213b1..e50f85de23a 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -2865,9 +2865,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 423224e10c5..6f54ec61cbb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -53,7 +53,7 @@ cfg-if = "1.0.0" pin-project = "1.1.0" console = "0.15.7" bytes = "1.11.1" -tar = "0.4.38" +tar = "0.4.45" [build-dependencies] serde = { version="1.0.163", features = ["derive"] } diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 6e21ddb3729..39091fdc1db 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -722,9 +722,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -1922,9 +1922,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5287,9 +5287,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5328,9 +5328,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5369,9 +5369,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5410,9 +5410,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5451,9 +5451,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5492,9 +5492,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5533,9 +5533,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5574,9 +5574,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5615,9 +5615,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5656,9 +5656,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5697,9 +5697,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5738,9 +5738,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5779,9 +5779,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -5820,9 +5820,9 @@ Furthermore, the crates are (usually automatically) derived from Apple SDKs, and that may have implications for licensing, see below for details. [#23]: https://github.com/madsmtm/objc2/issues/23 -[MIT]: https://opensource.org/license/MIT -[Zlib]: https://zlib.net/zlib_license.html -[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 +[MIT]: ./LICENSE-MIT.txt +[Zlib]: ./LICENSE-ZLIB.txt +[Apache-2.0]: ./LICENSE-APACHE.txt ## Apple SDKs @@ -10137,7 +10137,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tar 0.4.44 - MIT OR Apache-2.0 +tar 0.4.45 - MIT OR Apache-2.0 https://github.com/alexcrichton/tar-rs Copyright (c) The tar-rs Project Contributors @@ -14089,9 +14089,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Some files in the "tests/data" subdirectory of this repository are under other -licences; see files named LICENSE.*.txt for details. --------------------------------------------------------- --------------------------------------------------------- diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index b73d0aa885b..6c301ca9502 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -8,7 +8,7 @@ use std::process::Command; use clap::Parser; use cli::{ - commands::{args, serve_web, tunnels, update, version, CommandContext}, + commands::{agent_host, args, serve_web, tunnels, update, version, CommandContext}, constants::get_default_user_agent, desktop, log, state::LauncherPaths, @@ -103,6 +103,10 @@ async fn main() -> Result<(), std::convert::Infallible> { serve_web::serve_web(context!(), sw_args).await } + Some(args::Commands::AgentHost(ah_args)) => { + agent_host::agent_host(context!(), ah_args).await + } + Some(args::Commands::Tunnel(mut tunnel_args)) => match tunnel_args.subcommand.take() { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, Some(args::TunnelSubcommand::Unregister) => tunnels::unregister(context!()).await, diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 027716947a3..1b706653c6e 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -5,6 +5,7 @@ mod context; +pub mod agent_host; pub mod args; pub mod serve_web; pub mod tunnels; diff --git a/cli/src/commands/agent_host.rs b/cli/src/commands/agent_host.rs new file mode 100644 index 00000000000..a5291281ba0 --- /dev/null +++ b/cli/src/commands/agent_host.rs @@ -0,0 +1,738 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::convert::Infallible; +use std::fs; +use std::io::{Read, Write}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response, Server}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::sync::Mutex; + +use crate::async_pipe::{get_socket_name, get_socket_rw_stream, AsyncPipe}; +use crate::constants::VSCODE_CLI_QUALITY; +use crate::download_cache::DownloadCache; +use crate::log; +use crate::options::Quality; +use crate::tunnels::paths::{get_server_folder_name, SERVER_FOLDER_NAME}; +use crate::tunnels::shutdown_signal::ShutdownRequest; +use crate::update_service::{ + unzip_downloaded_release, Platform, Release, TargetKind, UpdateService, +}; +use crate::util::command::new_script_command; +use crate::util::errors::AnyError; +use crate::util::http::{self, ReqwestSimpleHttp}; +use crate::util::io::SilentCopyProgress; +use crate::util::sync::{new_barrier, Barrier, BarrierOpener}; +use crate::{ + tunnels::legal, + util::{errors::CodeError, prereqs::PreReqChecker}, +}; + +use super::{args::AgentHostArgs, CommandContext}; + +/// How often to check for server updates. +const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); +/// How often to re-check whether the server has exited when an update is pending. +const UPDATE_POLL_INTERVAL: Duration = Duration::from_secs(10 * 60); +/// How long to wait for the server to signal readiness. +const STARTUP_TIMEOUT: Duration = Duration::from_secs(30); + +/// Runs a local agent host server. Downloads the latest VS Code server on +/// demand, starts it with `--enable-remote-auto-shutdown`, and proxies +/// WebSocket connections from a local TCP port to the server's agent host +/// socket. The server auto-shuts down when idle; the CLI checks for updates +/// in the background and starts the latest version on the next connection. +pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result { + legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; + + let platform: Platform = PreReqChecker::new().verify().await?; + + if !args.without_connection_token { + if let Some(p) = args.connection_token_file.as_deref() { + let token = fs::read_to_string(PathBuf::from(p)) + .map_err(CodeError::CouldNotReadConnectionTokenFile)?; + args.connection_token = Some(token.trim().to_string()); + } else { + let token_path = ctx.paths.root().join("agent-host-token"); + let token = mint_connection_token(&token_path, args.connection_token.clone()) + .map_err(CodeError::CouldNotCreateConnectionTokenFile)?; + args.connection_token = Some(token); + args.connection_token_file = Some(token_path.to_string_lossy().to_string()); + } + } + + let manager = AgentHostManager::new(&ctx, platform, args.clone())?; + + // Eagerly resolve the latest version so the first connection is fast. + // Skip when using a dev override since updates don't apply. + if option_env!("VSCODE_CLI_OVERRIDE_SERVER_PATH").is_none() { + match manager.get_latest_release().await { + Ok(release) => { + if let Err(e) = manager.ensure_downloaded(&release).await { + warning!(ctx.log, "Error downloading latest server version: {}", e); + } + } + Err(e) => warning!(ctx.log, "Error resolving initial server version: {}", e), + } + + // Start background update checker + let manager_for_updates = manager.clone(); + tokio::spawn(async move { + manager_for_updates.run_update_loop().await; + }); + } + + // Bind the HTTP/WebSocket proxy + let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]); + + let addr: SocketAddr = match &args.host { + Some(h) => SocketAddr::new(h.parse().map_err(CodeError::InvalidHostAddress)?, args.port), + None => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port), + }; + let builder = Server::try_bind(&addr).map_err(CodeError::CouldNotListenOnInterface)?; + let bound_addr = builder.local_addr(); + + let mut url = format!("ws://{bound_addr}"); + if let Some(ct) = &args.connection_token { + url.push_str(&format!("?tkn={ct}")); + } + ctx.log + .result(format!("Agent host proxy listening on {url}")); + + let manager_for_svc = manager.clone(); + let make_svc = move || { + let mgr = manager_for_svc.clone(); + let service = service_fn(move |req| { + let mgr = mgr.clone(); + async move { handle_request(mgr, req).await } + }); + async move { Ok::<_, Infallible>(service) } + }; + + let server_future = builder + .serve(make_service_fn(|_| make_svc())) + .with_graceful_shutdown(async { + let _ = shutdown.wait().await; + }); + + let r = server_future.await; + manager.kill_running_server().await; + r.map_err(CodeError::CouldNotListenOnInterface)?; + + Ok(0) +} + +// ---- AgentHostManager ------------------------------------------------------- + +/// State of the running VS Code server process. +struct RunningServer { + child: tokio::process::Child, + commit: String, +} + +/// Manages the VS Code server lifecycle: on-demand start, auto-restart +/// after idle shutdown, and background update checking. +struct AgentHostManager { + log: log::Logger, + args: AgentHostArgs, + platform: Platform, + cache: DownloadCache, + update_service: UpdateService, + /// The latest known release, with the time it was checked. + latest_release: Mutex>, + /// The currently running server, if any. + running: Mutex>, + /// Barrier that opens when a server is ready (socket path available). + /// Reset each time a new server is started. + ready: Mutex>>>, +} + +impl AgentHostManager { + fn new( + ctx: &CommandContext, + platform: Platform, + args: AgentHostArgs, + ) -> Result, CodeError> { + // Seed latest_release from cache if available + let cache = ctx.paths.server_cache.clone(); + Ok(Arc::new(Self { + log: ctx.log.clone(), + args, + platform, + cache, + update_service: UpdateService::new( + ctx.log.clone(), + Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), + ), + latest_release: Mutex::new(None), + running: Mutex::new(None), + ready: Mutex::new(None), + })) + } + + /// Returns the socket path to a running server, starting one if needed. + async fn ensure_server(self: &Arc) -> Result { + // Fast path: if we already have a barrier, wait on it + { + let ready = self.ready.lock().await; + if let Some(barrier) = &*ready { + if barrier.is_open() { + // Check if the process is still running + let running = self.running.lock().await; + if running.is_some() { + return barrier + .clone() + .wait() + .await + .unwrap() + .map_err(CodeError::ServerDownloadError); + } + } else { + // Still starting up, wait for it + let mut barrier = barrier.clone(); + drop(ready); + return barrier + .wait() + .await + .unwrap() + .map_err(CodeError::ServerDownloadError); + } + } + } + + // Need to start a new server + self.start_server().await + } + + /// Starts the server with the latest already-downloaded version. + /// Only blocks on a network fetch if no version has been downloaded yet. + async fn start_server(self: &Arc) -> Result { + let (release, server_dir) = self.get_cached_or_download().await?; + + let (mut barrier, opener) = new_barrier::>(); + { + let mut ready = self.ready.lock().await; + *ready = Some(barrier.clone()); + } + + let self_clone = self.clone(); + let release_clone = release.clone(); + tokio::spawn(async move { + self_clone + .run_server(release_clone, server_dir, opener) + .await; + }); + + barrier + .wait() + .await + .unwrap() + .map_err(CodeError::ServerDownloadError) + } + + /// Runs the server process to completion, handling readiness signaling. + async fn run_server( + self: &Arc, + release: Release, + server_dir: PathBuf, + opener: BarrierOpener>, + ) { + let executable = if let Some(p) = option_env!("VSCODE_CLI_OVERRIDE_SERVER_PATH") { + PathBuf::from(p) + } else { + server_dir + .join(SERVER_FOLDER_NAME) + .join("bin") + .join(release.quality.server_entrypoint()) + }; + + let agent_host_socket = get_socket_name(); + let mut cmd = new_script_command(&executable); + cmd.stdin(std::process::Stdio::null()); + cmd.stderr(std::process::Stdio::piped()); + cmd.stdout(std::process::Stdio::piped()); + cmd.arg("--socket-path"); + cmd.arg(get_socket_name()); + cmd.arg("--agent-host-path"); + cmd.arg(&agent_host_socket); + cmd.args([ + "--start-server", + "--accept-server-license-terms", + "--enable-remote-auto-shutdown", + ]); + + if let Some(a) = &self.args.server_data_dir { + cmd.arg("--server-data-dir"); + cmd.arg(a); + } + if self.args.without_connection_token { + cmd.arg("--without-connection-token"); + } + if let Some(ct) = &self.args.connection_token_file { + cmd.arg("--connection-token-file"); + cmd.arg(ct); + } + cmd.env_remove("VSCODE_DEV"); + + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + opener.open(Err(e.to_string())); + return; + } + }; + + let commit_prefix = &release.commit[..release.commit.len().min(7)]; + let (mut stdout, mut stderr) = ( + BufReader::new(child.stdout.take().unwrap()).lines(), + BufReader::new(child.stderr.take().unwrap()).lines(), + ); + + // Wait for readiness with a timeout + let mut opener = Some(opener); + let socket_path = agent_host_socket.clone(); + let startup_deadline = tokio::time::sleep(STARTUP_TIMEOUT); + tokio::pin!(startup_deadline); + + let mut ready = false; + loop { + tokio::select! { + Ok(Some(l)) = stdout.next_line() => { + debug!(self.log, "[{} stdout]: {}", commit_prefix, l); + if !ready && l.contains("Agent host server listening on") { + ready = true; + if let Some(o) = opener.take() { + o.open(Ok(socket_path.clone())); + } + } + } + Ok(Some(l)) = stderr.next_line() => { + debug!(self.log, "[{} stderr]: {}", commit_prefix, l); + } + _ = &mut startup_deadline, if !ready => { + warning!(self.log, "[{}]: Server did not become ready within {}s", commit_prefix, STARTUP_TIMEOUT.as_secs()); + // Don't fail — the server may still start up, just slowly + if let Some(o) = opener.take() { + o.open(Ok(socket_path.clone())); + } + ready = true; + } + e = child.wait() => { + info!(self.log, "[{} process]: exited: {:?}", commit_prefix, e); + if let Some(o) = opener.take() { + o.open(Err(format!("Server exited before ready: {e:?}"))); + } + break; + } + } + + if ready { + break; + } + } + + // Store the running server state + { + let mut running = self.running.lock().await; + *running = Some(RunningServer { + child, + commit: release.commit.clone(), + }); + } + + if !ready { + return; + } + + info!(self.log, "[{}]: Server ready", commit_prefix); + + // Continue reading output until the process exits + let log = self.log.clone(); + let commit_prefix = commit_prefix.to_string(); + let self_clone = self.clone(); + tokio::spawn(async move { + loop { + tokio::select! { + Ok(Some(l)) = stdout.next_line() => { + debug!(log, "[{} stdout]: {}", commit_prefix, l); + } + Ok(Some(l)) = stderr.next_line() => { + debug!(log, "[{} stderr]: {}", commit_prefix, l); + } + else => break, + } + } + + // Server process has exited (auto-shutdown or crash) + info!(log, "[{}]: Server process ended", commit_prefix); + let mut running = self_clone.running.lock().await; + if let Some(r) = &*running { + if r.commit == commit_prefix || r.commit.starts_with(&commit_prefix) { + // Only clear if it's still our server + } + } + *running = None; + }); + } + + /// Returns a release and its local directory. Prefers the latest known + /// release if it has already been downloaded; otherwise falls back to any + /// cached version. Only fetches from the network and downloads if + /// nothing is cached at all. + async fn get_cached_or_download(&self) -> Result<(Release, PathBuf), CodeError> { + // When using a dev override, skip the update service entirely - + // the override path is used directly by run_server(). + if option_env!("VSCODE_CLI_OVERRIDE_SERVER_PATH").is_some() { + let release = Release { + name: String::new(), + commit: String::from("dev"), + platform: self.platform, + target: TargetKind::Server, + quality: Quality::Insiders, + }; + return Ok((release, PathBuf::new())); + } + + // Best case: the latest known release is already downloaded + if let Some((_, release)) = &*self.latest_release.lock().await { + let name = get_server_folder_name(release.quality, &release.commit); + if let Some(dir) = self.cache.exists(&name) { + return Ok((release.clone(), dir)); + } + } + + let quality = VSCODE_CLI_QUALITY + .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .and_then(|q| { + Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) + })?; + + // Fall back to any cached version (still instant, just not the newest). + // Cache entries are named "-" via get_server_folder_name. + for entry in self.cache.get() { + if let Some(dir) = self.cache.exists(&entry) { + let (entry_quality, commit) = match entry.split_once('-') { + Some((q, c)) => match Quality::try_from(q.to_lowercase().as_str()) { + Ok(parsed) => (parsed, c.to_string()), + Err(_) => (quality, entry.clone()), + }, + None => (quality, entry.clone()), + }; + let release = Release { + name: String::new(), + commit, + platform: self.platform, + target: TargetKind::Server, + quality: entry_quality, + }; + return Ok((release, dir)); + } + } + + // Nothing cached — must fetch and download (blocks the first connection) + info!(self.log, "No cached server version, downloading latest..."); + let release = self.get_latest_release().await?; + let dir = self.ensure_downloaded(&release).await?; + Ok((release, dir)) + } + + /// Ensures the release is downloaded, returning the server directory. + async fn ensure_downloaded(&self, release: &Release) -> Result { + let cache_name = get_server_folder_name(release.quality, &release.commit); + if let Some(dir) = self.cache.exists(&cache_name) { + return Ok(dir); + } + + info!(self.log, "Downloading server {}", release.commit); + let release = release.clone(); + let log = self.log.clone(); + let update_service = self.update_service.clone(); + self.cache + .create(&cache_name, |target_dir| async move { + let tmpdir = tempfile::tempdir().unwrap(); + let response = update_service.get_download_stream(&release).await?; + let name = response.url_path_basename().unwrap(); + let archive_path = tmpdir.path().join(name); + http::download_into_file( + &archive_path, + log.get_download_logger("Downloading server:"), + response, + ) + .await?; + let server_dir = target_dir.join(SERVER_FOLDER_NAME); + unzip_downloaded_release(&archive_path, &server_dir, SilentCopyProgress())?; + Ok(()) + }) + .await + .map_err(|e| CodeError::ServerDownloadError(e.to_string())) + } + + /// Gets the latest release, caching the result. + async fn get_latest_release(&self) -> Result { + let mut latest = self.latest_release.lock().await; + let now = Instant::now(); + + let quality = VSCODE_CLI_QUALITY + .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) + .and_then(|q| { + Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality")) + })?; + + let result = self + .update_service + .get_latest_commit(self.platform, TargetKind::Server, quality) + .await + .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); + + // If the update service is unavailable, fall back to the cached version + if let (Err(e), Some((_, previous))) = (&result, latest.clone()) { + warning!(self.log, "Error checking for updates, using cached: {}", e); + *latest = Some((now, previous.clone())); + return Ok(previous); + } + + let release = result?; + debug!(self.log, "Resolved server version: {}", release); + *latest = Some((now, release.clone())); + Ok(release) + } + + /// Background loop: checks for updates periodically and pre-downloads + /// new versions when the server is idle. + async fn run_update_loop(self: Arc) { + let mut interval = tokio::time::interval(UPDATE_CHECK_INTERVAL); + interval.tick().await; // skip the immediate first tick + + loop { + interval.tick().await; + + let new_release = match self.get_latest_release().await { + Ok(r) => r, + Err(e) => { + warning!(self.log, "Update check failed: {}", e); + continue; + } + }; + + // Check if we already have this version + let name = get_server_folder_name(new_release.quality, &new_release.commit); + if self.cache.exists(&name).is_some() { + continue; + } + + info!(self.log, "New server version available: {}", new_release); + + // Wait until the server is not running before downloading + loop { + { + let running = self.running.lock().await; + if running.is_none() { + break; + } + } + debug!(self.log, "Server still running, waiting before updating..."); + tokio::time::sleep(UPDATE_POLL_INTERVAL).await; + } + + // Download the new version + match self.ensure_downloaded(&new_release).await { + Ok(_) => info!(self.log, "Updated server to {}", new_release), + Err(e) => warning!(self.log, "Failed to download update: {}", e), + } + } + } + + /// Kills the currently running server, if any. + async fn kill_running_server(&self) { + let mut running = self.running.lock().await; + if let Some(mut server) = running.take() { + let _ = server.child.kill().await; + } + } +} + +// ---- HTTP/WebSocket proxy --------------------------------------------------- + +/// Proxies an incoming HTTP/WebSocket request to the agent host's Unix socket. +async fn handle_request( + manager: Arc, + req: Request, +) -> Result, Infallible> { + let socket_path = match manager.ensure_server().await { + Ok(p) => p, + Err(e) => { + error!(manager.log, "Error starting agent host: {:?}", e); + return Ok(Response::builder() + .status(503) + .body(Body::from(format!("Error starting agent host: {e:?}"))) + .unwrap()); + } + }; + + let is_upgrade = req.headers().contains_key(hyper::header::UPGRADE); + + let rw = match get_socket_rw_stream(&socket_path).await { + Ok(rw) => rw, + Err(e) => { + error!( + manager.log, + "Error connecting to agent host socket: {:?}", e + ); + return Ok(Response::builder() + .status(503) + .body(Body::from(format!("Error connecting to agent host: {e:?}"))) + .unwrap()); + } + }; + + if is_upgrade { + Ok(forward_ws_to_server(rw, req).await) + } else { + Ok(forward_http_to_server(rw, req).await) + } +} + +/// Proxies a standard HTTP request through the socket. +async fn forward_http_to_server(rw: AsyncPipe, req: Request) -> Response { + let (mut request_sender, connection) = + match hyper::client::conn::Builder::new().handshake(rw).await { + Ok(r) => r, + Err(e) => return connection_err(e), + }; + + tokio::spawn(connection); + + request_sender + .send_request(req) + .await + .unwrap_or_else(connection_err) +} + +/// Proxies a WebSocket upgrade request through the socket. +async fn forward_ws_to_server(rw: AsyncPipe, mut req: Request) -> Response { + let (mut request_sender, connection) = + match hyper::client::conn::Builder::new().handshake(rw).await { + Ok(r) => r, + Err(e) => return connection_err(e), + }; + + tokio::spawn(connection); + + let mut proxied_req = Request::builder().uri(req.uri()); + for (k, v) in req.headers() { + proxied_req = proxied_req.header(k, v); + } + + let mut res = request_sender + .send_request(proxied_req.body(Body::empty()).unwrap()) + .await + .unwrap_or_else(connection_err); + + let mut proxied_res = Response::new(Body::empty()); + *proxied_res.status_mut() = res.status(); + for (k, v) in res.headers() { + proxied_res.headers_mut().insert(k, v.clone()); + } + + if res.status() == hyper::StatusCode::SWITCHING_PROTOCOLS { + tokio::spawn(async move { + let (s_req, s_res) = + tokio::join!(hyper::upgrade::on(&mut req), hyper::upgrade::on(&mut res)); + + if let (Ok(mut s_req), Ok(mut s_res)) = (s_req, s_res) { + let _ = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await; + } + }); + } + + proxied_res +} + +fn connection_err(err: hyper::Error) -> Response { + Response::builder() + .status(503) + .body(Body::from(format!( + "Error connecting to agent host: {err:?}" + ))) + .unwrap() +} + +fn mint_connection_token(path: &Path, prefer_token: Option) -> std::io::Result { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.read(true); + #[cfg(not(windows))] + f.mode(0o600); + let mut f = f.open(path)?; + + if prefer_token.is_none() { + let mut t = String::new(); + f.read_to_string(&mut t)?; + let t = t.trim(); + if !t.is_empty() { + return Ok(t.to_string()); + } + } + + f.set_len(0)?; + let prefer_token = prefer_token.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + f.write_all(prefer_token.as_bytes())?; + Ok(prefer_token) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn mint_connection_token_generates_and_persists() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("token"); + + // First call with no preference generates a UUID and persists it + let token1 = mint_connection_token(&path, None).unwrap(); + assert!(!token1.is_empty()); + assert_eq!(fs::read_to_string(&path).unwrap(), token1); + + // Second call with no preference reads the existing token + let token2 = mint_connection_token(&path, None).unwrap(); + assert_eq!(token1, token2); + } + + #[test] + fn mint_connection_token_respects_preferred() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("token"); + + // Providing a preferred token writes it to the file + let token = mint_connection_token(&path, Some("my-token".to_string())).unwrap(); + assert_eq!(token, "my-token"); + assert_eq!(fs::read_to_string(&path).unwrap(), "my-token"); + } + + #[test] + fn mint_connection_token_preferred_overwrites_existing() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("token"); + + mint_connection_token(&path, None).unwrap(); + + // Providing a preference overwrites any existing token + let token = mint_connection_token(&path, Some("override".to_string())).unwrap(); + assert_eq!(token, "override"); + assert_eq!(fs::read_to_string(&path).unwrap(), "override"); + } +} diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 6301bdd3104..9211cdc38d4 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -185,6 +185,10 @@ pub enum Commands { /// Runs the control server on process stdin/stdout #[clap(hide = true)] CommandShell(CommandShellArgs), + + /// Runs a local agent host server. + #[clap(name = "agent-host")] + AgentHost(AgentHostArgs), } #[derive(Args, Debug, Clone)] @@ -221,6 +225,31 @@ pub struct ServeWebArgs { pub commit_id: Option, } +#[derive(Args, Debug, Clone)] +pub struct AgentHostArgs { + /// Host to listen on, defaults to 'localhost' + #[clap(long)] + pub host: Option, + /// Port to listen on. If 0 is passed a random free port is picked. + #[clap(long, default_value_t = 0)] + pub port: u16, + /// A secret that must be included with all requests. + #[clap(long)] + pub connection_token: Option, + /// A file containing a secret that must be included with all requests. + #[clap(long)] + pub connection_token_file: Option, + /// Run without a connection token. Only use this if the connection is secured by other means. + #[clap(long)] + pub without_connection_token: bool, + /// If set, the user accepts the server license terms and the server will be started without a user prompt. + #[clap(long)] + pub accept_server_license_terms: bool, + /// Specifies the directory that server data is kept in. + #[clap(long)] + pub server_data_dir: Option, +} + #[derive(Args, Debug, Clone)] pub struct CommandShellArgs { #[clap(flatten)] diff --git a/eslint.config.js b/eslint.config.js index e7bc3f37aaa..26943ddee99 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -92,6 +92,7 @@ export default tseslint.config( 'local/code-no-localized-model-description': 'warn', 'local/code-policy-localization-key-match': 'warn', 'local/code-no-localization-template-literals': 'error', + 'local/code-no-icons-in-localized-strings': 'warn', 'local/code-no-http-import': ['warn', { target: 'src/vs/**' }], 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], 'local/code-layering': [ @@ -183,6 +184,18 @@ export default tseslint.config( ] } }, + // Disallow common telemetry properties in event data + { + files: [ + 'src/**/*.ts', + ], + plugins: { + 'local': pluginLocal, + }, + rules: { + 'local/code-no-telemetry-common-property': 'warn', + } + }, // Disallow 'in' operator except in type predicates { files: [ @@ -322,6 +335,7 @@ export default tseslint.config( 'src/vs/workbench/services/remote/common/tunnelModel.ts', 'src/vs/workbench/services/search/common/textSearchManager.ts', 'src/vs/workbench/test/browser/workbenchTestServices.ts', + 'src/vs/platform/agentHost/common/state/protocol/reducers.ts', 'test/automation/src/playwrightDriver.ts', '.eslint-plugin-local/**/*', ], @@ -1042,6 +1056,33 @@ export default tseslint.config( ] } }, + // electron-main layer: prevent static imports of heavy node_modules + // that would be synchronously loaded on startup + { + files: [ + 'src/vs/code/electron-main/**/*.ts', + 'src/vs/code/node/**/*.ts', + 'src/vs/platform/*/electron-main/**/*.ts', + 'src/vs/platform/*/node/**/*.ts', + ], + languageOptions: { + parser: tseslint.parser, + }, + plugins: { + 'local': pluginLocal, + }, + rules: { + 'local/code-no-static-node-module-import': [ + 'error', + // Files that run in separate processes, not on the electron-main startup path + 'src/vs/platform/agentHost/node/copilot/**/*.ts', + 'src/vs/platform/files/node/watcher/**/*.ts', + 'src/vs/platform/terminal/node/**/*.ts', + // Files that use small, safe modules + 'src/vs/platform/environment/node/argv.ts', + ] + } + }, // browser/electron-browser layer { files: [ @@ -1456,6 +1497,7 @@ export default tseslint.config( // - electron-main 'when': 'hasNode', 'allow': [ + '@github/copilot-sdk', '@parcel/watcher', '@vscode/sqlite3', '@vscode/vscode-languagedetection', @@ -1498,6 +1540,7 @@ export default tseslint.config( 'vscode-regexpp', 'vscode-textmate', 'worker_threads', + 'ws', '@xterm/addon-clipboard', '@xterm/addon-image', '@xterm/addon-ligatures', @@ -1947,11 +1990,14 @@ export default tseslint.config( 'vs/editor/~', 'vs/editor/contrib/*/~', 'vs/editor/editor.all.js', + 'vs/sessions/~', + 'vs/sessions/services/*/~', + 'vs/sessions/contrib/*/~', 'vs/workbench/~', 'vs/workbench/api/~', 'vs/workbench/services/*/~', 'vs/workbench/contrib/*/~', - 'vs/workbench/contrib/terminal/terminal.all.js' + 'vs/workbench/contrib/terminal/terminal.all.js', ] }, { @@ -1965,6 +2011,7 @@ export default tseslint.config( 'vs/editor/contrib/*/~', 'vs/editor/editor.all.js', 'vs/sessions/~', + 'vs/sessions/services/*/~', 'vs/sessions/contrib/*/~', 'vs/workbench/~', 'vs/workbench/api/~', @@ -1977,13 +2024,14 @@ export default tseslint.config( 'target': 'src/vs/sessions/sessions.web.main.ts', 'layer': 'browser', 'restrictions': [ - 'vs/base/*/~', + 'vs/base/~', 'vs/base/parts/*/~', 'vs/platform/*/~', 'vs/editor/~', 'vs/editor/contrib/*/~', 'vs/editor/editor.all.js', 'vs/sessions/~', + 'vs/sessions/services/*/~', 'vs/sessions/contrib/*/~', 'vs/workbench/~', 'vs/workbench/api/~', @@ -2083,6 +2131,7 @@ export default tseslint.config( 'vs/editor/contrib/*/~', 'vs/workbench/~', 'vs/workbench/services/*/~', + 'vs/sessions/~', 'vs/sessions/services/*/~', { 'when': 'test', @@ -2271,21 +2320,13 @@ export default tseslint.config( '@typescript-eslint': tseslint.plugin, }, rules: { - '@typescript-eslint/naming-convention': [ + 'no-restricted-syntax': [ 'warn', { - 'selector': 'default', - 'modifiers': ['private'], - 'format': null, - 'leadingUnderscore': 'require' + selector: ':matches(PropertyDefinition, TSParameterProperty, MethodDefinition[key.name!="constructor"])[accessibility="private"]', + message: 'Use #private instead', }, - { - 'selector': 'default', - 'modifiers': ['public'], - 'format': null, - 'leadingUnderscore': 'forbid' - } - ] + ], } }, // Additional extension strictness rules @@ -2349,7 +2390,10 @@ export default tseslint.config( 'selector': `NewExpression[callee.object.name='Intl']`, 'message': 'Use safeIntl helper instead for safe and lazy use of potentially expensive Intl methods.' }, + { + 'selector': 'TSAsExpression[typeAnnotation.type="TSTypeReference"][typeAnnotation.typeName.type="TSQualifiedName"][typeAnnotation.typeName.left.type="Identifier"][typeAnnotation.typeName.left.name="sinon"][typeAnnotation.typeName.right.name="SinonStub"]', + 'message': `Avoid casting with 'as sinon.SinonStub'. Prefer typed stubs from 'sinon.stub(...)' or capture the stub in a typed variable.` + }, ], } - }, -); + }); diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 6eb3de2f572..fa99e209e40 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "2e6860d87d4019b0b793b1e21e9e5c82185a01aa" + "commitHash": "32ee13e806edf480ef9423adcf5ecf61ba39561b" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index 89b08d5c5b2..e344197ccb4 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/2e6860d87d4019b0b793b1e21e9e5c82185a01aa", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/32ee13e806edf480ef9423adcf5ecf61ba39561b", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -5578,6 +5578,12 @@ { "include": "#preprocessor-app-directive-property" }, + { + "include": "#preprocessor-app-directive-exclude" + }, + { + "include": "#preprocessor-app-directive-include" + }, { "include": "#preprocessor-app-directive-project" }, @@ -5627,6 +5633,28 @@ } } }, + "preprocessor-app-directive-exclude": { + "match": "\\b(exclude)\\b\\s*(.*)?\\s*", + "captures": { + "1": { + "name": "keyword.preprocessor.exclude.cs" + }, + "2": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, + "preprocessor-app-directive-include": { + "match": "\\b(include)\\b\\s*(.*)?\\s*", + "captures": { + "1": { + "name": "keyword.preprocessor.include.cs" + }, + "2": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, "preprocessor-app-directive-project": { "match": "\\b(project)\\b\\s*(.*)?\\s*", "captures": { diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 1fd31eeae79..0abce3a2cfb 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -218,7 +218,7 @@ }, "scope": "resource", "default": [], - "description": "%css.lint.validProperties.desc%" + "markdownDescription": "%css.lint.validProperties.desc%" }, "css.lint.ieHack": { "type": "string", @@ -534,7 +534,7 @@ }, "scope": "resource", "default": [], - "description": "%scss.lint.validProperties.desc%" + "markdownDescription": "%scss.lint.validProperties.desc%" }, "scss.lint.ieHack": { "type": "string", @@ -840,7 +840,7 @@ }, "scope": "resource", "default": [], - "description": "%less.lint.validProperties.desc%" + "markdownDescription": "%less.lint.validProperties.desc%" }, "less.lint.ieHack": { "type": "string", diff --git a/extensions/css-language-features/package.nls.json b/extensions/css-language-features/package.nls.json index 057ec214bc2..d3de22412c2 100644 --- a/extensions/css-language-features/package.nls.json +++ b/extensions/css-language-features/package.nls.json @@ -33,7 +33,7 @@ "css.format.enable.desc": "Enable/disable default CSS formatter.", "css.format.newlineBetweenSelectors.desc": "Separate selectors with a new line.", "css.format.newlineBetweenRules.desc": "Separate rulesets by a blank line.", - "css.format.spaceAroundSelectorSeparator.desc": "Ensure a space character around selector separators '>', '+', '~' (e.g. `a > b`).", + "css.format.spaceAroundSelectorSeparator.desc": "Ensure a space character around selector separators `>`, `+`, `~` (e.g. `a > b`).", "css.format.braceStyle.desc": "Put braces on the same line as rules (`collapse`) or put braces on own line (`expand`).", "css.format.preserveNewLines.desc": "Whether existing line breaks before rules and declarations should be preserved.", "css.format.maxPreserveNewLines.desc": "Maximum number of line breaks to be preserved in one chunk, when `#css.format.preserveNewLines#` is enabled.", @@ -67,7 +67,7 @@ "less.format.enable.desc": "Enable/disable default LESS formatter.", "less.format.newlineBetweenSelectors.desc": "Separate selectors with a new line.", "less.format.newlineBetweenRules.desc": "Separate rulesets by a blank line.", - "less.format.spaceAroundSelectorSeparator.desc": "Ensure a space character around selector separators '>', '+', '~' (e.g. `a > b`).", + "less.format.spaceAroundSelectorSeparator.desc": "Ensure a space character around selector separators `>`, `+`, `~` (e.g. `a > b`).", "less.format.braceStyle.desc": "Put braces on the same line as rules (`collapse`) or put braces on own line (`expand`).", "less.format.preserveNewLines.desc": "Whether existing line breaks before rules and declarations should be preserved.", "less.format.maxPreserveNewLines.desc": "Maximum number of line breaks to be preserved in one chunk, when `#less.format.preserveNewLines#` is enabled.", @@ -101,7 +101,7 @@ "scss.format.enable.desc": "Enable/disable default SCSS formatter.", "scss.format.newlineBetweenSelectors.desc": "Separate selectors with a new line.", "scss.format.newlineBetweenRules.desc": "Separate rulesets by a blank line.", - "scss.format.spaceAroundSelectorSeparator.desc": "Ensure a space character around selector separators '>', '+', '~' (e.g. `a > b`).", + "scss.format.spaceAroundSelectorSeparator.desc": "Ensure a space character around selector separators `>`, `+`, `~` (e.g. `a > b`).", "scss.format.braceStyle.desc": "Put braces on the same line as rules (`collapse`) or put braces on own line (`expand`).", "scss.format.preserveNewLines.desc": "Whether existing line breaks before rules and declarations should be preserved.", "scss.format.maxPreserveNewLines.desc": "Maximum number of line breaks to be preserved in one chunk, when `#scss.format.preserveNewLines#` is enabled.", diff --git a/extensions/emmet/package.nls.json b/extensions/emmet/package.nls.json index 2a58c39641d..683bcc7f307 100644 --- a/extensions/emmet/package.nls.json +++ b/extensions/emmet/package.nls.json @@ -50,10 +50,10 @@ "emmetPreferencesFormatNoIndentTags": "An array of tag names that should never get inner indentation.", "emmetPreferencesFormatForceIndentTags": "An array of tag names that should always get inner indentation.", "emmetPreferencesAllowCompactBoolean": "If `true`, compact notation of boolean attributes are produced.", - "emmetPreferencesCssWebkitProperties": "Comma separated CSS properties that get the 'webkit' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'webkit' prefix.", - "emmetPreferencesCssMozProperties": "Comma separated CSS properties that get the 'moz' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'moz' prefix.", - "emmetPreferencesCssOProperties": "Comma separated CSS properties that get the 'o' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'o' prefix.", - "emmetPreferencesCssMsProperties": "Comma separated CSS properties that get the 'ms' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'ms' prefix.", + "emmetPreferencesCssWebkitProperties": "Comma separated CSS properties that get the `webkit` vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the `webkit` prefix.", + "emmetPreferencesCssMozProperties": "Comma separated CSS properties that get the `moz` vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the `moz` prefix.", + "emmetPreferencesCssOProperties": "Comma separated CSS properties that get the `o` vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the `o` prefix.", + "emmetPreferencesCssMsProperties": "Comma separated CSS properties that get the `ms` vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the `ms` prefix.", "emmetPreferencesCssFuzzySearchMinScore": "The minimum score (from 0 to 1) that fuzzy-matched abbreviation should achieve. Lower values may produce many false-positive matches, higher values may reduce possible matches.", "emmetOptimizeStylesheetParsing": "When set to `false`, the whole file is parsed to determine if current position is valid for expanding Emmet abbreviations. When set to `true`, only the content around the current position in CSS/SCSS/Less files is parsed.", "emmetPreferencesOutputInlineBreak": "The number of sibling inline elements needed for line breaks to be placed between those elements. If `0`, inline elements are always expanded onto a single line.", diff --git a/extensions/git/esbuild.mts b/extensions/git/esbuild.mts index 1b397880bc6..52037129935 100644 --- a/extensions/git/esbuild.mts +++ b/extensions/git/esbuild.mts @@ -32,4 +32,7 @@ run({ }, srcDir, outdir: outDir, + additionalOptions: { + external: ['vscode', '@vscode/fs-copyfile'], + }, }, process.argv, copyNonTsFiles); diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index b552ce9fa5b..ceff06a20e8 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -11,8 +11,9 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", + "@vscode/fs-copyfile": "2.0.0", "byline": "^5.0.0", - "file-type": "16.5.4", + "file-type": "21.3.2", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" @@ -28,6 +29,16 @@ "vscode": "^1.5.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@joaomoreno/unique-names-generator": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@joaomoreno/unique-names-generator/-/unique-names-generator-5.2.0.tgz", @@ -161,10 +172,28 @@ "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", "license": "MIT" }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" }, "node_modules/@types/byline": { "version": "4.2.31", @@ -218,6 +247,19 @@ "vscode": "^1.75.0" } }, + "node_modules/@vscode/fs-copyfile": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vscode/fs-copyfile/-/fs-copyfile-2.0.0.tgz", + "integrity": "sha512-ARb4+9rN905WjJtQ2mSBG/q4pjJkSRun/MkfCeRkk7h/5J8w4vd18NCePFJ/ZucIwXx/7mr9T6nz9Vtt1tk7hg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">=22.6.0" + } + }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -226,17 +268,36 @@ "node": ">=0.10.0" } }, - "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" + "ms": "^2.1.3" }, "engines": { - "node": ">=10" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -259,12 +320,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + ], + "license": "BSD-3-Clause" }, "node_modules/isexe": { "version": "3.1.1", @@ -274,17 +331,17 @@ "node": ">=16" } }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" }, "node_modules/picomatch": { "version": "2.3.1", @@ -297,71 +354,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "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==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" + "@tokenizer/token": "^0.3.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "type": "github", @@ -369,21 +371,35 @@ } }, "node_modules/token-types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.0.tgz", - "integrity": "sha512-P0rrp4wUpefLncNamWIef62J0v0kQR/GfDVji9WKY7GDCWy5YbVSrKUTam07iWPZQGy0zWNOfstYTykMmPNR7w==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", "dependencies": { + "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -391,11 +407,6 @@ "dev": true, "license": "MIT" }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, "node_modules/vscode-uri": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.0.0.tgz", diff --git a/extensions/git/package.json b/extensions/git/package.json index 1fbac49569f..723d36fe7ba 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -38,6 +38,7 @@ "scmTextDocument", "scmValidation", "statusBarItemTooltip", + "taskRunOptions", "tabInputMultiDiff", "tabInputTextMerge", "textEditorDiffInformation", @@ -4346,8 +4347,9 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", + "@vscode/fs-copyfile": "2.0.0", "byline": "^5.0.0", - "file-type": "16.5.4", + "file-type": "21.3.2", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index b97337596d1..d003348045f 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -212,6 +212,10 @@ export class ApiRepository implements Repository { return this.#repository.diffBetweenWithStats(ref1, ref2, path); } + diffBetweenWithStats2(ref: string, path?: string): Promise { + return this.#repository.diffBetweenWithStats2(ref, path); + } + hashObject(data: string): Promise { return this.#repository.hashObject(data); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 8a258ba5741..95da0f84c74 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -278,6 +278,7 @@ export interface Repository { diffBetween(ref1: string, ref2: string, path: string): Promise; diffBetweenPatch(ref1: string, ref2: string, path?: string): Promise; diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; + diffBetweenWithStats2(ref: string, path?: string): Promise; hashObject(data: string): Promise; diff --git a/extensions/git/src/diagnostics.ts b/extensions/git/src/diagnostics.ts index a8c1a3deea3..64bf11076fe 100644 --- a/extensions/git/src/diagnostics.ts +++ b/extensions/git/src/diagnostics.ts @@ -85,7 +85,11 @@ export class GitCommitInputBoxDiagnosticsManager { const threshold = index === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength; if (line.text.length > threshold) { - const diagnostic = new Diagnostic(line.range, l10n.t('{0} characters over {1} in current line', line.text.length - threshold, threshold), this.severity); + const charactersOver = line.text.length - threshold; + const lineLengthMessage = charactersOver === 1 + ? l10n.t('{0} character over {1} in current line', charactersOver, threshold) + : l10n.t('{0} characters over {1} in current line', charactersOver, threshold); + const diagnostic = new Diagnostic(line.range, lineLengthMessage, this.severity); diagnostic.code = DiagnosticCodes.line_length; diagnostics.push(diagnostic); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 90284866a51..14e61bc3f8b 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -10,7 +10,7 @@ import * as cp from 'child_process'; import { fileURLToPath } from 'url'; import which from 'which'; import { EventEmitter } from 'events'; -import * as filetype from 'file-type'; +import { fileTypeFromBuffer } from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePathWithNoFallback, Mutable } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; import type { Commit as ApiCommit, Ref, Branch, Remote, LogOptions, Change, CommitOptions, RefQuery as ApiRefQuery, InitOptions, DiffChange, Worktree as ApiWorktree } from './api/git'; @@ -1691,7 +1691,7 @@ export class Repository { } if (!isText) { - const result = await filetype.fromBuffer(buffer); + const result = await fileTypeFromBuffer(buffer); if (!result) { return { mimetype: 'application/octet-stream' }; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index bd6b6a5c7ff..657ecaa5534 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { cp } from '@vscode/fs-copyfile'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; import picomatch from 'picomatch'; -import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, CustomExecution, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProcessExecution, ProgressLocation, ProgressOptions, RelativePattern, scm, ShellExecution, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, Task, TaskPanelKind, TaskRevealKind, TaskRunOn, tasks, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; import type { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, LogOptions, Ref, Remote, RepositoryKind } from './api/git'; @@ -464,9 +465,9 @@ class DotGitWatcher implements IFileWatcher { const rootWatcher = watch(repository.dotGit.path); this.disposables.push(rootWatcher); - // Ignore changes to the "index.lock" file, and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files. + // Ignore changes to the "index.lock" file (including worktree index.lock files), and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files. // Watchman creates a cookie file inside the git directory whenever a query is run (https://facebook.github.io/watchman/docs/cookies.html). - const filteredRootWatcher = filterEvent(rootWatcher.event, uri => uri.scheme === 'file' && !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path)); + const filteredRootWatcher = filterEvent(rootWatcher.event, uri => uri.scheme === 'file' && !/\/\.git(\/index\.lock|\/worktrees\/[^/]+\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path)); this.event = anyEvent(filteredRootWatcher, this.emitter.event); repository.onDidRunGitStatus(this.updateTransientWatchers, this, this.disposables); @@ -931,7 +932,7 @@ export class Repository implements Disposable { // FS changes should trigger `git status`: // - any change inside the repository working tree - // - any change whithin the first level of the `.git` folder, except the folder itself and `index.lock` + // - any change within the first level of the `.git` folder, except the folder itself and `index.lock` (repository and worktree) const onFileChange = anyEvent(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange); onFileChange(this.onFileChange, this, this.disposables); @@ -940,12 +941,17 @@ export class Repository implements Disposable { this.disposables.push(new FileEventLogger(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange, logger)); - // Parent source control - const parentRoot = repository.kind === 'submodule' - ? repository.dotGit.superProjectPath - : repository.kind === 'worktree' && repository.dotGit.commonPath - ? path.dirname(repository.dotGit.commonPath) - : undefined; + // Parent source control. Repositories opened in the Sessions app + // don't use the parent/child relationship and it is expected for + // a worktree repository to be opened while the main repository + // is closed. + const parentRoot = workspace.isAgentSessionsWorkspace + ? undefined + : repository.kind === 'submodule' + ? repository.dotGit.superProjectPath + : repository.kind === 'worktree' && repository.dotGit.commonPath + ? path.dirname(repository.dotGit.commonPath) + : undefined; const parent = parentRoot ? this.repositoryResolver.getRepository(parentRoot)?.sourceControl : undefined; @@ -1225,6 +1231,14 @@ export class Repository implements Disposable { this.repository.diffBetweenWithStats(`${ref1}...${ref2}`, { path, similarityThreshold })); } + diffBetweenWithStats2(ref: string, path?: string): Promise { + const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root)); + const similarityThreshold = scopedConfig.get('similarityThreshold', 50); + + return this.run(Operation.Diff, () => + this.repository.diffBetweenWithStats(ref, { path, similarityThreshold })); + } + diffTrees(treeish1: string, treeish2?: string): Promise { const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root)); const similarityThreshold = scopedConfig.get('similarityThreshold', 50); @@ -1892,15 +1906,41 @@ export class Repository implements Disposable { this.globalState.update(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${this.root}`, newWorktreeRoot); } - // Copy worktree include files. We explicitly do not await this - // since we don't want to block the worktree creation on the - // copy operation. - this._copyWorktreeIncludeFiles(worktreePath!); + this._setupWorktree(worktreePath!); return worktreePath!; }); } + private async _setupWorktree(worktreePath: string): Promise { + // Copy worktree include files and wait for the copy to complete + // before running any worktree-created tasks. + await this._copyWorktreeIncludeFiles(worktreePath); + + await this._runWorktreeCreatedTasks(worktreePath); + } + + private async _runWorktreeCreatedTasks(worktreePath: string): Promise { + try { + const allTasks = await tasks.fetchTasks(); + const worktreeTasks = allTasks.filter(task => task.runOptions.runOn === TaskRunOn.WorktreeCreated); + + for (const task of worktreeTasks) { + const worktreeTask = retargetTaskToWorktree(task, worktreePath); + if (!worktreeTask) { + this.logger.warn(`[Repository][_runWorktreeCreatedTasks] Skipped task '${task.name}' because it could not be retargeted to worktree '${worktreePath}'.`); + continue; + } + + tasks.executeTask(worktreeTask).then(undefined, err => { + this.logger.warn(`[Repository][_runWorktreeCreatedTasks] Failed to execute worktree-created task '${task.name}' for '${worktreePath}': ${err}`); + }); + } + } catch (err) { + this.logger.warn(`[Repository][_runWorktreeCreatedTasks] Failed to execute worktree-created tasks for '${worktreePath}': ${err}`); + } + } + private async _getWorktreeIncludePaths(): Promise> { const config = workspace.getConfiguration('git', Uri.file(this.root)); const worktreeIncludeFiles = config.get('worktreeIncludeFiles', []); @@ -1930,59 +1970,76 @@ export class Repository implements Disposable { gitIgnoredFiles.delete(uri.fsPath); } - // Add the folder paths for git ignored files + // Compute the base directory for each glob pattern (the fixed + // prefix before any wildcard characters). This will be used to + // optimize the upward traversal when adding parent directories. + const filePatternBases = new Set(); + for (const pattern of worktreeIncludeFiles) { + const segments = pattern.split(/[\/\\]/); + const fixedSegments: string[] = []; + for (const seg of segments) { + if (/[*?{}[\]]/.test(seg)) { + break; + } + fixedSegments.push(seg); + } + filePatternBases.add(path.join(this.root, ...fixedSegments)); + } + + // Add the folder paths for git ignored files, walking + // up only to the nearest file pattern base directory. const gitIgnoredPaths = new Set(gitIgnoredFiles); for (const filePath of gitIgnoredFiles) { let dir = path.dirname(filePath); - while (dir !== this.root && !gitIgnoredFiles.has(dir)) { + while (dir !== this.root && !gitIgnoredPaths.has(dir)) { gitIgnoredPaths.add(dir); + if (filePatternBases.has(dir)) { + break; + } dir = path.dirname(dir); } } - return gitIgnoredPaths; + // Find minimal set of paths (folders and files) to copy. Keep only topmost + // paths — if a directory is already in the set, all its descendants are + // implicitly included and don't need separate entries. + let lastTopmost: string | undefined; + const pathsToCopy = new Set(); + for (const p of Array.from(gitIgnoredPaths).sort()) { + if (lastTopmost && (p === lastTopmost || p.startsWith(lastTopmost + path.sep))) { + continue; + } + pathsToCopy.add(p); + lastTopmost = p; + } + + return pathsToCopy; } private async _copyWorktreeIncludeFiles(worktreePath: string): Promise { - const gitIgnoredPaths = await this._getWorktreeIncludePaths(); - if (gitIgnoredPaths.size === 0) { + const worktreeIncludePaths = await this._getWorktreeIncludePaths(); + if (worktreeIncludePaths.size === 0) { return; } try { - // Find minimal set of paths (folders and files) to copy. - // The goal is to reduce the number of copy operations - // needed. - const pathsToCopy = new Set(); - for (const filePath of gitIgnoredPaths) { - const relativePath = path.relative(this.root, filePath); - const firstSegment = relativePath.split(path.sep)[0]; - pathsToCopy.add(path.join(this.root, firstSegment)); - } - - const startTime = Date.now(); + const startTime = performance.now(); const limiter = new Limiter(15); - const files = Array.from(pathsToCopy); + const files = Array.from(worktreeIncludePaths); // Copy files - const results = await Promise.allSettled(files.map(sourceFile => - limiter.queue(async () => { + const results = await Promise.allSettled(files.map(sourceFile => { + return limiter.queue(async () => { const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); - await fsPromises.cp(sourceFile, targetFile, { - filter: src => gitIgnoredPaths.has(src), - force: true, - mode: fs.constants.COPYFILE_FICLONE, - recursive: true, - verbatimSymlinks: true - }); - }) - )); + await cp(sourceFile, targetFile, { force: true, recursive: true, verbatimSymlinks: true }); + }); + })); // Log any failed operations const failedOperations = results.filter(r => r.status === 'rejected'); - this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${Date.now() - startTime}ms]`); + this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${(performance.now() - startTime).toFixed(2)}ms]`); if (failedOperations.length > 0) { window.showWarningMessage(l10n.t('Failed to copy {0} folder(s)/file(s) to the worktree.', failedOperations.length)); @@ -3349,3 +3406,56 @@ export class Repository implements Disposable { this.disposables = dispose(this.disposables); } } + +function retargetTaskToWorktree(task: Task, worktreePath: string): Task | undefined { + const execution = retargetTaskExecution(task.execution, worktreePath); + if (!execution) { + return undefined; + } + + const worktreeFolder: WorkspaceFolder = { + uri: Uri.file(worktreePath), + name: path.basename(worktreePath), + index: workspace.workspaceFolders?.length ?? 0 + }; + + const worktreeTask = new Task({ ...task.definition }, worktreeFolder, task.name, task.source, execution, task.problemMatchers); + worktreeTask.detail = task.detail; + worktreeTask.group = task.group; + worktreeTask.isBackground = task.isBackground; + worktreeTask.presentationOptions = { ...task.presentationOptions, reveal: TaskRevealKind.Never, panel: TaskPanelKind.New }; + worktreeTask.runOptions = { ...task.runOptions }; + + return worktreeTask; +} + +function retargetTaskExecution(execution: ProcessExecution | ShellExecution | CustomExecution | undefined, worktreePath: string): ProcessExecution | ShellExecution | CustomExecution | undefined { + if (!execution) { + return undefined; + } + + if (execution instanceof ProcessExecution) { + return new ProcessExecution(execution.process, execution.args, { + ...execution.options, + cwd: worktreePath + }); + } + + if (execution instanceof ShellExecution) { + if (execution.commandLine !== undefined) { + return new ShellExecution(execution.commandLine, { + ...execution.options, + cwd: worktreePath + }); + } + + if (execution.command !== undefined) { + return new ShellExecution(execution.command, execution.args ?? [], { + ...execution.options, + cwd: worktreePath + }); + } + } + + return execution; +} diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 2a7ad5259ac..a34d12aaa48 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -27,6 +27,7 @@ "../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts", "../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts", "../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts", + "../../src/vscode-dts/vscode.proposed.taskRunOptions.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", diff --git a/extensions/github/esbuild.mts b/extensions/github/esbuild.mts index f91916e622d..51494c329a5 100644 --- a/extensions/github/esbuild.mts +++ b/extensions/github/esbuild.mts @@ -16,4 +16,10 @@ run({ }, srcDir, outdir: outDir, + additionalOptions: { + banner: { + // The tunnel package uses `require` and needs this shim to work in an ESM environment + js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`, + }, + }, }, process.argv); diff --git a/extensions/grunt/src/main.ts b/extensions/grunt/src/main.ts index b94b00c4462..9c2fee82b50 100644 --- a/extensions/grunt/src/main.ts +++ b/extensions/grunt/src/main.ts @@ -18,9 +18,9 @@ function exists(file: string): Promise { }); } -function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: string; stderr: string }> { +function exec(command: string, args: string[], options: cp.ExecFileOptions): Promise<{ stdout: string; stderr: string }> { return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { - cp.exec(command, options, (error, stdout, stderr) => { + cp.execFile(command, args, options, (error, stdout, stderr) => { if (error) { reject({ error, stdout, stderr }); } @@ -143,9 +143,9 @@ class FolderDetector { return emptyTasks; } - const commandLine = `${await this._gruntCommand} --help --no-color`; + const gruntCommand = await this._gruntCommand; try { - const { stdout, stderr } = await exec(commandLine, { cwd: rootPath }); + const { stdout, stderr } = await exec(gruntCommand, ['--help', '--no-color'], { cwd: rootPath }); if (stderr) { getOutputChannel().appendLine(stderr); showError(); diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index fe0a23cb591..1368b6cda47 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -59,17 +59,17 @@ "url": { "type": "string", "default": "/user.schema.json", - "description": "%json.schemas.url.desc%" + "markdownDescription": "%json.schemas.url.desc%" }, "fileMatch": { "type": "array", "items": { "type": "string", "default": "MyFile.json", - "description": "%json.schemas.fileMatch.item.desc%" + "markdownDescription": "%json.schemas.fileMatch.item.desc%" }, "minItems": 1, - "description": "%json.schemas.fileMatch.desc%" + "markdownDescription": "%json.schemas.fileMatch.desc%" }, "schema": { "$ref": "http://json-schema.org/draft-07/schema#", @@ -141,7 +141,7 @@ "additionalProperties": { "type": "boolean" }, - "description": "%json.schemaDownload.trustedDomains.desc%", + "markdownDescription": "%json.schemaDownload.trustedDomains.desc%", "tags": [ "usesOnlineServices" ] diff --git a/extensions/json-language-features/package.nls.json b/extensions/json-language-features/package.nls.json index 9052d3781c9..30199b2bb33 100644 --- a/extensions/json-language-features/package.nls.json +++ b/extensions/json-language-features/package.nls.json @@ -2,12 +2,12 @@ "displayName": "JSON Language Features", "description": "Provides rich language support for JSON files.", "json.schemas.desc": "Associate schemas to JSON files in the current project.", - "json.schemas.url.desc": "A URL or absolute file path to a schema. Can be a relative path (starting with './') in workspace and workspace folder settings.", - "json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas. `*` and '**' can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.", - "json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' and '**' to match against when resolving JSON files to schemas. When beginning with '!', it defines an exclusion pattern.", + "json.schemas.url.desc": "A URL or absolute file path to a schema. Can be a relative path (starting with `./`) in workspace and workspace folder settings.", + "json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas. `*` and `**` can be used as a wildcard. Exclusion patterns can also be defined and start with `!`. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.", + "json.schemas.fileMatch.item.desc": "A file pattern that can contain `*` and `**` to match against when resolving JSON files to schemas. When beginning with `!`, it defines an exclusion pattern.", "json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.", "json.format.enable.desc": "Enable/disable default JSON formatter", - "json.format.keepLines.desc" : "Keep all existing new lines when formatting.", + "json.format.keepLines.desc": "Keep all existing new lines when formatting.", "json.validate.enable.desc": "Enable/disable JSON validation.", "json.tracing.desc": "Traces the communication between VS Code and the JSON language server.", "json.colorDecorators.enable.desc": "Enables or disables color decorators", @@ -20,5 +20,5 @@ "json.command.clearCache": "Clear Schema Cache", "json.command.sort": "Sort Document", "json.workspaceTrust": "The extension requires workspace trust to load schemas from http and https.", - "json.schemaDownload.trustedDomains.desc": "List of trusted domains for downloading JSON schemas over http(s). Use '*' to trust all domains. '*' can also be used as a wildcard in domain names." + "json.schemaDownload.trustedDomains.desc": "List of trusted domains for downloading JSON schemas over http(s). Use `*` to trust all domains. `*` can also be used as a wildcard in domain names." } diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index 811cbcd2e91..edfb856aacc 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -11,7 +11,7 @@ import { import { runSafe, runSafeAsync } from './utils/runner'; import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; -import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position, SortOptions } from 'vscode-json-languageservice'; +import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position, SortOptions, SeverityLevel } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; import { Utils, URI } from 'vscode-uri'; import * as l10n from '@vscode/l10n'; @@ -216,7 +216,13 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) schemas?: JSONSchemaSettings[]; format?: { enable?: boolean }; keepLines?: { enable?: boolean }; - validate?: { enable?: boolean }; + validate?: { + enable?: boolean; + comments?: SeverityLevel; + trailingCommas?: SeverityLevel; + schemaValidation?: SeverityLevel; + schemaRequest?: SeverityLevel; + }; resultLimit?: number; jsonFoldingLimit?: number; jsoncFoldingLimit?: number; @@ -242,6 +248,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let schemaAssociations: ISchemaAssociations | SchemaConfiguration[] | undefined = undefined; let formatterRegistrations: Thenable[] | null = null; let validateEnabled = true; + let commentsSeverity: SeverityLevel | undefined = undefined; + let trailingCommasSeverity: SeverityLevel | undefined = undefined; + let schemaValidationSeverity: SeverityLevel | undefined = undefined; + let schemaRequestSeverity: SeverityLevel | undefined = undefined; let keepLinesEnabled = false; // The settings have changed. Is sent on server activation as well. @@ -250,6 +260,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) runtime.configureHttpRequests?.(settings?.http?.proxy, !!settings.http?.proxyStrictSSL); jsonConfigurationSettings = settings.json?.schemas; validateEnabled = !!settings.json?.validate?.enable; + commentsSeverity = settings.json?.validate?.comments; + trailingCommasSeverity = settings.json?.validate?.trailingCommas; + schemaValidationSeverity = settings.json?.validate?.schemaValidation; + schemaRequestSeverity = settings.json?.validate?.schemaRequest; keepLinesEnabled = settings.json?.keepLines?.enable || false; updateConfiguration(); @@ -388,7 +402,12 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return []; // ignore empty documents } const jsonDocument = getJSONDocument(textDocument); - const documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; + const documentSettings: DocumentLanguageSettings = { + comments: commentsSeverity ?? (textDocument.languageId === 'jsonc' ? 'ignore' : 'error'), + trailingCommas: trailingCommasSeverity ?? (textDocument.languageId === 'jsonc' ? 'warning' : 'error'), + schemaValidation: schemaValidationSeverity, + schemaRequest: schemaRequestSeverity + }; return await languageService.doValidation(textDocument, jsonDocument, documentSettings); } diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index e8c9413cb07..936d6124cee 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "93454227ce9a7aa92f41b157c6a74f3971b4ae14" + "commitHash": "25cc285b5e8accab4ff7725eeb8594f458b45ce4" } }, "license": "MIT", diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index b222b3ba644..c7a6b4da10f 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/93454227ce9a7aa92f41b157c6a74f3971b4ae14", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/25cc285b5e8accab4ff7725eeb8594f458b45ce4", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", @@ -301,7 +301,7 @@ "keyword": { "patterns": [ { - "match": "\\b(?>>} - */ -const mangleMap = new Map(); - -/** - * @param {string} projectPath - */ -function getMangledFileContents(projectPath) { - let entry = mangleMap.get(projectPath); - if (!entry) { - const log = (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data); - log(`Mangling ${projectPath}`); - const ts2tsMangler = new Mangler(projectPath, log, { mangleExports: true, manglePrivateFields: true }); - entry = ts2tsMangler.computeNewFileContents(); - mangleMap.set(projectPath, entry); - } - - return entry; -} - -/** - * @type {webpack.LoaderDefinitionFunction} - */ -module.exports = async function (source, sourceMap, meta) { - if (this.mode !== 'production') { - // Only enable mangling in production builds - return source; - } - if (true) { - // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 - return source; - } - const options = this.getOptions(); - if (options.disabled) { - // Dynamically disabled - return source; - } - - if (source !== fs.readFileSync(this.resourcePath).toString()) { - // File content has changed by previous webpack steps. - // Skip mangling. - return source; - } - - const callback = this.async(); - - const fileContentsMap = await getMangledFileContents(options.configFile); - - const newContents = fileContentsMap.get(this.resourcePath); - callback(null, newContents?.out ?? source, sourceMap, meta); -}; diff --git a/extensions/markdown-language-features/preview-src/activeLineMarker.ts b/extensions/markdown-language-features/preview-src/activeLineMarker.ts index 75c1ed7cbc9..0e63dad4599 100644 --- a/extensions/markdown-language-features/preview-src/activeLineMarker.ts +++ b/extensions/markdown-language-features/preview-src/activeLineMarker.ts @@ -5,27 +5,27 @@ import { getElementsForSourceLine } from './scroll-sync'; export class ActiveLineMarker { - private _current: any; + #current: HTMLElement | undefined; onDidChangeTextEditorSelection(line: number, documentVersion: number) { const { previous } = getElementsForSourceLine(line, documentVersion); - this._update(previous && (previous.codeElement || previous.element)); + this.#update(previous && (previous.codeElement || previous.element)); } - private _update(before: HTMLElement | undefined) { - this._unmarkActiveElement(this._current); - this._markActiveElement(before); - this._current = before; + #update(before: HTMLElement | undefined) { + this.#unmarkActiveElement(this.#current); + this.#markActiveElement(before); + this.#current = before; } - private _unmarkActiveElement(element: HTMLElement | undefined) { + #unmarkActiveElement(element: HTMLElement | undefined) { if (!element) { return; } element.classList.toggle('code-active-line', false); } - private _markActiveElement(element: HTMLElement | undefined) { + #markActiveElement(element: HTMLElement | undefined) { if (!element) { return; } diff --git a/extensions/markdown-language-features/preview-src/csp.ts b/extensions/markdown-language-features/preview-src/csp.ts index fcc38352da8..4db9d7b116f 100644 --- a/extensions/markdown-language-features/preview-src/csp.ts +++ b/extensions/markdown-language-features/preview-src/csp.ts @@ -11,45 +11,49 @@ import { getStrings } from './strings'; * Shows an alert when there is a content security policy violation. */ export class CspAlerter { - private _didShow = false; - private _didHaveCspWarning = false; + #didShow = false; + #didHaveCspWarning = false; - private _messaging?: MessagePoster; + #messaging?: MessagePoster; + + readonly #settingsManager: SettingsManager; constructor( - private readonly _settingsManager: SettingsManager, + settingsManager: SettingsManager, ) { + this.#settingsManager = settingsManager; + document.addEventListener('securitypolicyviolation', () => { - this._onCspWarning(); + this.#onCspWarning(); }); window.addEventListener('message', (event) => { if (event?.data && event.data.name === 'vscode-did-block-svg') { - this._onCspWarning(); + this.#onCspWarning(); } }); } public setPoster(poster: MessagePoster) { - this._messaging = poster; - if (this._didHaveCspWarning) { - this._showCspWarning(); + this.#messaging = poster; + if (this.#didHaveCspWarning) { + this.#showCspWarning(); } } - private _onCspWarning() { - this._didHaveCspWarning = true; - this._showCspWarning(); + #onCspWarning() { + this.#didHaveCspWarning = true; + this.#showCspWarning(); } - private _showCspWarning() { + #showCspWarning() { const strings = getStrings(); - const settings = this._settingsManager.settings; + const settings = this.#settingsManager.settings; - if (this._didShow || settings.disableSecurityWarnings || !this._messaging) { + if (this.#didShow || settings.disableSecurityWarnings || !this.#messaging) { return; } - this._didShow = true; + this.#didShow = true; const notification = document.createElement('a'); notification.innerText = strings.cspAlertMessageText; @@ -59,7 +63,7 @@ export class CspAlerter { notification.setAttribute('role', 'button'); notification.setAttribute('aria-label', strings.cspAlertMessageLabel); notification.onclick = () => { - this._messaging!.postMessage('showPreviewSecuritySelector', { source: settings.source }); + this.#messaging!.postMessage('showPreviewSecuritySelector', { source: settings.source }); }; document.body.appendChild(notification); } diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index bd5683bf0d5..d481bb24e53 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -27,12 +27,14 @@ const vscode = acquireVsCodeApi(); interface State { scrollProgress?: number; resource?: string; + line?: number; + fragment?: string; } const originalState: State = vscode.getState() ?? {}; -const state = { +const state: State = { ...originalState, - ...getData('data-state') + ...getData>('data-state') }; if (typeof originalState.scrollProgress !== 'undefined' && originalState?.resource !== state.resource) { @@ -342,10 +344,10 @@ document.addEventListener('click', event => { return; } - let node: any = event.target; + let node = event.target as Element | null; while (node) { - if (node.tagName && node.tagName === 'A' && node.href) { - if (node.getAttribute('href').startsWith('#')) { + if (node.tagName && node.tagName === 'A' && (node as HTMLAnchorElement).href) { + if (node.getAttribute('href')?.startsWith('#')) { return; } @@ -353,13 +355,13 @@ document.addEventListener('click', event => { if (!hrefText) { hrefText = node.getAttribute('href'); // Pass through known schemes - if (passThroughLinkSchemes.some(scheme => hrefText.startsWith(scheme))) { + if (hrefText && passThroughLinkSchemes.some(scheme => hrefText!.startsWith(scheme))) { return; } } // If original link doesn't look like a url, delegate back to VS Code to resolve - if (!/^[a-z\-]+:/i.test(hrefText)) { + if (hrefText && !/^[a-z\-]+:/i.test(hrefText)) { messaging.postMessage('openLink', { href: hrefText }); event.preventDefault(); event.stopPropagation(); @@ -368,7 +370,7 @@ document.addEventListener('click', event => { return; } - node = node.parentNode; + node = node.parentElement; } }, true); diff --git a/extensions/markdown-language-features/preview-src/loading.ts b/extensions/markdown-language-features/preview-src/loading.ts index c6e6d27acd9..f3270b22214 100644 --- a/extensions/markdown-language-features/preview-src/loading.ts +++ b/extensions/markdown-language-features/preview-src/loading.ts @@ -5,15 +5,20 @@ import { MessagePoster } from './messaging'; export class StyleLoadingMonitor { - private readonly _unloadedStyles: string[] = []; - private _finishedLoading: boolean = false; + readonly #unloadedStyles: string[] = []; + #finishedLoading: boolean = false; - private _poster?: MessagePoster; + #poster?: MessagePoster; constructor() { - const onStyleLoadError = (event: any) => { - const source = event.target.dataset.source; - this._unloadedStyles.push(source); + const onStyleLoadError = (event: Event | string) => { + if (!(event instanceof Event)) { + return; + } + const source = (event.target as HTMLElement | null)?.dataset.source; + if (source) { + this.#unloadedStyles.push(source); + } }; window.addEventListener('DOMContentLoaded', () => { @@ -25,18 +30,18 @@ export class StyleLoadingMonitor { }); window.addEventListener('load', () => { - if (!this._unloadedStyles.length) { + if (!this.#unloadedStyles.length) { return; } - this._finishedLoading = true; - this._poster?.postMessage('previewStyleLoadError', { unloadedStyles: this._unloadedStyles }); + this.#finishedLoading = true; + this.#poster?.postMessage('previewStyleLoadError', { unloadedStyles: this.#unloadedStyles }); }); } public setPoster(poster: MessagePoster): void { - this._poster = poster; - if (this._finishedLoading) { - poster.postMessage('previewStyleLoadError', { unloadedStyles: this._unloadedStyles }); + this.#poster = poster; + if (this.#finishedLoading) { + poster.postMessage('previewStyleLoadError', { unloadedStyles: this.#unloadedStyles }); } } } diff --git a/extensions/markdown-language-features/preview-src/messaging.ts b/extensions/markdown-language-features/preview-src/messaging.ts index 1fb29f0b55b..0458cac997c 100644 --- a/extensions/markdown-language-features/preview-src/messaging.ts +++ b/extensions/markdown-language-features/preview-src/messaging.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SettingsManager } from './settings'; +import type { WebviewApi } from 'vscode-webview'; import type { FromWebviewMessage } from '../types/previewMessaging'; +import { SettingsManager } from './settings'; export interface MessagePoster { /** @@ -16,7 +17,7 @@ export interface MessagePoster { ): void; } -export const createPosterForVsCode = (vscode: any, settingsManager: SettingsManager): MessagePoster => { +export const createPosterForVsCode = (vscode: WebviewApi, settingsManager: SettingsManager): MessagePoster => { return { postMessage( type: T['type'], diff --git a/extensions/markdown-language-features/preview-src/scroll-sync.ts b/extensions/markdown-language-features/preview-src/scroll-sync.ts index 4f5e3f7d2fe..f4eae58d47c 100644 --- a/extensions/markdown-language-features/preview-src/scroll-sync.ts +++ b/extensions/markdown-language-features/preview-src/scroll-sync.ts @@ -9,7 +9,7 @@ const codeLineClass = 'code-line'; export class CodeLineElement { - private readonly _detailParentElements: readonly HTMLDetailsElement[]; + readonly #detailParentElements: readonly HTMLDetailsElement[]; constructor( readonly element: HTMLElement, @@ -17,11 +17,11 @@ export class CodeLineElement { readonly codeElement?: HTMLElement, readonly endLine?: number, ) { - this._detailParentElements = Array.from(getParentsWithTagName(element, 'DETAILS')); + this.#detailParentElements = Array.from(getParentsWithTagName(element, 'DETAILS')); } get isVisible(): boolean { - if (this._detailParentElements.some(x => !x.open)) { + if (this.#detailParentElements.some(x => !x.open)) { return false; } diff --git a/extensions/markdown-language-features/preview-src/settings.ts b/extensions/markdown-language-features/preview-src/settings.ts index 0fb5d0c2686..6d642b58c64 100644 --- a/extensions/markdown-language-features/preview-src/settings.ts +++ b/extensions/markdown-language-features/preview-src/settings.ts @@ -33,13 +33,13 @@ export function getData(key: string): T { } export class SettingsManager { - private _settings: PreviewSettings = getData('data-settings'); + #settings: PreviewSettings = getData('data-settings'); public get settings(): PreviewSettings { - return this._settings; + return this.#settings; } public updateSettings(newSettings: PreviewSettings) { - this._settings = newSettings; + this.#settings = newSettings; } } diff --git a/extensions/markdown-language-features/src/client/client.ts b/extensions/markdown-language-features/src/client/client.ts index bf7be3f3206..dc279f02d84 100644 --- a/extensions/markdown-language-features/src/client/client.ts +++ b/extensions/markdown-language-features/src/client/client.ts @@ -17,37 +17,43 @@ export type LanguageClientConstructor = (name: string, description: string, clie export class MdLanguageClient implements IDisposable { + readonly #client: BaseLanguageClient; + readonly #workspace: VsCodeMdWorkspace; + constructor( - private readonly _client: BaseLanguageClient, - private readonly _workspace: VsCodeMdWorkspace, - ) { } + client: BaseLanguageClient, + workspace: VsCodeMdWorkspace, + ) { + this.#client = client; + this.#workspace = workspace; + } dispose(): void { - this._client.stop(); - this._workspace.dispose(); + this.#client.stop(); + this.#workspace.dispose(); } resolveLinkTarget(linkText: string, uri: vscode.Uri): Promise { - return this._client.sendRequest(proto.resolveLinkTarget, { linkText, uri: uri.toString() }); + return this.#client.sendRequest(proto.resolveLinkTarget, { linkText, uri: uri.toString() }); } getEditForFileRenames(files: ReadonlyArray<{ oldUri: string; newUri: string }>, token: vscode.CancellationToken) { - return this._client.sendRequest(proto.getEditForFileRenames, files, token); + return this.#client.sendRequest(proto.getEditForFileRenames, files, token); } getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken) { - return this._client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token); + return this.#client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token); } prepareUpdatePastedLinks(doc: vscode.Uri, ranges: readonly vscode.Range[], token: vscode.CancellationToken) { - return this._client.sendRequest(proto.prepareUpdatePastedLinks, { + return this.#client.sendRequest(proto.prepareUpdatePastedLinks, { uri: doc.toString(), ranges: ranges.map(range => Range.create(range.start.line, range.start.character, range.end.line, range.end.character)), }, token); } getUpdatePastedLinksEdit(pastingIntoDoc: vscode.Uri, edits: readonly vscode.TextEdit[], metadata: string, token: vscode.CancellationToken) { - return this._client.sendRequest(proto.getUpdatePastedLinksEdit, { + return this.#client.sendRequest(proto.getUpdatePastedLinksEdit, { metadata, pasteIntoDoc: pastingIntoDoc.toString(), edits: edits.map(edit => TextEdit.replace(edit.range, edit.newText)), diff --git a/extensions/markdown-language-features/src/client/fileWatchingManager.ts b/extensions/markdown-language-features/src/client/fileWatchingManager.ts index e2010edda8a..c617a73634d 100644 --- a/extensions/markdown-language-features/src/client/fileWatchingManager.ts +++ b/extensions/markdown-language-features/src/client/fileWatchingManager.ts @@ -17,12 +17,12 @@ type DirWatcherEntry = { export class FileWatcherManager { - private readonly _fileWatchers = new Map(); - private readonly _dirWatchers = new ResourceMap<{ + readonly #dirWatchers = new ResourceMap<{ readonly watcher: vscode.FileSystemWatcher; refCount: number; }>(); @@ -35,7 +35,7 @@ export class FileWatcherManager { const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'), !listeners.create, !listeners.change, !listeners.delete); const parentDirWatchers: DirWatcherEntry[] = []; - this._fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers }); + this.#fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers }); if (listeners.create) { watcher.onDidCreate(listeners.create); } if (listeners.change) { watcher.onDidChange(listeners.change); } @@ -46,12 +46,12 @@ export class FileWatcherManager { for (let dirUri = Utils.dirname(uri); dirUri.path.length > 1; dirUri = Utils.dirname(dirUri)) { const disposables: IDisposable[] = []; - let parentDirWatcher = this._dirWatchers.get(dirUri); + let parentDirWatcher = this.#dirWatchers.get(dirUri); if (!parentDirWatcher) { const glob = new vscode.RelativePattern(Utils.dirname(dirUri), Utils.basename(dirUri)); const parentWatcher = vscode.workspace.createFileSystemWatcher(glob, !listeners.create, true, !listeners.delete); parentDirWatcher = { refCount: 0, watcher: parentWatcher }; - this._dirWatchers.set(dirUri, parentDirWatcher); + this.#dirWatchers.set(dirUri, parentDirWatcher); } parentDirWatcher.refCount++; @@ -81,16 +81,16 @@ export class FileWatcherManager { } delete(id: number): void { - const entry = this._fileWatchers.get(id); + const entry = this.#fileWatchers.get(id); if (entry) { for (const dirWatcher of entry.dirWatchers) { disposeAll(dirWatcher.disposables); - const dirWatcherEntry = this._dirWatchers.get(dirWatcher.uri); + const dirWatcherEntry = this.#dirWatchers.get(dirWatcher.uri); if (dirWatcherEntry) { if (--dirWatcherEntry.refCount <= 0) { dirWatcherEntry.watcher.dispose(); - this._dirWatchers.delete(dirWatcher.uri); + this.#dirWatchers.delete(dirWatcher.uri); } } } @@ -98,6 +98,6 @@ export class FileWatcherManager { entry.watcher.dispose(); } - this._fileWatchers.delete(id); + this.#fileWatchers.delete(id); } } diff --git a/extensions/markdown-language-features/src/client/inMemoryDocument.ts b/extensions/markdown-language-features/src/client/inMemoryDocument.ts index 953f0da7c89..2726adb6de1 100644 --- a/extensions/markdown-language-features/src/client/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/client/inMemoryDocument.ts @@ -9,7 +9,7 @@ import { ITextDocument } from '../types/textDocument'; export class InMemoryDocument implements ITextDocument { - private readonly _doc: TextDocument; + readonly #doc: TextDocument; public readonly uri: vscode.Uri; public readonly version: number; @@ -21,15 +21,15 @@ export class InMemoryDocument implements ITextDocument { ) { this.uri = uri; this.version = version; - this._doc = TextDocument.create(this.uri.toString(), 'markdown', 0, contents); + this.#doc = TextDocument.create(this.uri.toString(), 'markdown', 0, contents); } getText(range?: vscode.Range): string { - return this._doc.getText(range); + return this.#doc.getText(range); } positionAt(offset: number): vscode.Position { - const pos = this._doc.positionAt(offset); + const pos = this.#doc.positionAt(offset); return new vscode.Position(pos.line, pos.character); } } diff --git a/extensions/markdown-language-features/src/client/workspace.ts b/extensions/markdown-language-features/src/client/workspace.ts index 9ea3173c9cc..b07a93ade78 100644 --- a/extensions/markdown-language-features/src/client/workspace.ts +++ b/extensions/markdown-language-features/src/client/workspace.ts @@ -17,47 +17,47 @@ import { ResourceMap } from '../util/resourceMap'; */ export class VsCodeMdWorkspace extends Disposable { - private readonly _watcher: vscode.FileSystemWatcher | undefined; + readonly #watcher: vscode.FileSystemWatcher | undefined; - private readonly _documentCache = new ResourceMap(); + readonly #documentCache = new ResourceMap(); - private readonly _utf8Decoder = new TextDecoder('utf-8'); + readonly #utf8Decoder = new TextDecoder('utf-8'); constructor() { super(); - this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md')); + this.#watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md')); - this._register(this._watcher.onDidChange(async resource => { - this._documentCache.delete(resource); + this._register(this.#watcher.onDidChange(async resource => { + this.#documentCache.delete(resource); })); - this._register(this._watcher.onDidDelete(resource => { - this._documentCache.delete(resource); + this._register(this.#watcher.onDidDelete(resource => { + this.#documentCache.delete(resource); })); this._register(vscode.workspace.onDidOpenTextDocument(e => { - this._documentCache.delete(e.uri); + this.#documentCache.delete(e.uri); })); this._register(vscode.workspace.onDidCloseTextDocument(e => { - this._documentCache.delete(e.uri); + this.#documentCache.delete(e.uri); })); } - private _isRelevantMarkdownDocument(doc: vscode.TextDocument) { + #isRelevantMarkdownDocument(doc: vscode.TextDocument) { return isMarkdownFile(doc) && doc.uri.scheme !== 'vscode-bulkeditpreview'; } public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise { - const existing = this._documentCache.get(resource); + const existing = this.#documentCache.get(resource); if (existing) { return existing; } - const matchingDocument = vscode.workspace.textDocuments.find((doc) => this._isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString()); + const matchingDocument = vscode.workspace.textDocuments.find((doc) => this.#isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString()); if (matchingDocument) { - this._documentCache.set(resource, matchingDocument); + this.#documentCache.set(resource, matchingDocument); return matchingDocument; } @@ -69,9 +69,9 @@ export class VsCodeMdWorkspace extends Disposable { const bytes = await vscode.workspace.fs.readFile(resource); // We assume that markdown is in UTF-8 - const text = this._utf8Decoder.decode(bytes); + const text = this.#utf8Decoder.decode(bytes); const doc = new InMemoryDocument(resource, text, 0); - this._documentCache.set(resource, doc); + this.#documentCache.set(resource, doc); return doc; } catch { return undefined; diff --git a/extensions/markdown-language-features/src/commandManager.ts b/extensions/markdown-language-features/src/commandManager.ts index ae2c4985066..0563db020c7 100644 --- a/extensions/markdown-language-features/src/commandManager.ts +++ b/extensions/markdown-language-features/src/commandManager.ts @@ -12,27 +12,27 @@ export interface Command { } export class CommandManager { - private readonly _commands = new Map(); + readonly #commands = new Map(); public dispose() { - for (const registration of this._commands.values()) { + for (const registration of this.#commands.values()) { registration.dispose(); } - this._commands.clear(); + this.#commands.clear(); } public register(command: T): vscode.Disposable { - this._registerCommand(command.id, command.execute, command); + this.#registerCommand(command.id, command.execute, command); return new vscode.Disposable(() => { - this._commands.delete(command.id); + this.#commands.delete(command.id); }); } - private _registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { - if (this._commands.has(id)) { + #registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { + if (this.#commands.has(id)) { return; } - this._commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); + this.#commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); } } diff --git a/extensions/markdown-language-features/src/commands/copyImage.ts b/extensions/markdown-language-features/src/commands/copyImage.ts index 86fd349c730..09a683ecfb2 100644 --- a/extensions/markdown-language-features/src/commands/copyImage.ts +++ b/extensions/markdown-language-features/src/commands/copyImage.ts @@ -10,12 +10,16 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class CopyImageCommand implements Command { public readonly id = '_markdown.copyImage'; + readonly #webviewManager: MarkdownPreviewManager; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - ) { } + webviewManager: MarkdownPreviewManager, + ) { + this.#webviewManager = webviewManager; + } public execute(args: { id: string; resource: string }) { const source = vscode.Uri.parse(args.resource); - this._webviewManager.findPreview(source)?.copyImage(args.id); + this.#webviewManager.findPreview(source)?.copyImage(args.id); } } diff --git a/extensions/markdown-language-features/src/commands/openImage.ts b/extensions/markdown-language-features/src/commands/openImage.ts index 64b1831df0d..4a121fd6989 100644 --- a/extensions/markdown-language-features/src/commands/openImage.ts +++ b/extensions/markdown-language-features/src/commands/openImage.ts @@ -10,12 +10,16 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class OpenImageCommand implements Command { public readonly id = '_markdown.openImage'; + readonly #webviewManager: MarkdownPreviewManager; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - ) { } + webviewManager: MarkdownPreviewManager, + ) { + this.#webviewManager = webviewManager; + } public execute(args: { resource: string; imageSource: string }) { const source = vscode.Uri.parse(args.resource); - this._webviewManager.openDocumentLink(args.imageSource, source); + this.#webviewManager.openDocumentLink(args.imageSource, source); } } diff --git a/extensions/markdown-language-features/src/commands/refreshPreview.ts b/extensions/markdown-language-features/src/commands/refreshPreview.ts index a94fa797462..52f320098b7 100644 --- a/extensions/markdown-language-features/src/commands/refreshPreview.ts +++ b/extensions/markdown-language-features/src/commands/refreshPreview.ts @@ -10,13 +10,19 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class RefreshPreviewCommand implements Command { public readonly id = 'markdown.preview.refresh'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #engine: MarkdownItEngine; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _engine: MarkdownItEngine - ) { } + webviewManager: MarkdownPreviewManager, + engine: MarkdownItEngine + ) { + this.#webviewManager = webviewManager; + this.#engine = engine; + } public execute() { - this._engine.cleanCache(); - this._webviewManager.refresh(); + this.#engine.cleanCache(); + this.#webviewManager.refresh(); } } diff --git a/extensions/markdown-language-features/src/commands/reloadPlugins.ts b/extensions/markdown-language-features/src/commands/reloadPlugins.ts index 16be408bbef..5d410780a2b 100644 --- a/extensions/markdown-language-features/src/commands/reloadPlugins.ts +++ b/extensions/markdown-language-features/src/commands/reloadPlugins.ts @@ -10,14 +10,20 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class ReloadPlugins implements Command { public readonly id = 'markdown.api.reloadPlugins'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #engine: MarkdownItEngine; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _engine: MarkdownItEngine, - ) { } + webviewManager: MarkdownPreviewManager, + engine: MarkdownItEngine, + ) { + this.#webviewManager = webviewManager; + this.#engine = engine; + } public execute(): void { - this._engine.reloadPlugins(); - this._engine.cleanCache(); - this._webviewManager.refresh(); + this.#engine.reloadPlugins(); + this.#engine.cleanCache(); + this.#webviewManager.refresh(); } } diff --git a/extensions/markdown-language-features/src/commands/renderDocument.ts b/extensions/markdown-language-features/src/commands/renderDocument.ts index ccefddedbd2..ffcbab6d41a 100644 --- a/extensions/markdown-language-features/src/commands/renderDocument.ts +++ b/extensions/markdown-language-features/src/commands/renderDocument.ts @@ -10,11 +10,15 @@ import { ITextDocument } from '../types/textDocument'; export class RenderDocument implements Command { public readonly id = 'markdown.api.render'; + readonly #engine: MarkdownItEngine; + public constructor( - private readonly _engine: MarkdownItEngine - ) { } + engine: MarkdownItEngine + ) { + this.#engine = engine; + } public async execute(document: ITextDocument | string): Promise { - return (await (this._engine.render(document))).html; + return (await (this.#engine.render(document))).html; } } diff --git a/extensions/markdown-language-features/src/commands/showPreview.ts b/extensions/markdown-language-features/src/commands/showPreview.ts index d5d430ade0e..4d59062c1a1 100644 --- a/extensions/markdown-language-features/src/commands/showPreview.ts +++ b/extensions/markdown-language-features/src/commands/showPreview.ts @@ -53,14 +53,20 @@ async function showPreview( export class ShowPreviewCommand implements Command { public readonly id = 'markdown.showPreview'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #telemetryReporter: TelemetryReporter; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _telemetryReporter: TelemetryReporter - ) { } + webviewManager: MarkdownPreviewManager, + telemetryReporter: TelemetryReporter + ) { + this.#webviewManager = webviewManager; + this.#telemetryReporter = telemetryReporter; + } public execute(mainUri?: vscode.Uri, allUris?: vscode.Uri[], previewSettings?: DynamicPreviewSettings) { for (const uri of Array.isArray(allUris) ? allUris : [mainUri]) { - showPreview(this._webviewManager, this._telemetryReporter, uri, { + showPreview(this.#webviewManager, this.#telemetryReporter, uri, { sideBySide: false, locked: previewSettings?.locked }); @@ -71,13 +77,19 @@ export class ShowPreviewCommand implements Command { export class ShowPreviewToSideCommand implements Command { public readonly id = 'markdown.showPreviewToSide'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #telemetryReporter: TelemetryReporter; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _telemetryReporter: TelemetryReporter - ) { } + webviewManager: MarkdownPreviewManager, + telemetryReporter: TelemetryReporter + ) { + this.#webviewManager = webviewManager; + this.#telemetryReporter = telemetryReporter; + } public execute(uri?: vscode.Uri, previewSettings?: DynamicPreviewSettings) { - showPreview(this._webviewManager, this._telemetryReporter, uri, { + showPreview(this.#webviewManager, this.#telemetryReporter, uri, { sideBySide: true, locked: previewSettings?.locked }); @@ -88,13 +100,19 @@ export class ShowPreviewToSideCommand implements Command { export class ShowLockedPreviewToSideCommand implements Command { public readonly id = 'markdown.showLockedPreviewToSide'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #telemetryReporter: TelemetryReporter; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _telemetryReporter: TelemetryReporter - ) { } + webviewManager: MarkdownPreviewManager, + telemetryReporter: TelemetryReporter + ) { + this.#webviewManager = webviewManager; + this.#telemetryReporter = telemetryReporter; + } public execute(uri?: vscode.Uri) { - showPreview(this._webviewManager, this._telemetryReporter, uri, { + showPreview(this.#webviewManager, this.#telemetryReporter, uri, { sideBySide: true, locked: true }); diff --git a/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts b/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts index 7ea5a4079ed..fb6cd9ce9e7 100644 --- a/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts +++ b/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts @@ -12,19 +12,25 @@ import { isMarkdownFile } from '../util/file'; export class ShowPreviewSecuritySelectorCommand implements Command { public readonly id = 'markdown.showPreviewSecuritySelector'; + readonly #previewSecuritySelector: PreviewSecuritySelector; + readonly #previewManager: MarkdownPreviewManager; + public constructor( - private readonly _previewSecuritySelector: PreviewSecuritySelector, - private readonly _previewManager: MarkdownPreviewManager - ) { } + previewSecuritySelector: PreviewSecuritySelector, + previewManager: MarkdownPreviewManager + ) { + this.#previewSecuritySelector = previewSecuritySelector; + this.#previewManager = previewManager; + } public execute(resource: string | undefined) { - if (this._previewManager.activePreviewResource) { - this._previewSecuritySelector.showSecuritySelectorForResource(this._previewManager.activePreviewResource); + if (this.#previewManager.activePreviewResource) { + this.#previewSecuritySelector.showSecuritySelectorForResource(this.#previewManager.activePreviewResource); } else if (resource) { const source = vscode.Uri.parse(resource); - this._previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source); + this.#previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source); } else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) { - this._previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri); + this.#previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri); } } } diff --git a/extensions/markdown-language-features/src/commands/showSource.ts b/extensions/markdown-language-features/src/commands/showSource.ts index 87d6b21ec68..3a6bb3e0e20 100644 --- a/extensions/markdown-language-features/src/commands/showSource.ts +++ b/extensions/markdown-language-features/src/commands/showSource.ts @@ -10,12 +10,16 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class ShowSourceCommand implements Command { public readonly id = 'markdown.showSource'; + readonly #previewManager: MarkdownPreviewManager; + public constructor( - private readonly _previewManager: MarkdownPreviewManager - ) { } + previewManager: MarkdownPreviewManager + ) { + this.#previewManager = previewManager; + } public execute() { - const { activePreviewResource, activePreviewResourceColumn } = this._previewManager; + const { activePreviewResource, activePreviewResourceColumn } = this.#previewManager; if (activePreviewResource && activePreviewResourceColumn) { return vscode.workspace.openTextDocument(activePreviewResource).then(document => { return vscode.window.showTextDocument(document, activePreviewResourceColumn); diff --git a/extensions/markdown-language-features/src/commands/toggleLock.ts b/extensions/markdown-language-features/src/commands/toggleLock.ts index 9975d4872bb..0bc4656d08f 100644 --- a/extensions/markdown-language-features/src/commands/toggleLock.ts +++ b/extensions/markdown-language-features/src/commands/toggleLock.ts @@ -9,11 +9,15 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class ToggleLockCommand implements Command { public readonly id = 'markdown.preview.toggleLock'; + readonly #previewManager: MarkdownPreviewManager; + public constructor( - private readonly _previewManager: MarkdownPreviewManager - ) { } + previewManager: MarkdownPreviewManager + ) { + this.#previewManager = previewManager; + } public execute() { - this._previewManager.toggleLock(); + this.#previewManager.toggleLock(); } } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index 7791d6b19e4..e59a207487d 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -36,14 +36,18 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v ...Object.values(rootMediaMimesTypes).map(type => `${type}/*`), ]; - private readonly _yieldTo = [ + readonly #yieldTo = [ vscode.DocumentDropOrPasteEditKind.Text, vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment'), // Prefer notebook attachments ]; + readonly #parser: IMdParser; + constructor( - private readonly _parser: IMdParser, - ) { } + parser: IMdParser, + ) { + this.#parser = parser; + } public async provideDocumentDropEdits( document: vscode.TextDocument, @@ -51,8 +55,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken, ): Promise { - const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, { - insert: this._getEnabled(document, 'editor.drop.enabled'), + const edit = await this.#createEdit(document, [new vscode.Range(position, position)], dataTransfer, { + insert: this.#getEnabled(document, 'editor.drop.enabled'), copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles) }, undefined, token); @@ -64,7 +68,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v dropEdit.title = edit.label; dropEdit.kind = edit.kind; dropEdit.additionalEdit = edit.additionalEdits; - dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo]; + dropEdit.yieldTo = [...this.#yieldTo, ...edit.yieldTo]; return dropEdit; } @@ -75,8 +79,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, ): Promise { - const edit = await this._createEdit(document, ranges, dataTransfer, { - insert: this._getEnabled(document, 'editor.paste.enabled'), + const edit = await this.#createEdit(document, ranges, dataTransfer, { + insert: this.#getEnabled(document, 'editor.paste.enabled'), copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles) }, context, token); @@ -86,11 +90,11 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, edit.kind); pasteEdit.additionalEdit = edit.additionalEdits; - pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo]; + pasteEdit.yieldTo = [...this.#yieldTo, ...edit.yieldTo]; return [pasteEdit]; } - private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink { + #getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink { const setting = vscode.workspace.getConfiguration('markdown', document).get(settingName, true); // Convert old boolean values to new enum setting if (setting === false) { @@ -102,7 +106,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v } } - private async _createEdit( + async #createEdit( document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, @@ -117,27 +121,27 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v return; } - let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token); + let edit = await this.#createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token); if (token.isCancellationRequested) { return; } if (!edit) { - edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token); + edit = await this.#createEditFromUriListData(document, ranges, dataTransfer, context, token); } if (!edit || token.isCancellationRequested) { return; } - if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) { + if (!(await shouldInsertMarkdownLinkByDefault(this.#parser, document, settings.insert, ranges, token))) { edit.yieldTo.push(vscode.DocumentDropOrPasteEditKind.Empty.append('uri')); } return edit; } - private async _createEditFromUriListData( + async #createEditFromUriListData( document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, @@ -194,7 +198,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v * * This tries copying files outside of the workspace into the workspace. */ - private async _createEditForMediaFiles( + async #createEditForMediaFiles( document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, copyIntoWorkspace: CopyFilesSettings, diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts index 1625977a72c..fe9030704c1 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts @@ -12,7 +12,7 @@ import { CopyFileConfiguration, getCopyFileConfiguration, parseGlob, resolveCopy export class NewFilePathGenerator { - private readonly _usedPaths = new Set(); + readonly #usedPaths = new Set(); async getNewFilePath( document: vscode.TextDocument, @@ -33,13 +33,13 @@ export class NewFilePathGenerator { const name = i === 0 ? baseName : `${baseName}-${i}`; const uri = vscode.Uri.joinPath(root, name + ext); - if (this._wasPathAlreadyUsed(uri)) { + if (this.#wasPathAlreadyUsed(uri)) { continue; } // Try overwriting if it already exists if (config.overwriteBehavior === 'overwrite') { - this._usedPaths.add(uri.toString()); + this.#usedPaths.add(uri.toString()); return { uri, overwrite: true }; } @@ -47,17 +47,17 @@ export class NewFilePathGenerator { try { await vscode.workspace.fs.stat(uri); } catch { - if (!this._wasPathAlreadyUsed(uri)) { + if (!this.#wasPathAlreadyUsed(uri)) { // Does not exist - this._usedPaths.add(uri.toString()); + this.#usedPaths.add(uri.toString()); return { uri, overwrite: false }; } } } } - private _wasPathAlreadyUsed(uri: vscode.Uri) { - return this._usedPaths.has(uri.toString()); + #wasPathAlreadyUsed(uri: vscode.Uri) { + return this.#usedPaths.has(uri.toString()); } } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index a947216fe32..faad54e10c3 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -21,9 +21,13 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { public static readonly pasteMimeTypes = [Mime.textPlain]; + readonly #parser: IMdParser; + constructor( - private readonly _parser: IMdParser, - ) { } + parser: IMdParser, + ) { + this.#parser = parser; + } async provideDocumentPasteEdits( document: vscode.TextDocument, @@ -64,7 +68,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { workspaceEdit.set(document.uri, edit.edits); pasteEdit.additionalEdit = workspaceEdit; - if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) { + if (!(await shouldInsertMarkdownLinkByDefault(this.#parser, document, pasteUrlSetting, ranges, token))) { pasteEdit.yieldTo = [ vscode.DocumentDropOrPasteEditKind.Text, vscode.DocumentDropOrPasteEditKind.Empty.append('uri') diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index 8df16f4dcc6..871333cf4a4 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -19,18 +19,18 @@ export enum DiagnosticCode { class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { - private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks'; + static readonly #addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks'; - private static readonly _metadata: vscode.CodeActionProviderMetadata = { + static readonly #metadata: vscode.CodeActionProviderMetadata = { providedCodeActionKinds: [ vscode.CodeActionKind.QuickFix ], }; public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable { - const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider._metadata); + const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.#metadata); const commandReg = commandManager.register({ - id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + id: AddToIgnoreLinksQuickFixProvider.#addToIgnoreLinksCommandId, execute(resource: vscode.Uri, path: string) { const settingId = 'validate.ignoredLinks'; const config = vscode.workspace.getConfiguration('markdown', resource); @@ -58,7 +58,7 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { vscode.CodeActionKind.QuickFix); fix.command = { - command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + command: AddToIgnoreLinksQuickFixProvider.#addToIgnoreLinksCommandId, title: '', arguments: [document.uri, hrefText], }; diff --git a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts index bda8b721e8b..23a2ed9e770 100644 --- a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts +++ b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts @@ -13,9 +13,13 @@ export class FindFileReferencesCommand implements Command { public readonly id = 'markdown.findAllFileReferences'; + readonly #client: MdLanguageClient; + constructor( - private readonly _client: MdLanguageClient, - ) { } + client: MdLanguageClient, + ) { + this.#client = client; + } public async execute(resource?: vscode.Uri) { resource ??= vscode.window.activeTextEditor?.document.uri; @@ -28,7 +32,7 @@ export class FindFileReferencesCommand implements Command { location: vscode.ProgressLocation.Window, title: vscode.l10n.t("Finding file references") }, async (_progress, token) => { - const locations = (await this._client.getReferencesToFileInWorkspace(resource, token)).map(loc => { + const locations = (await this.#client.getReferencesToFileInWorkspace(resource, token)).map(loc => { return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range)); }); diff --git a/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts b/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts index 5d42a033842..d912caa9060 100644 --- a/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts +++ b/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts @@ -33,46 +33,48 @@ interface RenameAction { class UpdateLinksOnFileRenameHandler extends Disposable { - private readonly _delayer = new Delayer(50); - private readonly _pendingRenames = new Set(); + readonly #delayer = new Delayer(50); + readonly #pendingRenames = new Set(); + readonly #client: MdLanguageClient; public constructor( - private readonly _client: MdLanguageClient, + client: MdLanguageClient, ) { super(); + this.#client = client; this._register(vscode.workspace.onDidRenameFiles(async (e) => { await Promise.all(e.files.map(async (rename) => { - if (await this._shouldParticipateInLinkUpdate(rename.newUri)) { - this._pendingRenames.add(rename); + if (await this.#shouldParticipateInLinkUpdate(rename.newUri)) { + this.#pendingRenames.add(rename); } })); - if (this._pendingRenames.size) { - this._delayer.trigger(() => { + if (this.#pendingRenames.size) { + this.#delayer.trigger(() => { vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: vscode.l10n.t("Checking for Markdown links to update") - }, () => this._flushRenames()); + }, () => this.#flushRenames()); }); } })); } - private async _flushRenames(): Promise { - const renames = Array.from(this._pendingRenames); - this._pendingRenames.clear(); + async #flushRenames(): Promise { + const renames = Array.from(this.#pendingRenames); + this.#pendingRenames.clear(); - const result = await this._getEditsForFileRename(renames, noopToken); + const result = await this.#getEditsForFileRename(renames, noopToken); if (result?.edit.size) { - if (await this._confirmActionWithUser(result.resourcesBeingRenamed)) { + if (await this.#confirmActionWithUser(result.resourcesBeingRenamed)) { await vscode.workspace.applyEdit(result.edit); } } } - private async _confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise { + async #confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise { if (!newResources.length) { return false; } @@ -81,7 +83,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { const setting = config.get(settingNames.enabled); switch (setting) { case UpdateLinksOnFileMoveSetting.Prompt: - return this._promptUser(newResources); + return this.#promptUser(newResources); case UpdateLinksOnFileMoveSetting.Always: return true; case UpdateLinksOnFileMoveSetting.Never: @@ -89,7 +91,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return false; } } - private async _shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise { + async #shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise { const config = vscode.workspace.getConfiguration('markdown', newUri); const setting = config.get(settingNames.enabled); if (setting === UpdateLinksOnFileMoveSetting.Never) { @@ -113,7 +115,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return false; } - private async _promptUser(newResources: readonly vscode.Uri[]): Promise { + async #promptUser(newResources: readonly vscode.Uri[]): Promise { if (!newResources.length) { return false; } @@ -138,7 +140,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { const choice = await vscode.window.showInformationMessage( newResources.length === 1 ? vscode.l10n.t("Update Markdown links for '{0}'?", Utils.basename(newResources[0])) - : this._getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), { + : this.#getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), { modal: true, }, rejectItem, acceptItem, alwaysItem, neverItem); @@ -154,7 +156,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { config.update( settingNames.enabled, UpdateLinksOnFileMoveSetting.Always, - this._getConfigTargetScope(config, settingNames.enabled)); + this.#getConfigTargetScope(config, settingNames.enabled)); return true; } case neverItem: { @@ -162,7 +164,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { config.update( settingNames.enabled, UpdateLinksOnFileMoveSetting.Never, - this._getConfigTargetScope(config, settingNames.enabled)); + this.#getConfigTargetScope(config, settingNames.enabled)); return false; } default: { @@ -171,8 +173,8 @@ class UpdateLinksOnFileRenameHandler extends Disposable { } } - private async _getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> { - const result = await this._client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token); + async #getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> { + const result = await this.#client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token); if (!result?.edit.documentChanges?.length) { return undefined; } @@ -192,7 +194,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { }; } - private _getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string { + #getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string { const MAX_CONFIRM_FILES = 10; const paths = [start]; @@ -211,7 +213,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return paths.join('\n'); } - private _getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget { + #getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget { const inspected = config.inspect(settingsName); if (inspected?.workspaceFolderValue) { return vscode.ConfigurationTarget.WorkspaceFolder; diff --git a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts index f8a7128eb05..b27a869db0e 100644 --- a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts @@ -13,16 +13,20 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider public static readonly metadataMime = 'application/vnd.vscode.markdown.updatelinks.metadata'; + readonly #client: MdLanguageClient; + constructor( - private readonly _client: MdLanguageClient, - ) { } + client: MdLanguageClient, + ) { + this.#client = client; + } async prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { - if (!this._isEnabled(document)) { + if (!this.#isEnabled(document)) { return; } - const metadata = await this._client.prepareUpdatePastedLinks(document.uri, ranges, token); + const metadata = await this.#client.prepareUpdatePastedLinks(document.uri, ranges, token); if (token.isCancellationRequested) { return; } @@ -37,7 +41,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, ): Promise { - if (!this._isEnabled(document)) { + if (!this.#isEnabled(document)) { return; } @@ -56,7 +60,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider // - copy empty line // - Copy with multiple cursors and paste into multiple locations // - ... - const edits = await this._client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token); + const edits = await this.#client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token); if (!edits?.length || token.isCancellationRequested) { return; } @@ -73,7 +77,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider return [pasteEdit]; } - private _isEnabled(document: vscode.TextDocument): boolean { + #isEnabled(document: vscode.TextDocument): boolean { return vscode.workspace.getConfiguration('markdown', document.uri).get('editor.updateLinksOnPaste.enabled', true); } } diff --git a/extensions/markdown-language-features/src/logging.ts b/extensions/markdown-language-features/src/logging.ts index b5ea76f3608..30839d8c756 100644 --- a/extensions/markdown-language-features/src/logging.ts +++ b/extensions/markdown-language-features/src/logging.ts @@ -12,11 +12,11 @@ export interface ILogger { } export class VsCodeOutputLogger extends Disposable implements ILogger { - private _outputChannelValue?: vscode.LogOutputChannel; + #outputChannelValue?: vscode.LogOutputChannel; - private get _outputChannel() { - this._outputChannelValue ??= this._register(vscode.window.createOutputChannel('Markdown', { log: true })); - return this._outputChannelValue; + get #outputChannel() { + this.#outputChannelValue ??= this._register(vscode.window.createOutputChannel('Markdown', { log: true })); + return this.#outputChannelValue; } constructor() { @@ -24,6 +24,6 @@ export class VsCodeOutputLogger extends Disposable implements ILogger { } public trace(title: string, message: string, data?: any): void { - this._outputChannel.trace(`${title}: ${message}`, ...(data ? [JSON.stringify(data, null, 4)] : [])); + this.#outputChannel.trace(`${title}: ${message}`, ...(data ? [JSON.stringify(data, null, 4)] : [])); } } diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 0f4c7eb6717..4ed3186c380 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -44,37 +44,37 @@ const pluginSourceMap: MarkdownIt.PluginSimple = (md): void => { type MarkdownItConfig = Readonly>>; class TokenCache { - private _cachedDocument?: { + #cachedDocument?: { readonly uri: vscode.Uri; readonly version: number; readonly config: MarkdownItConfig; }; - private _tokens?: MarkdownIt.Token[]; + #tokens?: MarkdownIt.Token[]; public tryGetCached(document: ITextDocument, config: MarkdownItConfig): MarkdownIt.Token[] | undefined { - if (this._cachedDocument - && this._cachedDocument.uri.toString() === document.uri.toString() - && document.version >= 0 && this._cachedDocument.version === document.version - && this._cachedDocument.config.breaks === config.breaks - && this._cachedDocument.config.linkify === config.linkify + if (this.#cachedDocument + && this.#cachedDocument.uri.toString() === document.uri.toString() + && document.version >= 0 && this.#cachedDocument.version === document.version + && this.#cachedDocument.config.breaks === config.breaks + && this.#cachedDocument.config.linkify === config.linkify ) { - return this._tokens; + return this.#tokens; } return undefined; } public update(document: ITextDocument, config: MarkdownItConfig, tokens: MarkdownIt.Token[]) { - this._cachedDocument = { + this.#cachedDocument = { uri: document.uri, version: document.version, config, }; - this._tokens = tokens; + this.#tokens = tokens; } public clean(): void { - this._cachedDocument = undefined; - this._tokens = undefined; + this.#cachedDocument = undefined; + this.#tokens = undefined; } } @@ -98,40 +98,45 @@ export interface IMdParser { export class MarkdownItEngine implements IMdParser { - private _md?: Promise; + #md?: Promise; - private readonly _tokenCache = new TokenCache(); + readonly #tokenCache = new TokenCache(); public readonly slugifier: ISlugifier; - public constructor( - private readonly _contributionProvider: MarkdownContributionProvider, - slugifier: ISlugifier, - private readonly _logger: ILogger, - ) { - this.slugifier = slugifier; + readonly #contributionProvider: MarkdownContributionProvider; + readonly #logger: ILogger; - _contributionProvider.onContributionsChanged(() => { + public constructor( + contributionProvider: MarkdownContributionProvider, + slugifier: ISlugifier, + logger: ILogger, + ) { + this.#contributionProvider = contributionProvider; + this.slugifier = slugifier; + this.#logger = logger; + + contributionProvider.onContributionsChanged(() => { // Markdown plugin contributions may have changed - this._md = undefined; - this._tokenCache.clean(); + this.#md = undefined; + this.#tokenCache.clean(); }); } public async getEngine(resource: vscode.Uri | undefined): Promise { - const config = this._getConfig(resource); - return this._getEngine(config); + const config = this.#getConfig(resource); + return this.#getEngine(config); } - private async _getEngine(config: MarkdownItConfig): Promise { - if (!this._md) { - this._md = (async () => { + async #getEngine(config: MarkdownItConfig): Promise { + if (!this.#md) { + this.#md = (async () => { const markdownIt = await import('markdown-it'); let md: MarkdownIt = markdownIt.default(await getMarkdownOptions(() => md)); md.linkify.set({ fuzzyLink: false }); - for (const plugin of this._contributionProvider.contributions.markdownItPlugins.values()) { + for (const plugin of this.#contributionProvider.contributions.markdownItPlugins.values()) { try { md = (await plugin)(md); } catch (e) { @@ -154,43 +159,43 @@ export class MarkdownItEngine implements IMdParser { alt: ['paragraph', 'reference', 'blockquote', 'list'] }); - this._addImageRenderer(md); - this._addFencedRenderer(md); - this._addLinkNormalizer(md); - this._addLinkValidator(md); - this._addNamedHeaders(md); - this._addLinkRenderer(md); + this.#addImageRenderer(md); + this.#addFencedRenderer(md); + this.#addLinkNormalizer(md); + this.#addLinkValidator(md); + this.#addNamedHeaders(md); + this.#addLinkRenderer(md); md.use(pluginSourceMap); return md; })(); } - const md = await this._md!; + const md = await this.#md!; md.set(config); return md; } public reloadPlugins() { - this._md = undefined; + this.#md = undefined; } - private _tokenizeDocument( + #tokenizeDocument( document: ITextDocument, config: MarkdownItConfig, engine: MarkdownIt ): MarkdownIt.Token[] { - const cached = this._tokenCache.tryGetCached(document, config); + const cached = this.#tokenCache.tryGetCached(document, config); if (cached) { return cached; } - this._logger.trace('MarkdownItEngine', `tokenizeDocument - ${document.uri}`); - const tokens = this._tokenizeString(document.getText(), engine); - this._tokenCache.update(document, config, tokens); + this.#logger.trace('MarkdownItEngine', `tokenizeDocument - ${document.uri}`); + const tokens = this.#tokenizeString(document.getText(), engine); + this.#tokenCache.update(document, config, tokens); return tokens; } - private _tokenizeString(text: string, engine: MarkdownIt) { + #tokenizeString(text: string, engine: MarkdownIt) { const env: RenderEnv = { currentDocument: undefined, containingImages: new Set(), @@ -201,12 +206,12 @@ export class MarkdownItEngine implements IMdParser { } public async render(input: ITextDocument | string, resourceProvider?: WebviewResourceProvider): Promise { - const config = this._getConfig(typeof input === 'string' ? undefined : input.uri); - const engine = await this._getEngine(config); + const config = this.#getConfig(typeof input === 'string' ? undefined : input.uri); + const engine = await this.#getEngine(config); const tokens = typeof input === 'string' - ? this._tokenizeString(input, engine) - : this._tokenizeDocument(input, config, engine); + ? this.#tokenizeString(input, engine) + : this.#tokenizeDocument(input, config, engine); const env: RenderEnv = { containingImages: new Set(), @@ -227,16 +232,16 @@ export class MarkdownItEngine implements IMdParser { } public async tokenize(document: ITextDocument): Promise { - const config = this._getConfig(document.uri); - const engine = await this._getEngine(config); - return this._tokenizeDocument(document, config, engine); + const config = this.#getConfig(document.uri); + const engine = await this.#getEngine(config); + return this.#tokenizeDocument(document, config, engine); } public cleanCache(): void { - this._tokenCache.clean(); + this.#tokenCache.clean(); } - private _getConfig(resource?: vscode.Uri): MarkdownItConfig { + #getConfig(resource?: vscode.Uri): MarkdownItConfig { const config = MarkdownPreviewConfiguration.getForResource(resource ?? null); return { breaks: config.previewLineBreaks, @@ -245,7 +250,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addImageRenderer(md: MarkdownIt): void { + #addImageRenderer(md: MarkdownIt): void { const original = md.renderer.rules.image; md.renderer.rules.image = (tokens: MarkdownIt.Token[], idx: number, options, env: RenderEnv, self) => { const token = tokens[idx]; @@ -254,7 +259,7 @@ export class MarkdownItEngine implements IMdParser { env.containingImages?.add(src); if (!token.attrGet('data-src')) { - token.attrSet('src', this._toResourceUri(src, env.currentDocument, env.resourceProvider)); + token.attrSet('src', this.#toResourceUri(src, env.currentDocument, env.resourceProvider)); token.attrSet('data-src', src); } } @@ -267,7 +272,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addFencedRenderer(md: MarkdownIt): void { + #addFencedRenderer(md: MarkdownIt): void { const original = md.renderer.rules['fenced']; md.renderer.rules['fenced'] = (tokens: MarkdownIt.Token[], idx: number, options, env, self) => { const token = tokens[idx]; @@ -283,7 +288,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addLinkNormalizer(md: MarkdownIt): void { + #addLinkNormalizer(md: MarkdownIt): void { const normalizeLink = md.normalizeLink; md.normalizeLink = (link: string) => { try { @@ -299,7 +304,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addLinkValidator(md: MarkdownIt): void { + #addLinkValidator(md: MarkdownIt): void { const validateLink = md.validateLink; md.validateLink = (link: string) => { return validateLink(link) @@ -309,10 +314,10 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addNamedHeaders(md: MarkdownIt): void { + #addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; md.renderer.rules.heading_open = (tokens: MarkdownIt.Token[], idx: number, options, env: unknown, self) => { - const title = this._tokenToPlainText(tokens[idx + 1]); + const title = this.#tokenToPlainText(tokens[idx + 1]); const slug = (env as RenderEnv).slugifier ? (env as RenderEnv).slugifier.add(title) : this.slugifier.fromHeading(title); tokens[idx].attrSet('id', slug.value); @@ -324,9 +329,9 @@ export class MarkdownItEngine implements IMdParser { }; } - private _tokenToPlainText(token: MarkdownIt.Token): string { + #tokenToPlainText(token: MarkdownIt.Token): string { if (token.children) { - return token.children.map(x => this._tokenToPlainText(x)).join(''); + return token.children.map(x => this.#tokenToPlainText(x)).join(''); } switch (token.type) { @@ -339,7 +344,7 @@ export class MarkdownItEngine implements IMdParser { } } - private _addLinkRenderer(md: MarkdownIt): void { + #addLinkRenderer(md: MarkdownIt): void { const original = md.renderer.rules.link_open; md.renderer.rules.link_open = (tokens: MarkdownIt.Token[], idx: number, options, env, self) => { @@ -357,7 +362,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { + #toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { try { // Support file:// links if (isOfScheme(Schemes.file, href)) { diff --git a/extensions/markdown-language-features/src/markdownExtensions.ts b/extensions/markdown-language-features/src/markdownExtensions.ts index 1357b03d1de..568a0f0b638 100644 --- a/extensions/markdown-language-features/src/markdownExtensions.ts +++ b/extensions/markdown-language-features/src/markdownExtensions.ts @@ -119,36 +119,38 @@ export interface MarkdownContributionProvider { class VSCodeExtensionMarkdownContributionProvider extends Disposable implements MarkdownContributionProvider { - private _contributions?: MarkdownContributions; + #contributions?: MarkdownContributions; + readonly #extensionContext: vscode.ExtensionContext; public constructor( - private readonly _extensionContext: vscode.ExtensionContext, + extensionContext: vscode.ExtensionContext, ) { super(); + this.#extensionContext = extensionContext; this._register(vscode.extensions.onDidChange(() => { - const currentContributions = this._getCurrentContributions(); - const existingContributions = this._contributions || MarkdownContributions.Empty; + const currentContributions = this.#getCurrentContributions(); + const existingContributions = this.#contributions || MarkdownContributions.Empty; if (!MarkdownContributions.equal(existingContributions, currentContributions)) { - this._contributions = currentContributions; - this._onContributionsChanged.fire(this); + this.#contributions = currentContributions; + this.#onContributionsChanged.fire(this); } })); } public get extensionUri() { - return this._extensionContext.extensionUri; + return this.#extensionContext.extensionUri; } - private readonly _onContributionsChanged = this._register(new vscode.EventEmitter()); - public readonly onContributionsChanged = this._onContributionsChanged.event; + readonly #onContributionsChanged = this._register(new vscode.EventEmitter()); + public readonly onContributionsChanged = this.#onContributionsChanged.event; public get contributions(): MarkdownContributions { - this._contributions ??= this._getCurrentContributions(); - return this._contributions; + this.#contributions ??= this.#getCurrentContributions(); + return this.#contributions; } - private _getCurrentContributions(): MarkdownContributions { + #getCurrentContributions(): MarkdownContributions { return vscode.extensions.all .map(MarkdownContributions.fromExtension) .reduce(MarkdownContributions.merge, MarkdownContributions.Empty); diff --git a/extensions/markdown-language-features/src/preview/documentRenderer.ts b/extensions/markdown-language-features/src/preview/documentRenderer.ts index 61182a24436..f96fce9b745 100644 --- a/extensions/markdown-language-features/src/preview/documentRenderer.ts +++ b/extensions/markdown-language-features/src/preview/documentRenderer.ts @@ -41,16 +41,28 @@ export interface ImageInfo { } export class MdDocumentRenderer { + + readonly #engine: MarkdownItEngine; + readonly #context: vscode.ExtensionContext; + readonly #cspArbiter: ContentSecurityPolicyArbiter; + readonly #contributionProvider: MarkdownContributionProvider; + readonly #logger: ILogger; + constructor( - private readonly _engine: MarkdownItEngine, - private readonly _context: vscode.ExtensionContext, - private readonly _cspArbiter: ContentSecurityPolicyArbiter, - private readonly _contributionProvider: MarkdownContributionProvider, - private readonly _logger: ILogger + engine: MarkdownItEngine, + context: vscode.ExtensionContext, + cspArbiter: ContentSecurityPolicyArbiter, + contributionProvider: MarkdownContributionProvider, + logger: ILogger ) { + this.#engine = engine; + this.#context = context; + this.#cspArbiter = cspArbiter; + this.#contributionProvider = contributionProvider; + this.#logger = logger; this.iconPath = { - dark: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-dark.svg'), - light: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-light.svg'), + dark: vscode.Uri.joinPath(this.#context.extensionUri, 'media', 'preview-dark.svg'), + light: vscode.Uri.joinPath(this.#context.extensionUri, 'media', 'preview-light.svg'), }; } @@ -76,15 +88,15 @@ export class MdDocumentRenderer { scrollPreviewWithEditor: config.scrollPreviewWithEditor, scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, - disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(), + disableSecurityWarnings: this.#cspArbiter.shouldDisableSecurityWarnings(), webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(), }; - this._logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData); + this.#logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData); // Content Security Policy const nonce = generateUuid(); - const csp = this._getCsp(resourceProvider, sourceUri, nonce); + const csp = this.#getCsp(resourceProvider, sourceUri, nonce); const body = await this.renderBody(markdownDocument, resourceProvider); if (token.isCancellationRequested) { @@ -92,7 +104,7 @@ export class MdDocumentRenderer { } const html = ` - + @@ -101,12 +113,12 @@ export class MdDocumentRenderer { data-strings="${escapeAttribute(JSON.stringify(previewStrings))}" data-state="${escapeAttribute(JSON.stringify(state || {}))}" data-initial-md-content="${escapeAttribute(body.html)}"> - - ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} + + ${this.#getStyles(resourceProvider, sourceUri, config, imageInfo)} - ${this._getScripts(resourceProvider, nonce)} + ${this.#getScripts(resourceProvider, nonce)} `; return { @@ -119,7 +131,7 @@ export class MdDocumentRenderer { markdownDocument: vscode.TextDocument, resourceProvider: WebviewResourceProvider, ): Promise { - const rendered = await this._engine.render(markdownDocument, resourceProvider); + const rendered = await this.#engine.render(markdownDocument, resourceProvider); const html = `
${rendered.html}
`; return { html, @@ -138,13 +150,13 @@ export class MdDocumentRenderer { `; } - private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { + #extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { const webviewResource = resourceProvider.asWebviewUri( - vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile)); + vscode.Uri.joinPath(this.#context.extensionUri, 'media', mediaFile)); return webviewResource.toString(); } - private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string { + #fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string { if (!href) { return href; } @@ -168,18 +180,18 @@ export class MdDocumentRenderer { return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString(); } - private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { + #computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { if (!Array.isArray(config.styles)) { return ''; } const out: string[] = []; for (const style of config.styles) { - out.push(``); + out.push(``); } return out.join('\n'); } - private _getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { + #getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { return [ config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '', isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`, @@ -187,7 +199,7 @@ export class MdDocumentRenderer { ].join(' '); } - private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string { + #getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string { if (!imageInfo.length) { return ''; } @@ -204,20 +216,20 @@ export class MdDocumentRenderer { return ret; } - private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string { + #getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string { const baseStyles: string[] = []; - for (const resource of this._contributionProvider.contributions.previewStyles) { + for (const resource of this.#contributionProvider.contributions.previewStyles) { baseStyles.push(``); } return `${baseStyles.join('\n')} - ${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)} - ${this._getImageStabilizerStyles(imageInfo)}`; + ${this.#computeCustomStyleSheetIncludes(resourceProvider, resource, config)} + ${this.#getImageStabilizerStyles(imageInfo)}`; } - private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { + #getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { const out: string[] = []; - for (const resource of this._contributionProvider.contributions.previewScripts) { + for (const resource of this.#contributionProvider.contributions.previewScripts) { out.push(` +`; + + // Reuse existing webview or create one on first video navigation + let webview: IWebviewElement; + if (!this._videoWebview) { + webview = this._contentDisposables.add(this._webviewService.createWebviewElement({ + title: currentImage.name, + options: { disableServiceWorker: true }, + contentOptions: { allowScripts: true }, + extension: undefined, + })); + webview.mountTo(this._elements.videoContainer, this.window); + this._videoWebview = webview; + } else { + webview = this._videoWebview; + } + + webview.setHtml(videoHtml); + + // Send the video data to the webview via postMessage + const buffer = (rawData as Uint8Array).buffer; + webview.postMessage({ type: 'loadVideo', data: buffer, mimeType: currentImage.mimeType }, [buffer]); + } else { + // Show image, hide video container + this._elements.videoContainer.style.display = 'none'; + this._elements.mainImage.style.display = ''; + this._elements.mainImageContainer.style.cursor = ''; + + const url = await this._loadBlobUrl(currentImage); + + // If the user navigated while loading the blob URL, discard this result. + if (this._currentIndex !== navigationIndex) { + return; + } + + const tmp = new Image(); + tmp.src = url; + tmp.decode().then(() => { + // Only apply if user hasn't navigated away during decode + if (this._currentIndex === navigationIndex && this._elements) { + this._elements.mainImage.src = url; + this._elements.mainImage.alt = currentImage.name; + } + }, () => { + // Decode failed (invalid image) — still show src for browser fallback + if (this._currentIndex === navigationIndex && this._elements) { + this._elements.mainImage.src = url; + this._elements.mainImage.alt = currentImage.name; + } + }); + } + + // Reset zoom when switching images + this._applyZoom('fit'); + + // Update info bar: caption + separator + counter + if (currentImage.caption) { + this._elements.captionText.textContent = currentImage.caption; + this._elements.captionText.style.display = ''; + this._elements.captionSeparator.style.display = ''; + } else { + this._elements.captionText.textContent = ''; + this._elements.captionText.style.display = 'none'; + this._elements.captionSeparator.style.display = 'none'; + } + this._elements.counter.textContent = localize('imageCarousel.counter', "{0} / {1}", this._currentIndex + 1, this._flatImages.length); + + // Update button states + this._elements.prevBtn.disabled = this._currentIndex === 0; + this._elements.nextBtn.disabled = this._currentIndex === this._flatImages.length - 1; + + // Update thumbnail selection — only toggle active class and + // call getBoundingClientRect on the active thumbnail to avoid + // layout thrashing across all thumbnails on every navigation. + for (let i = 0; i < this._thumbnailElements.length; i++) { + const isActive = i === this._currentIndex; + const thumbnail = this._thumbnailElements[i]; + thumbnail.classList.toggle('active', isActive); + if (isActive) { + thumbnail.setAttribute('aria-current', 'page'); + } else { + thumbnail.removeAttribute('aria-current'); + } + } + + // Scroll the active thumbnail into view without blocking the main thread. + // Using scrollIntoView with 'nearest' avoids forced layout from + // getBoundingClientRect + scrollLeft and is handled efficiently by + // the browser's scroll machinery. + const activeThumbnail = this._thumbnailElements[this._currentIndex]; + if (activeThumbnail) { + activeThumbnail.scrollIntoView({ block: 'nearest', inline: 'nearest' }); + } + + // Update editor title to reflect current section + if (this.input instanceof ImageCarouselEditorInput) { + const currentSection = this._sections[entry.sectionIndex]; + this.input.setName(currentSection.title || this.input.collection.title); + } + + // Preload adjacent images for smoother navigation + this._preloadAdjacentImages(); + } + + private async _loadBlobUrl(image: ICarouselImage): Promise { + const cached = this._blobUrlCache.get(image.id); + if (cached) { + return cached; + } + + let buffer: Uint8Array; + if (image.data) { + // Handle both VSBuffer (has .buffer property) and raw Uint8Array from chat attachments + buffer = image.data instanceof Uint8Array ? image.data : image.data.buffer; + } else if (image.uri) { + const content = await this._fileService.readFile(image.uri); + buffer = content.value.buffer; + } else { + return ''; + } + + const blob = new Blob([buffer as Uint8Array], { type: image.mimeType }); + const url = URL.createObjectURL(blob); + this._blobUrlCache.set(image.id, url); + return url; + } + + private _revokeCachedBlobUrls(): void { + for (const url of this._blobUrlCache.values()) { + URL.revokeObjectURL(url); + } + this._blobUrlCache.clear(); + } + + private async _loadRawData(image: ICarouselImage): Promise { + if (image.data) { + return image.data instanceof Uint8Array ? image.data : image.data.buffer; + } else if (image.uri) { + const content = await this._fileService.readFile(image.uri); + return content.value.buffer; + } + return new Uint8Array(0); + } + + private _preloadAdjacentImages(): void { + for (const idx of [this._currentIndex - 1, this._currentIndex + 1]) { + if (idx >= 0 && idx < this._flatImages.length) { + const adjacentImage = this._flatImages[idx].image; + if (isVideoMimeType(adjacentImage.mimeType)) { + // For video, preload raw data into the file service cache + this._loadRawData(adjacentImage).catch(() => { /* ignore */ }); + } else { + this._loadBlobUrl(adjacentImage).then(url => { + // Pre-decode via decode() so the compositor doesn't block + // the main thread decoding this image during commit. + const img = new Image(); + img.src = url; + img.decode().catch(() => { /* invalid image */ }); + }); + } + } + } + } + + previous(): void { + if (this._currentIndex > 0) { + this._currentIndex--; + this.updateCurrentImage(); + } + } + + next(): void { + if (this._currentIndex < this._flatImages.length - 1) { + this._currentIndex++; + this.updateCurrentImage(); + } + } + + /** + * Compute the current display scale when transitioning from 'fit' to numeric zoom. + */ + private _initZoomFromFit(): void { + if (!this._elements) { + return; + } + const img = this._elements.mainImage; + if (img.naturalWidth > 0) { + this._zoomScale = img.clientWidth / img.naturalWidth; + } else { + this._zoomScale = 1; + } + } + + /** + * Zoom in to the next predefined zoom level. + */ + private _zoomIn(): void { + if (this._zoomScale === 'fit') { + this._initZoomFromFit(); + } + const scale = this._zoomScale as number; + let i = 0; + for (; i < ZOOM_LEVELS.length; ++i) { + if (ZOOM_LEVELS[i] > scale) { + break; + } + } + this._applyZoom(ZOOM_LEVELS[i] ?? MAX_SCALE); + } + + /** + * Zoom out to the previous predefined zoom level. + */ + private _zoomOut(): void { + if (this._zoomScale === 'fit') { + this._initZoomFromFit(); + } + const scale = this._zoomScale as number; + let i = ZOOM_LEVELS.length - 1; + for (; i >= 0; --i) { + if (ZOOM_LEVELS[i] < scale) { + break; + } + } + this._applyZoom(ZOOM_LEVELS[i] ?? MIN_SCALE); + } + + /** + * Apply fit-to-container or numeric zoom with scroll-center preservation. + */ + private _applyZoom(newScale: ZoomScale): void { + if (!this._elements) { + return; + } + + const container = this._elements.mainImageContainer; + const img = this._elements.mainImage; + + if (newScale === 'fit') { + this._zoomScale = 'fit'; + img.classList.add('scale-to-fit'); + img.classList.remove('pixelated'); + img.style.zoom = ''; + // Remove zoomed/overflow before scrollTo to avoid an expensive + // synchronous ScrollLayer that blocks the main thread. + const wasZoomed = container.classList.contains('zoomed'); + container.classList.remove('zoomed'); + container.classList.remove('zoom-out'); + if (wasZoomed) { + container.scrollTo(0, 0); + } + } else { + const scale = clamp(newScale, MIN_SCALE, MAX_SCALE); + this._zoomScale = scale; + + // Capture scroll center ratio before changing zoom. + const dx = container.scrollWidth > 0 + ? (container.scrollLeft + container.clientWidth / 2) / container.scrollWidth + : 0.5; + const dy = container.scrollHeight > 0 + ? (container.scrollTop + container.clientHeight / 2) / container.scrollHeight + : 0.5; + + img.classList.remove('scale-to-fit'); + img.classList.toggle('pixelated', scale >= PIXELATION_THRESHOLD); + img.style.zoom = String(scale); + container.classList.add('zoomed'); + + // Restore scroll center — works because setting img.style.zoom triggers + // synchronous layout, so scrollWidth/scrollHeight reflect the new size. + const newScrollX = container.scrollWidth * dx - container.clientWidth / 2; + const newScrollY = container.scrollHeight * dy - container.clientHeight / 2; + container.scrollTo(newScrollX, newScrollY); + } + } + + override focus(): void { + super.focus(); + this._elements?.root.focus(); + } + + override layout(dimension: Dimension): void { + if (this._container) { + this._container.style.width = `${dimension.width}px`; + this._container.style.height = `${dimension.height}px`; + } + } +} diff --git a/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts new file mode 100644 index 00000000000..50f76b0bff0 --- /dev/null +++ b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselEditorInput.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { IImageCarouselCollection } from './imageCarouselTypes.js'; + +export class ImageCarouselEditorInput extends EditorInput { + static readonly ID = 'workbench.input.imageCarousel'; + + private _resource: URI; + private _name: string; + + override get capabilities(): EditorInputCapabilities { + return super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.RequiresModal; + } + + constructor( + public readonly collection: IImageCarouselCollection, + public readonly startIndex: number = 0 + ) { + super(); + this._resource = URI.from({ + scheme: Schemas.vscodeImageCarousel, + path: `/${encodeURIComponent(collection.id)}`, + }); + this._name = collection.title; + } + + get typeId(): string { + return ImageCarouselEditorInput.ID; + } + + get resource(): URI { + return this._resource; + } + + override getName(): string { + return this._name; + } + + setName(name: string): void { + if (this._name !== name) { + this._name = name; + this._onDidChangeLabel.fire(); + } + } + + override matches(other: EditorInput | IUntypedEditorInput): boolean { + if (other instanceof ImageCarouselEditorInput) { + return other.collection.id === this.collection.id; + } + return false; + } +} diff --git a/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselTypes.ts b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselTypes.ts new file mode 100644 index 00000000000..0c713e948ad --- /dev/null +++ b/src/vs/workbench/contrib/imageCarousel/browser/imageCarouselTypes.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { VSBuffer } from '../../../../base/common/buffer.js'; + +export interface ICarouselImage { + readonly id: string; + readonly name: string; + readonly mimeType: string; + /** In-memory image data. Omit when the image can be loaded lazily from `uri`. */ + readonly data?: VSBuffer; + readonly uri?: URI; + readonly source?: string; + readonly caption?: string; +} + +export interface ICarouselSection { + readonly title: string; + readonly images: ReadonlyArray; +} + +export interface IImageCarouselCollection { + readonly id: string; + readonly title: string; + readonly sections: ReadonlyArray; +} + +export function isVideoMimeType(mimeType: string): boolean { + return mimeType.startsWith('video/'); +} diff --git a/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css b/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css new file mode 100644 index 00000000000..4ef892836f8 --- /dev/null +++ b/src/vs/workbench/contrib/imageCarousel/browser/media/imageCarousel.css @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.image-carousel-editor { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.image-carousel-editor .empty-message { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--vscode-descriptionForeground); + font-size: 14px; +} + +.image-carousel-editor .slideshow-container { + display: flex; + flex-direction: column; + height: 100%; + gap: 8px; + overflow: hidden; +} + +.image-carousel-editor .slideshow-container:focus, +.image-carousel-editor .slideshow-container:focus-visible { + outline: none !important; +} + +/* Image viewing area */ +.image-carousel-editor .image-area { + flex: 1; + min-height: 0; + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.image-carousel-editor .main-image-container { + width: 100%; + height: 100%; + display: flex; + overflow: hidden; + cursor: zoom-in; +} + +.image-carousel-editor .main-image-container.zoom-out { + cursor: zoom-out; +} + +.image-carousel-editor .main-image-container.zoomed { + overflow: auto; + padding: 0; + scrollbar-color: var(--vscode-scrollbarSlider-background) transparent; +} + +.image-carousel-editor .main-image-container.zoomed::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.image-carousel-editor .main-image-container.zoomed::-webkit-scrollbar-thumb { + background-color: var(--vscode-scrollbarSlider-background); +} + +.image-carousel-editor .main-image-container.zoomed::-webkit-scrollbar-thumb:hover { + background-color: var(--vscode-scrollbarSlider-hoverBackground); +} + +.image-carousel-editor .main-image-container.zoomed::-webkit-scrollbar-thumb:active { + background-color: var(--vscode-scrollbarSlider-activeBackground); +} + +.image-carousel-editor .main-image { + border-radius: 4px; + margin: auto; +} + +.image-carousel-editor .video-container { + width: 100%; + height: 100%; + border-radius: 4px; + overflow: hidden; +} + +.image-carousel-editor .main-image.scale-to-fit { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.image-carousel-editor .main-image.pixelated { + image-rendering: pixelated; +} + +/* Overlay navigation arrows */ +.image-carousel-editor .nav-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-foreground); + border: none; + border-radius: 50%; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 1; +} + +.monaco-workbench.monaco-reduce-motion .image-carousel-editor .nav-arrow { + transition: none; +} +.image-carousel-editor .slideshow-container:hover .nav-arrow:not(:disabled) { + opacity: 0.8; +} + +.image-carousel-editor .slideshow-container:hover .nav-arrow:disabled { + opacity: 0.3; +} + +.image-carousel-editor .nav-arrow:hover:not(:disabled) { + opacity: 1 !important; + background: var(--vscode-toolbar-activeBackground); + color: var(--vscode-foreground); +} + +.image-carousel-editor .nav-arrow:focus-visible:not(:disabled) { + opacity: 1 !important; + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.image-carousel-editor .nav-arrow:disabled { + cursor: default; +} + +.image-carousel-editor .nav-arrow.prev-arrow { + left: 8px; +} + +.image-carousel-editor .nav-arrow.next-arrow { + right: 8px; +} + +/* Bottom bar: caption + counter + thumbnails */ +.image-carousel-editor .bottom-bar { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 4px 0 8px; + flex-shrink: 0; + min-width: 0; + max-width: 100%; +} + +/* Extra gap before thumbnails */ +.image-carousel-editor .bottom-bar .sections-container { + margin-top: 4px; +} + +/* Info bar: caption · counter */ +.image-carousel-editor .image-info-bar { + text-align: center; + color: var(--vscode-descriptionForeground); + font-size: 12px; + padding: 0 24px; + min-height: 16px; +} + +.image-carousel-editor .caption-separator::before { + content: '\00b7'; + margin: 0 6px; + opacity: 0.6; +} + +.image-carousel-editor .image-counter { + opacity: 0.7; +} + +/* Sections container — flat horizontal thumbnail strip */ +.image-carousel-editor .sections-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + overflow-x: auto; + justify-content: center; + justify-content: safe center; + padding: 0; + flex-shrink: 0; + min-width: 0; + max-width: 100%; + scrollbar-width: none; +} + +.image-carousel-editor .sections-container::-webkit-scrollbar { + display: none; +} + +/* Subtle separator between sections */ +.image-carousel-editor .thumbnail-separator { + width: 1px; + height: 24px; + background: var(--vscode-editorWidget-border, var(--vscode-widget-border)); + opacity: 0.4; + flex-shrink: 0; + margin: 0 6px; +} + +.image-carousel-editor .thumbnail { + width: 48px; + height: 48px; + border: 2px solid transparent; + border-radius: 4px; + overflow: hidden; + cursor: pointer; + transition: border-color 0.15s ease, opacity 0.15s ease; + flex-shrink: 0; + opacity: 0.6; + padding: 0; + background: none; +} + +.image-carousel-editor .thumbnail:hover { + opacity: 1; + border-color: var(--vscode-focusBorder); +} + +.image-carousel-editor .thumbnail.active { + opacity: 1; + border-color: var(--vscode-focusBorder); +} + +.image-carousel-editor .thumbnail.broken .thumbnail-image { + display: none; +} + +.image-carousel-editor .thumbnail.broken { + display: flex; + align-items: center; + justify-content: center; + background: var(--vscode-editor-background); +} + +.image-carousel-editor .thumbnail.broken::after { + font-family: codicon; + content: '\eaea'; /* file-media */ + font-size: 20px; + color: var(--vscode-descriptionForeground); +} + +.image-carousel-editor .thumbnail-image { + width: 100%; + height: 100%; + object-fit: cover; + pointer-events: none; +} + +/* Video thumbnail: play icon centered in a dark background */ +.image-carousel-editor .thumbnail.video-thumbnail { + display: flex; + align-items: center; + justify-content: center; + background: var(--vscode-editor-background); +} + +.image-carousel-editor .thumbnail-play-icon { + font-size: 20px; + color: var(--vscode-descriptionForeground); +} diff --git a/src/vs/workbench/contrib/imageCarousel/test/browser/imageCarousel.contribution.test.ts b/src/vs/workbench/contrib/imageCarousel/test/browser/imageCarousel.contribution.test.ts new file mode 100644 index 00000000000..37b32ac3077 --- /dev/null +++ b/src/vs/workbench/contrib/imageCarousel/test/browser/imageCarousel.contribution.test.ts @@ -0,0 +1,464 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { NullFilesConfigurationService, createFileStat } from '../../../../test/common/workbenchTestServices.js'; +import { IExplorerService } from '../../../files/browser/files.js'; +import { ExplorerItem } from '../../../files/common/explorerModel.js'; +import { IFileService, IFileStat, IFileContent } from '../../../../../platform/files/common/files.js'; +import { IEditorService, MODAL_GROUP } from '../../../../services/editor/common/editorService.js'; +import { ImageCarouselEditorInput } from '../../browser/imageCarouselEditorInput.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { INotificationService } from '../../../../../platform/notification/common/notification.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; + +// Importing the contribution registers the actions +import '../../browser/imageCarousel.contribution.js'; + +function createExplorerItem( + path: string, + isFolder: boolean, + fileService: IFileService, + configService: TestConfigurationService, + parent?: ExplorerItem, +): ExplorerItem { + return new ExplorerItem( + URI.file(path), + fileService, + configService, + NullFilesConfigurationService, + parent, + isFolder, + ); +} + +suite('OpenImagesInCarouselFromExplorerAction', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let configService: TestConfigurationService; + let openedInputs: { input: ImageCarouselEditorInput; group: typeof MODAL_GROUP }[]; + let infoMessages: string[]; + let errorMessages: string[]; + + setup(() => { + openedInputs = []; + infoMessages = []; + errorMessages = []; + configService = new TestConfigurationService(); + instantiationService = workbenchInstantiationService(undefined, disposables); + }); + + function stubFileService(resolveMap: Map, fileContents: Map): void { + instantiationService.stub(IFileService, 'resolve', async (resource: URI) => { + const stat = resolveMap.get(resource.path); + if (!stat) { + throw new Error(`File not found: ${resource.path}`); + } + return stat; + }); + + instantiationService.stub(IFileService, 'readFile', async (resource: URI) => { + const content = fileContents.get(resource.path); + if (!content) { + throw new Error(`Cannot read: ${resource.path}`); + } + return { resource, value: content } as IFileContent; + }); + } + + function stubExplorerService(items: ExplorerItem[]): void { + instantiationService.stub(IExplorerService, { + getContext: () => items, + }); + } + + function stubEditorService(): void { + instantiationService.stub(IEditorService, 'openEditor', async (input: unknown, _options: unknown, group: unknown) => { + if (input instanceof ImageCarouselEditorInput) { + openedInputs.push({ input, group: group as typeof MODAL_GROUP }); + disposables.add(input); + } + return undefined; + }); + } + + function stubNotificationService(): void { + instantiationService.stub(INotificationService, 'info', (message: string) => { + infoMessages.push(message); + }); + instantiationService.stub(INotificationService, 'error', (message: string) => { + errorMessages.push(message); + }); + } + + test('single image file opens carousel with sibling images', async () => { + const fileService = instantiationService.get(IFileService); + const parent = createExplorerItem('/workspace/images', true, fileService, configService); + const imageItem = createExplorerItem('/workspace/images/photo.png', false, fileService, configService, parent); + + const pngData = VSBuffer.fromString('fake-png'); + const jpgData = VSBuffer.fromString('fake-jpg'); + const txtData = VSBuffer.fromString('text file'); + + const resolveMap = new Map(); + resolveMap.set('/workspace/images', createFileStat( + URI.file('/workspace/images'), false, false, true, false, [ + { resource: URI.file('/workspace/images/photo.png'), isFile: true }, + { resource: URI.file('/workspace/images/other.jpg'), isFile: true }, + { resource: URI.file('/workspace/images/readme.txt'), isFile: true }, + { resource: URI.file('/workspace/images/subfolder'), isDirectory: true, isFile: false }, + ] + )); + + const fileContents = new Map(); + fileContents.set('/workspace/images/photo.png', pngData); + fileContents.set('/workspace/images/other.jpg', jpgData); + fileContents.set('/workspace/images/readme.txt', txtData); + + stubFileService(resolveMap, fileContents); + stubExplorerService([imageItem]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command, 'Command should be registered'); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 1, 'Should open one editor'); + const input = openedInputs[0].input; + assert.strictEqual(input.collection.sections.length, 1); + + const images = input.collection.sections[0].images; + assert.strictEqual(images.length, 2, 'Should include 2 image siblings (png + jpg), not txt'); + // Images are sorted by basename: other.jpg before photo.png + assert.strictEqual(images[0].name, 'other.jpg'); + assert.strictEqual(images[1].name, 'photo.png'); + + // Start index should be the selected image (photo.png = index 1 after sorting) + assert.strictEqual(input.startIndex, 1); + }); + + test('folder opens carousel with all contained images', async () => { + const fileService = instantiationService.get(IFileService); + const folderItem = createExplorerItem('/workspace/images', true, fileService, configService); + + const gifData = VSBuffer.fromString('fake-gif'); + const webpData = VSBuffer.fromString('fake-webp'); + + const resolveMap = new Map(); + resolveMap.set('/workspace/images', createFileStat( + URI.file('/workspace/images'), false, false, true, false, [ + { resource: URI.file('/workspace/images/anim.gif'), isFile: true }, + { resource: URI.file('/workspace/images/photo.webp'), isFile: true }, + { resource: URI.file('/workspace/images/script.js'), isFile: true }, + ] + )); + + const fileContents = new Map(); + fileContents.set('/workspace/images/anim.gif', gifData); + fileContents.set('/workspace/images/photo.webp', webpData); + + stubFileService(resolveMap, fileContents); + stubExplorerService([folderItem]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 1); + const images = openedInputs[0].input.collection.sections[0].images; + assert.strictEqual(images.length, 2, 'Should include 2 images (gif + webp), not js'); + assert.strictEqual(images[0].name, 'anim.gif'); + assert.strictEqual(images[1].name, 'photo.webp'); + }); + + test('multiple selected images open in carousel', async () => { + const fileService = instantiationService.get(IFileService); + const img1 = createExplorerItem('/workspace/a.png', false, fileService, configService); + const img2 = createExplorerItem('/workspace/b.svg', false, fileService, configService); + const txtFile = createExplorerItem('/workspace/notes.txt', false, fileService, configService); + + const pngData = VSBuffer.fromString('fake-png'); + const svgData = VSBuffer.fromString(''); + + const resolveMap = new Map(); + + const fileContents = new Map(); + fileContents.set('/workspace/a.png', pngData); + fileContents.set('/workspace/b.svg', svgData); + + stubFileService(resolveMap, fileContents); + stubExplorerService([img1, img2, txtFile]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 1); + const images = openedInputs[0].input.collection.sections[0].images; + assert.strictEqual(images.length, 2, 'Should include only image files'); + assert.strictEqual(images[0].name, 'a.png'); + assert.strictEqual(images[1].name, 'b.svg'); + }); + + test('empty selection with resource argument opens carousel from that folder', async () => { + const pngData = VSBuffer.fromString('fake-png'); + const jpgData = VSBuffer.fromString('fake-jpg'); + + const folderUri = URI.file('/workspace/photos'); + const resolveMap = new Map(); + resolveMap.set('/workspace/photos', createFileStat( + folderUri, false, false, true, false, [ + { resource: URI.file('/workspace/photos/sunset.png'), isFile: true }, + { resource: URI.file('/workspace/photos/mountain.jpg'), isFile: true }, + { resource: URI.file('/workspace/photos/notes.txt'), isFile: true }, + ] + )); + + const fileContents = new Map(); + fileContents.set('/workspace/photos/sunset.png', pngData); + fileContents.set('/workspace/photos/mountain.jpg', jpgData); + + stubFileService(resolveMap, fileContents); + stubExplorerService([]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + // Pass the folder URI as the resource argument (as explorer does for empty-space click) + await instantiationService.invokeFunction(command.handler, folderUri); + + assert.strictEqual(openedInputs.length, 1, 'Should open carousel using resource argument fallback'); + const images = openedInputs[0].input.collection.sections[0].images; + assert.strictEqual(images.length, 2, 'Should include 2 images from the folder'); + }); + + test('empty selection without resource falls back to first workspace folder', async () => { + const pngData = VSBuffer.fromString('fake-png'); + + // Derive the workspace root from IWorkspaceContextService so the test + // works on all platforms (the path differs on Windows vs Unix). + const contextService = instantiationService.get(IWorkspaceContextService); + const wsRoot = contextService.getWorkspace().folders[0].uri; + const logoUri = URI.joinPath(wsRoot, 'logo.png'); + const readmeUri = URI.joinPath(wsRoot, 'readme.md'); + + const resolveMap = new Map(); + resolveMap.set(wsRoot.path, createFileStat( + wsRoot, false, false, true, false, [ + { resource: logoUri, isFile: true }, + { resource: readmeUri, isFile: true }, + ] + )); + + const fileContents = new Map(); + fileContents.set(logoUri.path, pngData); + + stubFileService(resolveMap, fileContents); + stubExplorerService([]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + // No resource argument — should fall back to workspace root + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 1, 'Should open carousel using workspace root fallback'); + const images = openedInputs[0].input.collection.sections[0].images; + assert.strictEqual(images.length, 1, 'Should include image from workspace root'); + assert.strictEqual(images[0].name, 'logo.png'); + }); + + test('empty selection with no images shows notification', async () => { + const folderUri = URI.file('/workspace/docs'); + const resolveMap = new Map(); + resolveMap.set('/workspace/docs', createFileStat( + folderUri, false, false, true, false, [ + { resource: URI.file('/workspace/docs/readme.md'), isFile: true }, + ] + )); + + stubFileService(resolveMap, new Map()); + stubExplorerService([]); + stubEditorService(); + stubNotificationService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler, folderUri); + + assert.strictEqual(openedInputs.length, 0, 'Should not open carousel when folder has no images'); + assert.strictEqual(infoMessages.length, 1, 'Should show notification'); + }); + + test('folder with no images shows notification', async () => { + const fileService = instantiationService.get(IFileService); + const folderItem = createExplorerItem('/workspace/docs', true, fileService, configService); + + const resolveMap = new Map(); + resolveMap.set('/workspace/docs', createFileStat( + URI.file('/workspace/docs'), false, false, true, false, [ + { resource: URI.file('/workspace/docs/readme.md'), isFile: true }, + { resource: URI.file('/workspace/docs/notes.txt'), isFile: true }, + ] + )); + + stubFileService(resolveMap, new Map()); + stubExplorerService([folderItem]); + stubEditorService(); + stubNotificationService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 0, 'Should not open carousel when folder has no images'); + assert.strictEqual(infoMessages.length, 1, 'Should show notification about no images'); + }); + + test('folder read error shows error notification', async () => { + const fileService = instantiationService.get(IFileService); + const folderItem = createExplorerItem('/workspace/restricted', true, fileService, configService); + + // resolve throws to simulate a permission error + const resolveMap = new Map(); + stubFileService(resolveMap, new Map()); + stubExplorerService([folderItem]); + stubEditorService(); + stubNotificationService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 0, 'Should not open carousel on folder read error'); + assert.strictEqual(errorMessages.length, 1, 'Should show error notification'); + assert.strictEqual(infoMessages.length, 0, 'Should not show info notification'); + }); + + test('images with URIs are passed lazily without reading file contents', async () => { + const folderUri = URI.file('/workspace/broken'); + + const resolveMap = new Map(); + resolveMap.set('/workspace/broken', createFileStat( + folderUri, false, false, true, false, [ + { resource: URI.file('/workspace/broken/corrupt.png'), isFile: true }, + { resource: URI.file('/workspace/broken/missing.jpg'), isFile: true }, + ] + )); + + // No file contents — with lazy loading, no readFile should be called at action time + let readFileCallCount = 0; + stubFileService(resolveMap, new Map()); + instantiationService.stub(IFileService, 'readFile', async () => { + readFileCallCount++; + throw new Error('readFile should not be called'); + }); + stubExplorerService([]); + stubEditorService(); + stubNotificationService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler, folderUri); + + assert.strictEqual(readFileCallCount, 0, 'readFile should not be called during action'); + assert.strictEqual(openedInputs.length, 1, 'Should open carousel with lazy image entries'); + const images = openedInputs[0].input.collection.sections[0].images; + assert.strictEqual(images.length, 2, 'Should include 2 lazy image entries'); + assert.strictEqual(images[0].data, undefined, 'Image data should not be loaded eagerly'); + assert.ok(images[0].uri, 'Image should have a URI for lazy loading'); + }); + + test('folder includes video files alongside images', async () => { + const fileService = instantiationService.get(IFileService); + const folderItem = createExplorerItem('/workspace/media', true, fileService, configService); + + const resolveMap = new Map(); + resolveMap.set('/workspace/media', createFileStat( + URI.file('/workspace/media'), false, false, true, false, [ + { resource: URI.file('/workspace/media/clip.mp4'), isFile: true }, + { resource: URI.file('/workspace/media/photo.png'), isFile: true }, + { resource: URI.file('/workspace/media/demo.webm'), isFile: true }, + { resource: URI.file('/workspace/media/readme.txt'), isFile: true }, + ] + )); + + stubFileService(resolveMap, new Map()); + stubExplorerService([folderItem]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 1); + const images = openedInputs[0].input.collection.sections[0].images; + assert.strictEqual(images.length, 3, 'Should include mp4 + png + webm, not txt'); + assert.strictEqual(images[0].name, 'clip.mp4'); + assert.strictEqual(images[1].name, 'demo.webm'); + assert.strictEqual(images[2].name, 'photo.png'); + }); + + test('single video file opens carousel with sibling media', async () => { + const fileService = instantiationService.get(IFileService); + const parent = createExplorerItem('/workspace/media', true, fileService, configService); + const videoItem = createExplorerItem('/workspace/media/clip.mp4', false, fileService, configService, parent); + + const resolveMap = new Map(); + resolveMap.set('/workspace/media', createFileStat( + URI.file('/workspace/media'), false, false, true, false, [ + { resource: URI.file('/workspace/media/clip.mp4'), isFile: true }, + { resource: URI.file('/workspace/media/photo.png'), isFile: true }, + { resource: URI.file('/workspace/media/notes.txt'), isFile: true }, + ] + )); + + stubFileService(resolveMap, new Map()); + stubExplorerService([videoItem]); + stubEditorService(); + + const { CommandsRegistry } = await import('../../../../../platform/commands/common/commands.js'); + const command = CommandsRegistry.getCommand('workbench.action.openImagesInCarousel'); + assert.ok(command); + + await instantiationService.invokeFunction(command.handler); + + assert.strictEqual(openedInputs.length, 1); + const input = openedInputs[0].input; + const images = input.collection.sections[0].images; + assert.strictEqual(images.length, 2, 'Should include mp4 + png siblings'); + assert.strictEqual(images[0].name, 'clip.mp4'); + assert.strictEqual(images[1].name, 'photo.png'); + assert.strictEqual(input.startIndex, 0, 'Start index should point to the selected video'); + }); +}); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 85237c14100..acb788bced6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -17,6 +17,7 @@ import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatEscapeToolContribution, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; +import { IInlineChatHistoryService, InlineChatHistoryService } from './inlineChatHistoryService.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { CancelAction, ChatSubmitAction } from '../../chat/browser/actions/chatExecuteActions.js'; import { localize } from '../../../../nls.js'; @@ -30,10 +31,13 @@ registerAction2(InlineChatActions.KeepSessionAction2); registerAction2(InlineChatActions.UndoSessionAction2); registerAction2(InlineChatActions.UndoAndCloseSessionAction2); registerAction2(InlineChatActions.CancelSessionAction); +registerAction2(InlineChatActions.ContinueInlineChatInChatViewAction); +registerAction2(InlineChatActions.RephraseInlineChatSessionAction); // --- browser registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); +registerSingleton(IInlineChatHistoryService, InlineChatHistoryService, InstantiationType.Delayed); // --- MENU special --- diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index aef9aeefc52..d4bfb599f0c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, ACTION_ASK_IN_CHAT, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_PENDING_CONFIRMATION, InlineChatConfigKeys, CTX_FIX_DIAGNOSTICS_ENABLED, CTX_INLINE_CHAT_AFFORDANCE_VISIBLE } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, ACTION_ASK_IN_CHAT, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_PENDING_CONFIRMATION, CTX_INLINE_CHAT_TERMINATED, InlineChatConfigKeys, CTX_FIX_DIAGNOSTICS_ENABLED, CTX_INLINE_CHAT_AFFORDANCE_VISIBLE, CTX_ASK_IN_CHAT_ENABLED } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; @@ -29,7 +29,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IChatEditingService } from '../../chat/common/editing/chatEditingService.js'; import { IChatWidgetService } from '../../chat/browser/chat.js'; import { ChatRequestQueueKind } from '../../chat/common/chatService/chatService.js'; - +import { ChatEntitlementContextKeys } from '../../../services/chat/common/chatEntitlementService.js'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -53,9 +53,13 @@ export class StartSessionAction extends Action2 { shortTitle: localize2('runShort', 'Inline Chat'), category: AbstractInlineChatAction.category, f1: true, - precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + precondition: ContextKeyExpr.and(inlineChatContextKey, ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate())), keybinding: { - when: EditorContextKeys.focus, + when: ContextKeyExpr.and( + EditorContextKeys.focus, + inlineChatContextKey, + ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate()) + ), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyI }, @@ -64,7 +68,7 @@ export class StartSessionAction extends Action2 { id: MenuId.EditorContext, group: '1_chat', order: 3, - when: inlineChatContextKey + when: ContextKeyExpr.and(inlineChatContextKey, ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate())) }, { id: MenuId.ChatTitleBarMenu, group: 'a_open', @@ -130,7 +134,7 @@ export class StartSessionAction extends Action2 { MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { group: '0_chat', order: 1, - when: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + when: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection, ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate()), ChatEntitlementContextKeys.Setup.hidden.negate()), command: { id: ACTION_START, title: localize('editCode', "Ask for Edits"), @@ -250,6 +254,12 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction { group: '2_chat', order: 1, when: ContextKeyExpr.and(CTX_FIX_DIAGNOSTICS_ENABLED, EditorContextKeys.selectionHasDiagnostics, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + }, { + id: MenuId.MarkerHoverStatusBar, + group: '1_fix', + order: 1, + when: ContextKeyExpr.and(CTX_FIX_DIAGNOSTICS_ENABLED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + precondition: null, }] }); } @@ -399,6 +409,49 @@ export class CancelSessionAction extends KeepOrUndoSessionAction { } } +export class ContinueInlineChatInChatViewAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat2.continueInChat', + title: localize2('continueInChat', "Ask in Chat"), + icon: Codicon.chatSparkle, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_HOVER_MODE, CTX_INLINE_CHAT_TERMINATED), + menu: [{ + id: MenuId.ChatEditorInlineExecute, + group: 'navigation', + order: 2, + when: ContextKeyExpr.and(CTX_HOVER_MODE, CTX_INLINE_CHAT_TERMINATED) + }] + }); + } + + override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor): Promise { + await ctrl.continueSessionInChat(); + } +} + +export class RephraseInlineChatSessionAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat2.rephrase', + title: localize2('rephrase', "Rephrase"), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_HOVER_MODE, CTX_INLINE_CHAT_TERMINATED), + menu: [{ + id: MenuId.ChatEditorInlineExecute, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(CTX_HOVER_MODE, CTX_INLINE_CHAT_TERMINATED) + }] + }); + } + + override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor): Promise { + await ctrl.rephraseSession(); + } +} + export class SubmitInlineChatInputAction extends AbstractInlineChatAction { constructor() { @@ -406,9 +459,9 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { id: 'inlineChat.submitInput', title: localize2('submitInput', "Send"), icon: Codicon.send, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate())), keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate())), weight: KeybindingWeight.EditorCore + 10, primary: KeyCode.Enter }, @@ -416,7 +469,7 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { id: MenuId.InlineChatInput, group: '0_main', order: 1, - when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate() + when: ContextKeyExpr.or(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate(), CTX_ASK_IN_CHAT_ENABLED.negate()) }] }); } @@ -424,6 +477,7 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: unknown[]): void { const value = ctrl.inputWidget.value; if (value) { + ctrl.inputWidget.addToHistory(value); ctrl.inputWidget.hide(); ctrl.run({ message: value, autoSend: true }); } @@ -459,7 +513,7 @@ export class AskInChatAction extends EditorAction2 { title: localize2('askInChat', 'Ask in Chat'), category: AbstractInlineChatAction.category, f1: true, - precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_ASK_IN_CHAT_ENABLED), keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, @@ -470,12 +524,12 @@ export class AskInChatAction extends EditorAction2 { id: MenuId.EditorContext, group: '1_chat', order: 3, - when: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + when: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_ASK_IN_CHAT_ENABLED) }, { id: MenuId.InlineChatEditorAffordance, group: '0_chat', order: 1, - when: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + when: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_ASK_IN_CHAT_ENABLED) }] }); } @@ -521,9 +575,9 @@ export class QueueInChatAction extends AbstractInlineChatAction { id: 'inlineChat.queueInChat', title: localize2('queueInChat', "Queue in Chat"), icon: Codicon.arrowUp, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_ASK_IN_CHAT_ENABLED), keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_ASK_IN_CHAT_ENABLED), weight: KeybindingWeight.EditorCore + 10, primary: KeyCode.Enter }, @@ -531,7 +585,7 @@ export class QueueInChatAction extends AbstractInlineChatAction { id: MenuId.InlineChatInput, group: '0_main', order: 1, - when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_ASK_IN_CHAT_ENABLED), }] }); } @@ -544,6 +598,9 @@ export class QueueInChatAction extends AbstractInlineChatAction { } const value = ctrl.inputWidget.value; + if (value) { + ctrl.inputWidget.addToHistory(value); + } ctrl.inputWidget.hide(); if (!value) { return; @@ -563,6 +620,6 @@ export class QueueInChatAction extends AbstractInlineChatAction { if (selection && !selection.isEmpty()) { await widget.attachmentModel.addFile(editor.getModel().uri, selection); } - await widget.acceptInput(value, { alwaysQueue: true, queue: ChatRequestQueueKind.Queued }); + await widget.acceptInput(value, { queue: ChatRequestQueueKind.Queued }); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index a5f6a2ad803..9b849b7de8b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -45,7 +45,7 @@ export class InlineChatAffordance extends Disposable { readonly #editor: ICodeEditor; readonly #inputWidget: InlineChatInputWidget; readonly #instantiationService: IInstantiationService; - readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number; placeholder: string } | undefined>(this, undefined); + readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number; placeholder: string; value?: string } | undefined>(this, undefined); readonly #selectionData = observableValue(this, undefined); constructor( @@ -175,7 +175,7 @@ export class InlineChatAffordance extends Disposable { const left = data.rect.left - editorRect.left; // Show the overlay widget - this.#inputWidget.show(data.lineNumber, left, data.above, data.placeholder); + this.#inputWidget.show(data.lineNumber, left, data.above, data.placeholder, data.value); })); this._store.add(autorun(r => { @@ -190,7 +190,7 @@ export class InlineChatAffordance extends Disposable { this.#selectionData.set(undefined, undefined); } - async showMenuAtSelection(placeholder: string): Promise { + async showMenuAtSelection(placeholder: string, value?: string): Promise { assertType(this.#editor.hasModel()); const direction = this.#editor.getSelection().getDirection(); @@ -205,7 +205,8 @@ export class InlineChatAffordance extends Disposable { rect: new DOMRect(x, y, 0, scrolledPosition.height), above: direction === SelectionDirection.RTL, lineNumber: position.lineNumber, - placeholder + placeholder, + value }, undefined); await waitForState(this.#inputWidget.position, pos => pos === null); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index d26f9270bdd..00280f3828d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -51,10 +51,10 @@ import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../. import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; -import { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_PENDING_CONFIRMATION, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_PENDING_CONFIRMATION, CTX_INLINE_CHAT_TERMINATED, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js'; -import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; +import { continueInPanelChat, IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -158,6 +158,7 @@ export class InlineChatController implements IEditorContribution { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const ctxPendingConfirmation = CTX_INLINE_CHAT_PENDING_CONFIRMATION.bindTo(contextKeyService); + const ctxTerminated = CTX_INLINE_CHAT_TERMINATED.bindTo(contextKeyService); const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); @@ -321,6 +322,11 @@ export class InlineChatController implements IEditorContribution { : localize('placeholderWithSelection', "Modify selected code"); }); + this._store.add(autorun(r => { + const session = visibleSessionObs.read(r); + ctxTerminated.set(!!session?.terminationState.read(r)); + })); + this._store.add(autorun(r => { @@ -363,12 +369,13 @@ export class InlineChatController implements IEditorContribution { const isInProgress = lastRequest?.response?.isInProgress.read(r); const isPendingConfirmation = !!lastRequest?.response?.isPendingConfirmation.read(r); const isError = !!lastRequest?.response?.result?.errorDetails; + const isTerminated = !!session.terminationState.read(r); ctxPendingConfirmation.set(isPendingConfirmation); const entry = session.editingSession.readEntry(session.uri, r); // When there's no entry (no changes made) and the response is complete, the widget should be hidden. // When there's an entry in Modified state, it needs to be settled (accepted/rejected). const isNotSettled = entry ? entry.state.read(r) === ModifiedFileEntryState.Modified : false; - if (isInProgress || isNotSettled || isPendingConfirmation || isError) { + if (isInProgress || isNotSettled || isPendingConfirmation || isError || isTerminated) { sessionOverlayWidget.show(session); } else { sessionOverlayWidget.hide(); @@ -415,7 +422,9 @@ export class InlineChatController implements IEditorContribution { this._store.add(autorun(r => { + const session = visibleSessionObs.read(r); const response = lastResponseObs.read(r); + const terminationState = session?.terminationState.read(r); this._zone.rawValue?.widget.updateInfo(''); @@ -425,6 +434,8 @@ export class InlineChatController implements IEditorContribution { // ERROR case this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); alert(response.result.errorDetails.message); + } else if (terminationState) { + this._zone.rawValue?.widget.updateInfo(`$(info) ${renderAsPlaintext(terminationState)}`); } // no response or not in progress @@ -610,6 +621,35 @@ export class InlineChatController implements IEditorContribution { session.dispose(); } + async continueSessionInChat(): Promise { + const session = this._currentSession.get(); + if (!session) { + return; + } + + await this._instaService.invokeFunction(continueInPanelChat, session); + } + + async rephraseSession(): Promise { + const session = this._currentSession.get(); + if (!session) { + return; + } + + const requestText = session.chatModel.getRequests().at(-1)?.message.text; + session.dispose(); + + if (!requestText) { + return; + } + + const selection = this._editor.getSelection(); + const placeholder = selection && !selection.isEmpty() + ? localize('placeholderWithSelectionHover', "Describe how to change this") + : localize('placeholderNoSelectionHover', "Describe what to generate"); + await this.inputOverlayWidget.showMenuAtSelection(placeholder, requestText); + } + private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise { const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get(); if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index e7773395fce..0be9f4f0502 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -192,6 +192,19 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi this._hide(); } })); + + this._store.add(this._editor.onDidScrollChange(() => { + const sel = selection.get(); + if (!sel) { + return; + } + const isInViewport = this._isPositionInViewport(); + if (isInViewport && !this._isVisible) { + this._show(sel); + } else if (!isInViewport && this._isVisible) { + this._hide(); + } + })); } private _show(selection: Selection): void { @@ -260,6 +273,30 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi }; } + private _isPositionInViewport(): boolean { + const widgetPosition = this._position?.position; + if (!widgetPosition) { + return false; + } + + // Check vertical visibility + const visibleRanges = this._editor.getVisibleRanges(); + const isLineVisible = visibleRanges.some(range => + widgetPosition.lineNumber >= range.startLineNumber && widgetPosition.lineNumber <= range.endLineNumber + ); + if (!isLineVisible) { + return false; + } + + // Check horizontal visibility + const scrolledPos = this._editor.getScrolledVisiblePosition(widgetPosition); + if (!scrolledPos) { + return false; + } + const layoutInfo = this._editor.getOptions().get(EditorOption.layoutInfo); + return scrolledPos.left >= 0 && scrolledPos.left <= layoutInfo.width; + } + private _hide(): void { if (this._isVisible) { this._isVisible = false; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts new file mode 100644 index 00000000000..3b501b8a03e --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HistoryNavigator2 } from '../../../../base/common/history.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +export const IInlineChatHistoryService = createDecorator('IInlineChatHistoryService'); + +export interface IInlineChatHistoryService { + readonly _serviceBrand: undefined; + + addToHistory(value: string): void; + previousValue(): string | undefined; + nextValue(): string | undefined; + isAtEnd(): boolean; + replaceLast(value: string): void; + resetCursor(): void; +} + +const _storageKey = 'inlineChat.history'; +const _capacity = 50; + +export class InlineChatHistoryService extends Disposable implements IInlineChatHistoryService { + declare readonly _serviceBrand: undefined; + + private readonly _history: HistoryNavigator2; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + ) { + super(); + + const raw = this._storageService.get(_storageKey, StorageScope.PROFILE); + let entries: string[] = ['']; + if (raw) { + try { + const parsed: string[] = JSON.parse(raw); + if (Array.isArray(parsed) && parsed.length > 0) { + entries = parsed; + // Ensure there's always an empty uncommitted entry at the end + if (entries[entries.length - 1] !== '') { + entries.push(''); + } + } + } catch { + // ignore invalid data + } + } + + this._history = new HistoryNavigator2(entries, _capacity); + + this._store.add(this._storageService.onWillSaveState(() => { + this._saveToStorage(); + })); + } + + private _saveToStorage(): void { + const values = [...this._history].filter(v => v.length > 0); + if (values.length === 0) { + this._storageService.remove(_storageKey, StorageScope.PROFILE); + } else { + this._storageService.store(_storageKey, JSON.stringify(values), StorageScope.PROFILE, StorageTarget.USER); + } + } + + addToHistory(value: string): void { + this._history.replaceLast(value); + this._history.add(''); + } + + previousValue(): string | undefined { + return this._history.previous(); + } + + nextValue(): string | undefined { + return this._history.next(); + } + + isAtEnd(): boolean { + return this._history.isAtEnd(); + } + + replaceLast(value: string): void { + this._history.replaceLast(value); + } + + resetCursor(): void { + this._history.resetCursor(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 04a76d7327a..3675acc488e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -7,13 +7,16 @@ import './media/inlineChatOverlayWidget.css'; import * as dom from '../../../../base/browser/dom.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { renderAsPlaintext, renderMarkdown } from '../../../../base/browser/markdownRenderer.js'; import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { BaseActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, constObservable, derived, IObservable, observableFromEvent, observableFromEventOpts, observableValue } from '../../../../base/common/observable.js'; +import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; @@ -38,6 +41,7 @@ import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOpt import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; import { IInlineChatSession2 } from './inlineChatSessionService.js'; import { assertType } from '../../../../base/common/types.js'; +import { IInlineChatHistoryService } from './inlineChatHistoryService.js'; /** * Overlay widget that displays a vertical action bar menu. @@ -59,7 +63,6 @@ export class InlineChatInputWidget extends Disposable { private _anchorLeft: number = 0; private _anchorAbove: boolean = false; - constructor( private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -67,6 +70,7 @@ export class InlineChatInputWidget extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, + @IInlineChatHistoryService private readonly _historyService: IInlineChatHistoryService, ) { super(); @@ -158,9 +162,17 @@ export class InlineChatInputWidget extends Disposable { const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); const minWidth = 220; const maxWidth = 600; - const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' - ? maxWidth - : Math.max(minWidth, Math.min(totalWidth, maxWidth)); + const midWidth = Math.round(maxWidth / 1.618); + let clampedWidth: number; + if (this._input.getOption(EditorOption.wordWrap) === 'on') { + clampedWidth = maxWidth; + } else if (totalWidth <= minWidth) { + clampedWidth = minWidth; + } else if (totalWidth <= midWidth) { + clampedWidth = midWidth; + } else { + clampedWidth = maxWidth; + } const lineHeight = this._input.getOption(EditorOption.lineHeight); const clampedHeight = Math.min(contentHeight.read(r), (3 * lineHeight)); @@ -211,15 +223,28 @@ export class InlineChatInputWidget extends Disposable { this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); this._store.add(toDisposable(() => inputWidgetFocused.reset())); - // Handle key events: ArrowDown to move to actions + // Handle key events: ArrowUp/ArrowDown for history navigation and action bar focus this._store.add(this._input.onKeyDown(e => { - if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { + if (e.keyCode === KeyCode.UpArrow) { + const position = this._input.getPosition(); + if (position && position.lineNumber === 1) { + this._showPreviousHistoryValue(); + e.preventDefault(); + e.stopPropagation(); + } + } else if (e.keyCode === KeyCode.DownArrow) { const model = this._input.getModel(); const position = this._input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { - e.preventDefault(); - e.stopPropagation(); - actionBar.focus(0); + if (!this._historyService.isAtEnd()) { + this._showNextHistoryValue(); + e.preventDefault(); + e.stopPropagation(); + } else if (!actionBar.isEmpty()) { + e.preventDefault(); + e.stopPropagation(); + actionBar.focus(0); + } } } })); @@ -251,18 +276,45 @@ export class InlineChatInputWidget extends Disposable { return this._input.getModel().getValue().trim(); } + addToHistory(value: string): void { + this._historyService.addToHistory(value); + } + + private _showPreviousHistoryValue(): void { + if (this._historyService.isAtEnd()) { + this._historyService.replaceLast(this._input.getModel().getValue()); + } + const value = this._historyService.previousValue(); + if (value !== undefined) { + this._input.getModel().setValue(value); + } + } + + private _showNextHistoryValue(): void { + if (this._historyService.isAtEnd()) { + return; + } + const value = this._historyService.nextValue(); + if (value !== undefined) { + this._input.getModel().setValue(value); + } + } + /** * Show the widget at the specified line. * @param lineNumber The line number to anchor the widget to * @param left Left offset relative to editor * @param anchorAbove Whether to anchor above the position (widget grows upward) */ - show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { + show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string, value?: string): void { this._showStore.clear(); + // Reset history cursor to the end (current uncommitted text) + this._historyService.resetCursor(); + // Clear input state this._input.updateOptions({ wordWrap: 'off', placeholder }); - this._input.getModel().setValue(''); + this._input.getModel().setValue(value ?? ''); // Store anchor info for scroll updates this._anchorLineNumber = lineNumber; @@ -300,7 +352,12 @@ export class InlineChatInputWidget extends Disposable { })); // Focus the input editor - setTimeout(() => this._input.focus(), 0); + setTimeout(() => { + this._input.focus(); + if (value) { + this._input.setSelection(this._input.getModel().getFullModelRange()); + } + }, 0); } private _updatePosition(): void { @@ -355,6 +412,10 @@ export class InlineChatSessionOverlayWidget extends Disposable { private readonly _domNode: HTMLElement = document.createElement('div'); private readonly _container: HTMLElement; + private readonly _markdownContainer: HTMLElement; + private readonly _markdownMessage: HTMLElement; + private readonly _markdownScrollable: DomScrollableElement; + private readonly _contentRow: HTMLElement; private readonly _statusNode: HTMLElement; private readonly _icon: HTMLElement; private readonly _message: HTMLElement; @@ -380,12 +441,29 @@ export class InlineChatSessionOverlayWidget extends Disposable { this._domNode.appendChild(this._container); this._container.classList.add('inline-chat-session-overlay-container'); + this._markdownContainer = document.createElement('div'); + this._markdownContainer.classList.add('markdown-scroll-container'); + + this._markdownMessage = document.createElement('div'); + this._markdownMessage.classList.add('markdown-message'); + this._markdownContainer.appendChild(this._markdownMessage); + this._markdownScrollable = this._store.add(new DomScrollableElement(this._markdownContainer, { + consumeMouseWheelIfScrollbarIsNeeded: true, + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Auto, + })); + this._container.appendChild(this._markdownScrollable.getDomNode()); + + this._contentRow = document.createElement('div'); + this._contentRow.classList.add('content-row'); + this._container.appendChild(this._contentRow); + // Create status node with icon and message this._statusNode = document.createElement('div'); this._statusNode.classList.add('status'); this._icon = dom.append(this._statusNode, dom.$('span')); this._message = dom.append(this._statusNode, dom.$('span.message')); - this._container.appendChild(this._statusNode); + this._contentRow.appendChild(this._statusNode); // Create toolbar node this._toolbarNode = document.createElement('div'); @@ -410,6 +488,14 @@ export class InlineChatSessionOverlayWidget extends Disposable { return undefined; } + const terminationState = session.terminationState.read(r); + if (terminationState) { + return { + markdown: terminationState, + icon: Codicon.info + }; + } + const response = chatModel.lastRequestObs.read(r)?.response; if (!response) { return { message: localize('working', "Working..."), icon: ThemeIcon.modify(Codicon.loading, 'spin') }; @@ -458,15 +544,42 @@ export class InlineChatSessionOverlayWidget extends Disposable { } }); + const markdownStore = this._showStore.add(new DisposableStore()); + this._showStore.add(autorun(r => { const value = requestMessage.read(r); if (value) { - this._message.innerText = renderAsPlaintext(value.message); - this._icon.className = ''; - this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); + if (value.message && value.icon) { + this._message.innerText = renderAsPlaintext(value.message); + this._icon.className = ''; + this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); + this._statusNode.classList.remove('hidden'); + this._contentRow.classList.remove('status-hidden'); + } else { + this._message.innerText = ''; + this._icon.className = ''; + this._statusNode.classList.add('hidden'); + this._contentRow.classList.add('status-hidden'); + } + markdownStore.clear(); + this._markdownMessage.replaceChildren(); + if (value.markdown) { + this._markdownScrollable.getDomNode().classList.remove('hidden'); + const markdown = typeof value.markdown === 'string' ? new MarkdownString(value.markdown) : value.markdown; + const rendered = markdownStore.add(renderMarkdown(markdown)); + this._markdownMessage.appendChild(rendered.element); + this._markdownScrollable.scanDomNode(); + } else { + this._markdownScrollable.getDomNode().classList.add('hidden'); + } } else { this._message.innerText = ''; this._icon.className = ''; + this._statusNode.classList.add('hidden'); + this._contentRow.classList.add('status-hidden'); + markdownStore.clear(); + this._markdownMessage.replaceChildren(); + this._markdownScrollable.getDomNode().classList.add('hidden'); } })); @@ -480,7 +593,7 @@ export class InlineChatSessionOverlayWidget extends Disposable { })); // Add toolbar - this._container.appendChild(this._toolbarNode); + this._contentRow.appendChild(this._toolbarNode); this._showStore.add(toDisposable(() => this._toolbarNode.remove())); const that = this; @@ -494,7 +607,7 @@ export class InlineChatSessionOverlayWidget extends Disposable { }, menuOptions: { renderShortTitle: true }, actionViewItemProvider: (action, options) => { - const primaryActions = ['inlineChat2.cancel', 'inlineChat2.keep', 'inlineChat2.close']; + const primaryActions = ['inlineChat2.cancel', 'inlineChat2.keep', 'inlineChat2.rephrase']; const labeledActions = primaryActions.concat(['inlineChat2.undo']); if (!labeledActions.includes(action.id)) { @@ -523,8 +636,12 @@ export class InlineChatSessionOverlayWidget extends Disposable { const padding = Math.round(lineHeight.read(r) * 2 / 3); // Cap max-width to the editor viewport (content area) - const maxWidth = layoutInfo.contentWidth - 2 * padding; + const maxWidth = Math.min(400, layoutInfo.contentWidth - 2 * padding); + const maxHeight = Math.min(150, Math.floor(layoutInfo.height / 3)); this._domNode.style.maxWidth = `${maxWidth}px`; + this._markdownScrollable.getDomNode().style.maxHeight = `${maxHeight}px`; + this._markdownContainer.style.maxHeight = `${maxHeight}px`; + this._markdownScrollable.scanDomNode(); // Position: top right, below sticky scroll with padding, left of minimap and scrollbar const top = stickyScrollHeight + padding; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 1e54c82cb2f..8a348c9fba5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; @@ -12,17 +14,21 @@ import { IChatWidgetService } from '../../chat/browser/chat.js'; import { IChatEditingSession } from '../../chat/common/editing/chatEditingService.js'; import { IChatModel, IChatModelInputState, IChatRequestModel } from '../../chat/common/model/chatModel.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; -import { ChatAgentLocation } from '../../chat/common/constants.js'; +import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; export const IInlineChatSessionService = createDecorator('IInlineChatSessionService'); +export type InlineChatSessionTerminationState = string | IMarkdownString; + export interface IInlineChatSession2 { readonly initialPosition: Position; readonly initialSelection: Selection; readonly uri: URI; readonly chatModel: IChatModel; readonly editingSession: IChatEditingSession; + readonly terminationState: IObservable; + setTerminationState(state: InlineChatSessionTerminationState | undefined): void; dispose(): void; } @@ -61,7 +67,7 @@ export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatMo } } -export async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined) { +export async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined, fileContext?: { uri: URI; selection: Selection }) { const widgetService = accessor.get(IChatWidgetService); const chatService = accessor.get(IChatService); @@ -74,10 +80,38 @@ export async function askInPanelChat(accessor: ServicesAccessor, request: IChatR const newModelRef = chatService.startNewLocalSession(ChatAgentLocation.Chat); const newModel = newModelRef.object; - newModel.inputModel.setState({ ...state }); + newModel.inputModel.setState({ + ...state, + mode: { id: 'agent', kind: ChatModeKind.Agent } + }); const widget = await widgetService.openSession(newModelRef.object.sessionResource); newModelRef.dispose(); // can be freed after opening because the widget also holds a reference + if (widget && fileContext && !fileContext.selection.isEmpty()) { + await widget.attachmentModel.addFile(fileContext.uri, fileContext.selection); + } widget?.acceptInput(request.message.text); } + +export async function continueInPanelChat(accessor: ServicesAccessor, session: IInlineChatSession2): Promise { + const request = session.chatModel.getRequests().at(-1); + if (!request) { + return; + } + + await askInPanelChat(accessor, request, session.chatModel.inputModel.state.get(), { uri: session.uri, selection: session.initialSelection }); + session.dispose(); +} + +export function rephraseInlineChat(accessor: ServicesAccessor, session: IInlineChatSession2): string | undefined { + const request = session.chatModel.getRequests().at(-1); + if (!request) { + return undefined; + } + + accessor.get(IChatService).removeRequest(session.chatModel.sessionResource, request.id); + session.chatModel.inputModel.setState({ inputText: request.message.text }); + session.setTerminationState(undefined); + return request.message.text; +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 6879c6b4391..2aa1b591c18 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, dispose, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; -import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -27,7 +27,7 @@ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js'; import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { askInPanelChat, IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; +import { continueInPanelChat, IInlineChatSession2, IInlineChatSessionService, InlineChatSessionTerminationState, rephraseInlineChat } from './inlineChatSessionService.js'; export class InlineChatError extends Error { static readonly code = 'InlineChatError'; @@ -83,6 +83,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); const chatModel = chatModelRef.object; chatModel.startEditingSession(false); + const terminationState = observableValue(this, undefined); const store = new DisposableStore(); store.add(toDisposable(() => { @@ -136,6 +137,11 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { initialSelection: editor.getSelection(), chatModel, editingSession: chatModel.editingSession!, + terminationState, + setTerminationState: state => { + terminationState.set(state, undefined); + this._onDidChangeSessions.fire(this); + }, dispose: store.dispose.bind(store) }; this._sessions.set(uri, result); @@ -235,7 +241,18 @@ export class InlineChatEscapeToolContribution extends Disposable { canBeReferencedInPrompt: false, alwaysDisplayInputOutput: false, displayName: localize('name', "Inline Chat to Panel Chat"), - modelDescription: 'Moves the inline chat session to the richer panel chat which supports edits across files, creating and deleting files, multi-turn conversations between the user and the assistant, and access to more IDE tools, like retrieve problems, interact with source control, run terminal commands etc.', + modelDescription: 'Show a short textual response when not being able to make code changes and when not having been asked for code changes. Can also be used to move the request to the richer panel chat which supports edits across files, creating and deleting files, multi-turn conversations between the user and the assistant, and access to more IDE tools, like retrieve problems, interact with source control, run terminal commands etc.', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + response: { + type: 'string', + description: localize('response.description', "Optional brief response for inline chat. Keep it at 10 words or fewer."), + maxLength: 200, + } + } + } }; constructor( @@ -243,6 +260,7 @@ export class InlineChatEscapeToolContribution extends Disposable { @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @IDialogService dialogService: IDialogService, @ICodeEditorService codeEditorService: ICodeEditorService, + @IConfigurationService configurationService: IConfigurationService, @IChatService chatService: IChatService, @ILogService logService: ILogService, @IStorageService storageService: IStorageService, @@ -268,6 +286,22 @@ export class InlineChatEscapeToolContribution extends Disposable { return { content: [{ kind: 'text', value: 'Cancel' }] }; } + const lastRequest = session.chatModel.getRequests().at(-1); + if (!lastRequest) { + logService.warn(`InlineChatEscapeToolContribution: no request found for id ${sessionResource}`); + return { content: [{ kind: 'text', value: 'Cancel' }], toolResultMessage: localize('tool.cancel', "Cancel") }; + } + + if (configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { + + const response = typeof invocation.parameters?.response === 'string' && invocation.parameters.response.trim().length > 0 + ? invocation.parameters.response.trim() + : localize('terminated.message', "Inline chat is designed for making single-file code changes. Continue your request in the Chat view or rephrase it for inline chat."); + + session.setTerminationState(response); + return { content: [{ kind: 'text', value: 'Success' }] }; + } + const dontAskAgain = storageService.getBoolean(InlineChatEscapeToolContribution.DONT_ASK_AGAIN_KEY, StorageScope.PROFILE); let result: { confirmed: boolean; checkboxChecked?: boolean }; @@ -290,14 +324,11 @@ export class InlineChatEscapeToolContribution extends Disposable { if (!editor || result.confirmed) { logService.trace('InlineChatEscapeToolContribution: moving session to panel chat'); - await instaService.invokeFunction(askInPanelChat, session.chatModel.getRequests().at(-1)!, session.chatModel.inputModel.state.get()); - session.dispose(); + await instaService.invokeFunction(continueInPanelChat, session); } else { logService.trace('InlineChatEscapeToolContribution: rephrase prompt'); - const lastRequest = session.chatModel.getRequests().at(-1)!; - chatService.removeRequest(session.chatModel.sessionResource, lastRequest.id); - session.chatModel.inputModel.setState({ inputText: lastRequest.message.text }); + instaService.invokeFunction(rephraseInlineChat, session); } if (result.checkboxChecked) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 9b8cada0767..68424c91024 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -98,29 +98,81 @@ } .inline-chat-session-overlay-container { - padding: 2px 4px; + padding: 4px; color: var(--vscode-foreground); background-color: var(--vscode-editorWidget-background); border-radius: 6px; border: 1px solid var(--vscode-contrastBorder); display: flex; - align-items: center; + flex-direction: column; + align-items: stretch; justify-content: center; - gap: 4px; + gap: 8px; z-index: 10; box-shadow: var(--vscode-shadow-lg); overflow: hidden; } +.inline-chat-session-overlay-container .markdown-message { + padding: 5px; + font-size: 12px; + line-height: 1.45; +} + +.inline-chat-session-overlay-container > .monaco-scrollable-element { + width: 100%; +} + +.inline-chat-session-overlay-container > .monaco-scrollable-element.hidden { + display: none; +} + +.inline-chat-session-overlay-container .markdown-scroll-container { + width: 100%; +} + +.inline-chat-session-overlay-container .markdown-message DIV P { + margin: 0; +} + +.inline-chat-session-overlay-container .markdown-message DIV P code { + font-family: var(--monaco-monospace-font); + font-size: var(--vscode-chat-font-size-body-xs); + color: var(--vscode-textPreformat-foreground); + background-color: var(--vscode-textPreformat-background); + padding: 1px 3px; + border-radius: 4px; + border: 1px solid var(--vscode-textPreformat-border); + white-space: pre-wrap; +} + + +.inline-chat-session-overlay-container .content-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + min-width: 0; +} + +.inline-chat-session-overlay-container .content-row.status-hidden { + justify-content: flex-end; +} + .inline-chat-session-overlay-container .status { align-items: center; display: inline-flex; + flex: 1; padding: 5px 0 5px 5px; font-size: 12px; overflow: hidden; gap: 6px; } +.inline-chat-session-overlay-container .status.hidden { + display: none; +} + .inline-chat-session-overlay-container .status .message { white-space: nowrap; overflow: hidden; diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 6331855f6a0..8832cbe3cc9 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -22,6 +22,7 @@ export const enum InlineChatConfigKeys { Affordance = 'inlineChat.affordance', RenderMode = 'inlineChat.renderMode', FixDiagnostics = 'inlineChat.fixDiagnostics', + AskInChat = 'inlineChat.askInChat', } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -67,7 +68,7 @@ Registry.as(Extensions.Configuration).registerConfigurat }, [InlineChatConfigKeys.RenderMode]: { description: localize('renderMode', "Controls how inline chat is rendered."), - default: 'zone', + default: 'hover', type: 'string', enum: ['zone', 'hover'], enumDescriptions: [ @@ -87,7 +88,12 @@ Registry.as(Extensions.Configuration).registerConfigurat mode: 'auto' }, tags: ['experimental'] - } + }, + [InlineChatConfigKeys.AskInChat]: { + description: localize('askInChat', "Controls whether files in a chat editing session use Ask in Chat instead of Inline Chat."), + default: true, + type: 'boolean', + }, } }); @@ -124,6 +130,7 @@ export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('i export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); export const CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT = new RawContextKey('inlineChatFileBelongsToChat', false, localize('inlineChatFileBelongsToChat', "Whether the current file belongs to a chat editing session")); export const CTX_INLINE_CHAT_PENDING_CONFIRMATION = new RawContextKey('inlineChatPendingConfirmation', false, localize('inlineChatPendingConfirmation', "Whether an inline chat request is pending user confirmation")); +export const CTX_INLINE_CHAT_TERMINATED = new RawContextKey('inlineChatTerminated', false, localize('inlineChatTerminated', "Whether the current inline chat session is terminated")); export const CTX_INLINE_CHAT_AFFORDANCE_VISIBLE = new RawContextKey('inlineChatAffordanceVisible', false, localize('inlineChatAffordanceVisible', "Whether an inline chat affordance widget is visible")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( @@ -137,6 +144,7 @@ export const CTX_INLINE_CHAT_V2_ENABLED = ContextKeyExpr.or( export const CTX_HOVER_MODE = ContextKeyExpr.equals('config.inlineChat.renderMode', 'hover'); export const CTX_FIX_DIAGNOSTICS_ENABLED = ContextKeyExpr.equals('config.inlineChat.fixDiagnostics', true); +export const CTX_ASK_IN_CHAT_ENABLED = ContextKeyExpr.equals('config.inlineChat.askInChat', true); // --- (selected) action identifier diff --git a/src/vs/workbench/contrib/keybindingsExport/electron-browser/keybindingsExport.contribution.ts b/src/vs/workbench/contrib/keybindingsExport/electron-browser/keybindingsExport.contribution.ts new file mode 100644 index 00000000000..6235b798361 --- /dev/null +++ b/src/vs/workbench/contrib/keybindingsExport/electron-browser/keybindingsExport.contribution.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { URI } from '../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { join } from '../../../../base/common/path.js'; +import { OperatingSystem } from '../../../../base/common/platform.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IKeybindingItem, KeybindingsRegistry } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeybindingResolver } from '../../../../platform/keybinding/common/keybindingResolver.js'; +import { ResolvedKeybindingItem } from '../../../../platform/keybinding/common/resolvedKeybindingItem.js'; +import { IKeyboardMapper } from '../../../../platform/keyboardLayout/common/keyboardMapper.js'; +import { IMacLinuxKeyboardMapping, IWindowsKeyboardMapping } from '../../../../platform/keyboardLayout/common/keyboardLayout.js'; +import { MacLinuxKeyboardMapper } from '../../../services/keybinding/common/macLinuxKeyboardMapper.js'; +import { WindowsKeyboardMapper } from '../../../services/keybinding/common/windowsKeyboardMapper.js'; +import { IKeymapInfo, KeymapInfo } from '../../../services/keybinding/common/keymapInfo.js'; +import { EN_US_WIN_LAYOUT } from '../../../services/keybinding/browser/keyboardLayouts/en.win.js'; +import { EN_US_DARWIN_LAYOUT } from '../../../services/keybinding/browser/keyboardLayouts/en.darwin.js'; +import { EN_US_LINUX_LAYOUT } from '../../../services/keybinding/browser/keyboardLayouts/en.linux.js'; +import { KeybindingIO, OutputBuilder } from '../../../services/keybinding/common/keybindingIO.js'; +import { getAllUnboundCommands } from '../../../services/keybinding/browser/unboundCommands.js'; + +export class KeybindingsExportContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.keybindingsExport'; + + constructor( + @INativeEnvironmentService private readonly nativeEnvironmentService: INativeEnvironmentService, + @IFileService private readonly fileService: IFileService, + @INativeHostService private readonly nativeHostService: INativeHostService, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + if (this.productService.quality === 'stable') { + return; + } + + const outputPath = this.nativeEnvironmentService.exportDefaultKeybindings; + if (outputPath !== undefined) { + const defaultPath = join(this.nativeEnvironmentService.appRoot, 'doc'); + void this.exportDefaultKeybindingsAndQuit(outputPath || defaultPath); + } + } + + private async exportDefaultKeybindingsAndQuit(outputPath: string): Promise { + try { + const platforms: { os: OperatingSystem; filename: string }[] = [ + { os: OperatingSystem.Windows, filename: 'doc.keybindings.win.json' }, + { os: OperatingSystem.Macintosh, filename: 'doc.keybindings.osx.json' }, + { os: OperatingSystem.Linux, filename: 'doc.keybindings.linux.json' }, + ]; + + for (const { os, filename } of platforms) { + const content = KeybindingsExportContribution._getDefaultKeybindingsContentForOS(os); + const filePath = join(outputPath, filename); + await this.fileService.writeFile(URI.file(filePath), VSBuffer.fromString(content)); + this.logService.info(`[${KeybindingsExportContribution.ID}] Wrote ${filePath}`); + } + + await this.nativeHostService.exit(0); + } catch (error) { + this.logService.error(`[${KeybindingsExportContribution.ID}] Failed to generate default keybindings`, error); + await this.nativeHostService.exit(1); + } + } + + private static _getDefaultKeybindingsContentForOS(os: OperatingSystem): string { + const items = KeybindingsRegistry.getDefaultKeybindingsForOS(os); + const mapper = KeybindingsExportContribution._createKeyboardMapperForOS(os); + const resolved = KeybindingsExportContribution._resolveKeybindingItemsWithMapper(items, mapper); + const resolver = new KeybindingResolver(resolved, [], () => { }); + const defaultKeybindings = resolver.getDefaultKeybindings(); + const boundCommands = resolver.getDefaultBoundCommands(); + return ( + KeybindingsExportContribution._formatDefaultKeybindings(defaultKeybindings) + + '\n\n' + + KeybindingsExportContribution._formatAllCommandsAsComment(boundCommands) + ); + } + + private static _createKeyboardMapperForOS(os: OperatingSystem): IKeyboardMapper { + const layoutMap: Record = { + [OperatingSystem.Windows]: EN_US_WIN_LAYOUT, + [OperatingSystem.Macintosh]: EN_US_DARWIN_LAYOUT, + [OperatingSystem.Linux]: EN_US_LINUX_LAYOUT, + }; + const layout = layoutMap[os]; + const keymapInfo = new KeymapInfo(layout.layout, layout.secondaryLayouts, layout.mapping); + switch (os) { + case OperatingSystem.Windows: + return new WindowsKeyboardMapper(true, keymapInfo.mapping, false); + case OperatingSystem.Macintosh: + return new MacLinuxKeyboardMapper(true, keymapInfo.mapping, false, OperatingSystem.Macintosh); + case OperatingSystem.Linux: + return new MacLinuxKeyboardMapper(true, keymapInfo.mapping, false, OperatingSystem.Linux); + } + } + + private static _resolveKeybindingItemsWithMapper(items: IKeybindingItem[], mapper: IKeyboardMapper): ResolvedKeybindingItem[] { + const result: ResolvedKeybindingItem[] = []; + for (const item of items) { + const when = item.when || undefined; + const keybinding = item.keybinding; + if (!keybinding) { + result.push(new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, true, item.extensionId, item.isBuiltinExtension)); + } else { + const resolvedKeybindings = mapper.resolveKeybinding(keybinding); + for (let i = resolvedKeybindings.length - 1; i >= 0; i--) { + result.push(new ResolvedKeybindingItem(resolvedKeybindings[i], item.command, item.commandArgs, when, true, item.extensionId, item.isBuiltinExtension)); + } + } + } + return result; + } + + private static _formatDefaultKeybindings(defaultKeybindings: readonly ResolvedKeybindingItem[]): string { + const out = new OutputBuilder(); + out.writeLine('['); + const lastIndex = defaultKeybindings.length - 1; + defaultKeybindings.forEach((k, index) => { + KeybindingIO.writeKeybindingItem(out, k); + if (index !== lastIndex) { + out.writeLine(','); + } else { + out.writeLine(); + } + }); + out.writeLine(']'); + return out.toString(); + } + + private static _formatAllCommandsAsComment(boundCommands: Map): string { + const unboundCommands = getAllUnboundCommands(boundCommands); + const pretty = unboundCommands.sort().join('\n// - '); + return '// Here are other available commands: ' + '\n// - ' + pretty; + } +} + +registerWorkbenchContribution2( + KeybindingsExportContribution.ID, + KeybindingsExportContribution, + WorkbenchPhase.Eventually, +); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts index 4d2d9df2c49..467d4f995e4 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; +import { IMcpGatewayServerInfo, IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; +import { IMcpGatewayResult, IMcpGatewayResultServer, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; /** * Browser implementation of the MCP Gateway Service. @@ -47,11 +48,20 @@ export class BrowserMcpGatewayService implements IWorkbenchMcpGatewayService { return connection.withChannel(McpGatewayChannelName, async channel => { const service = ProxyChannel.toService(channel); const info = await service.createGateway(undefined); - const address = URI.revive(info.address); - this._logService.info(`[McpGateway][BrowserWorkbench] Remote gateway created: ${address}`); + const servers = reviveServers(info.servers); + this._logService.info(`[McpGateway][BrowserWorkbench] Remote gateway created with ${servers.length} server(s)`); + + const onDidChangeServers = Event.map( + Event.filter( + channel.listen<{ gatewayId: string; servers: readonly IMcpGatewayServerInfo[] }>('onDidChangeGatewayServers'), + e => e.gatewayId === info.gatewayId, + ), + e => reviveServers(e.servers), + ); return { - address, + servers, + onDidChangeServers, dispose: () => { this._logService.info(`[McpGateway][BrowserWorkbench] Disposing remote gateway: ${info.gatewayId}`); service.disposeGateway(info.gatewayId); @@ -60,3 +70,7 @@ export class BrowserMcpGatewayService implements IWorkbenchMcpGatewayService { }); } } + +function reviveServers(servers: readonly IMcpGatewayServerInfo[]): IMcpGatewayResultServer[] { + return servers.map(s => ({ label: s.label, address: URI.revive(s.address) })); +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts index 7cb6a6efebd..3a3bfb38f88 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts @@ -43,11 +43,17 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { if (!isContributionEnabled(plugin.enablement.read(reader))) { continue; } + const servers = plugin.mcpServerDefinitions.read(reader); + if (servers.length === 0) { + continue; + } + seen.add(plugin.uri); let collectionState = this._collections.get(plugin.uri); if (!collectionState) { - collectionState = this.createCollectionState(plugin); + // note: all plugin servers are currently defined in the same file + collectionState = this.createCollectionState(plugin, servers[0].uri); this._collections.set(plugin.uri, collectionState); } } @@ -60,7 +66,7 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { })); } - private createCollectionState(plugin: IAgentPlugin) { + private createCollectionState(plugin: IAgentPlugin, manifestURI: URI) { const collectionId = `plugin.${plugin.uri}`; return this._mcpRegistry.registerCollection({ id: collectionId, @@ -72,7 +78,7 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { serverDefinitions: plugin.mcpServerDefinitions.map(defs => defs.map(d => this._toServerDefinition(collectionId, d)).filter(isDefined)), presentation: { - origin: plugin.uri, + origin: manifestURI, order: McpCollectionSortOrder.Plugin, }, }); @@ -91,6 +97,7 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { id: `${collectionId}.${name}`, label: name, launch, + variableReplacement: { target: ConfigurationTarget.USER }, cacheNonce: String(hash(launch)), }; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts index 7376729aa00..f97a7a8f92c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts @@ -3,20 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; export const IWorkbenchMcpGatewayService = createDecorator('IWorkbenchMcpGatewayService'); +/** + * A single server entry exposed by the gateway at the workbench layer. + */ +export interface IMcpGatewayResultServer { + readonly label: string; + readonly address: URI; +} + /** * Result of creating an MCP gateway, which is itself disposable. */ export interface IMcpGatewayResult extends IDisposable { /** - * The address of the HTTP endpoint for this gateway. + * The servers currently exposed by this gateway. */ - readonly address: URI; + readonly servers: readonly IMcpGatewayResultServer[]; + + /** + * Event that fires when the set of servers changes. + */ + readonly onDidChangeServers: Event; } /** diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 7e4e2c18691..172f0650aab 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -9,27 +9,31 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IGatewayCallToolResult, IGatewayServerResources, IGatewayServerResourceTemplates } from '../../../../platform/mcp/common/mcpGateway.js'; +import { IMcpGatewayServerDescriptor } from '../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../../../platform/mcp/common/modelContextProtocol.js'; import { McpServer } from './mcpServer.js'; import { IMcpServer, IMcpService, McpCapability, McpServerCacheState, McpToolVisibility } from './mcpTypes.js'; import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js'; -interface ICallToolArgs { +interface ICallToolForServerArgs { + serverId: string; name: string; args: Record; } -interface IReadResourceArgs { - serverIndex: number; +interface IReadResourceForServerArgs { + serverId: string; uri: string; } +interface IServerIdArg { + serverId: string; +} + export class McpGatewayToolBrokerChannel extends Disposable implements IServerChannel { private readonly _onDidChangeTools = this._register(new Emitter()); private readonly _onDidChangeResources = this._register(new Emitter()); - private readonly _serverIdMap = new Map(); - private _nextServerIndex = 0; + private readonly _onDidChangeServers = this._register(new Emitter()); /** * Per-server promise that races server startup against the grace period timeout. @@ -78,21 +82,23 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh resourcesInitialized = true; } })); + + let serversInitialized = false; + this._register(autorun(reader => { + const servers = this._mcpService.servers.read(reader); + + if (serversInitialized) { + this._logService.debug('[McpGateway][ToolBroker] Servers changed, firing onDidChangeServers'); + this._onDidChangeServers.fire(servers.map(s => ({ id: s.definition.id, label: s.definition.label }))); + } else { + serversInitialized = true; + } + })); } - private _getServerIndex(server: IMcpServer): number { - const defId = server.definition.id; - let index = this._serverIdMap.get(defId); - if (index === undefined) { - index = this._nextServerIndex++; - this._serverIdMap.set(defId, index); - } - return index; - } - - private _getServerByIndex(serverIndex: number): IMcpServer | undefined { + private _getServerById(serverId: string): IMcpServer | undefined { for (const server of this._mcpService.servers.get()) { - if (this._getServerIndex(server) === serverIndex) { + if (server.definition.id === serverId) { return server; } } @@ -128,10 +134,6 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private async _shouldUseCachedData(server: IMcpServer): Promise { const cacheState = server.cacheState.get(); if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { - // On first list call: wait up to the grace period for the server to start. - // On subsequent calls: the stored promise is already resolved, returns immediately. - // Outdated servers get the same grace period as Unknown — a prior fast startup - // does not guarantee a fast restart. await this._waitForStartup(server); const newState = server.cacheState.get(); return newState === McpServerCacheState.Live @@ -149,6 +151,8 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh return this._onDidChangeTools.event as Event; case 'onDidChangeResources': return this._onDidChangeResources.event as Event; + case 'onDidChangeServers': + return this._onDidChangeServers.event as Event; } throw new Error(`Invalid listen: ${event}`); @@ -158,26 +162,33 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh this._logService.debug(`[McpGateway][ToolBroker] IPC call: ${command}`); switch (command) { - case 'listTools': { - const tools = await this._listTools(); + case 'listServers': { + const servers = this._listServers(); + return servers as T; + } + case 'listToolsForServer': { + const { serverId } = arg as IServerIdArg; + const tools = await this._listToolsForServer(serverId); return tools as T; } - case 'callTool': { - const { name, args } = arg as ICallToolArgs; - const result = await this._callTool(name, args || {}, cancellationToken); + case 'callToolForServer': { + const { serverId, name, args } = arg as ICallToolForServerArgs; + const result = await this._callToolForServer(serverId, name, args || {}, cancellationToken); return result as T; } - case 'listResources': { - const resources = await this._listResources(); + case 'listResourcesForServer': { + const { serverId } = arg as IServerIdArg; + const resources = await this._listResourcesForServer(serverId); return resources as T; } - case 'readResource': { - const { serverIndex, uri } = arg as IReadResourceArgs; - const result = await this._readResource(serverIndex, uri, cancellationToken); + case 'readResourceForServer': { + const { serverId, uri } = arg as IReadResourceForServerArgs; + const result = await this._readResourceForServer(serverId, uri, cancellationToken); return result as T; } - case 'listResourceTemplates': { - const templates = await this._listResourceTemplates(); + case 'listResourceTemplatesForServer': { + const { serverId } = arg as IServerIdArg; + const templates = await this._listResourceTemplatesForServer(serverId); return templates as T; } } @@ -185,112 +196,114 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh throw new Error(`Invalid call: ${command}`); } - private async _listTools(): Promise { + private _listServers(): readonly IMcpGatewayServerDescriptor[] { const servers = this._mcpService.servers.get(); - const perServer = await Promise.all(servers.map(async server => { - if (!await this._shouldUseCachedData(server)) { - this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' not ready, skipping tool listing`); - return [] as MCP.Tool[]; - } - return server.tools.get() - .filter(t => t.visibility & McpToolVisibility.Model) - .map(t => t.definition); - })); - - const mcpTools = perServer.flat(); - this._logService.debug(`[McpGateway][ToolBroker] listTools result: ${mcpTools.length} tool(s): [${mcpTools.map(t => t.name).join(', ')}]`); - - return mcpTools; - } - - private async _callTool(name: string, args: Record, token: CancellationToken = CancellationToken.None): Promise { - this._logService.debug(`[McpGateway][ToolBroker] callTool '${name}' with args: ${JSON.stringify(args)}`); - - for (const server of this._mcpService.servers.get()) { - const tool = server.tools.get().find(t => - t.definition.name === name && (t.visibility & McpToolVisibility.Model) - ); - - if (tool) { - this._logService.debug(`[McpGateway][ToolBroker] Found tool '${name}' on server '${server.definition.id}' (index=${this._getServerIndex(server)})`); - const result = await tool.call(args, undefined, token); - this._logService.debug(`[McpGateway][ToolBroker] Tool '${name}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`); - return { result, serverIndex: this._getServerIndex(server) }; - } + const result: IMcpGatewayServerDescriptor[] = []; + for (const server of servers) { + result.push({ id: server.definition.id, label: server.definition.label }); } - - this._logService.warn(`[McpGateway][ToolBroker] Tool '${name}' not found on any server`); - throw new Error(`Unknown tool: ${name}`); - } - - private async _listResources(): Promise { - const results: IGatewayServerResources[] = []; - const servers = this._mcpService.servers.get(); - this._logService.debug(`[McpGateway][ToolBroker] listResources: ${servers.length} server(s) known`); - - await Promise.all(servers.map(async server => { - if (!await this._shouldUseCachedData(server)) { - return; - } - - const capabilities = server.capabilities.get(); - if (!capabilities || !(capabilities & McpCapability.Resources)) { - this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' has no resource capability, skipping`); - return; - } - - try { - const resources = await McpServer.callOn(server, h => h.listResources()); - this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' (index=${this._getServerIndex(server)}) listed ${resources.length} resource(s)`); - results.push({ serverIndex: this._getServerIndex(server), resources }); - } catch (error) { - this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to list resources`, error); - } - })); - - this._logService.debug(`[McpGateway][ToolBroker] listResources result: ${results.length} server(s) with resources`); - return results; - } - - private async _readResource(serverIndex: number, uri: string, token: CancellationToken = CancellationToken.None): Promise { - const server = this._getServerByIndex(serverIndex); - if (!server) { - this._logService.warn(`[McpGateway][ToolBroker] readResource: unknown server index ${serverIndex}`); - throw new Error(`Unknown server index: ${serverIndex}`); - } - - this._logService.debug(`[McpGateway][ToolBroker] readResource '${uri}' from server '${server.definition.id}' (index=${serverIndex})`); - const result = await McpServer.callOn(server, h => h.readResource({ uri }, token), token); - this._logService.debug(`[McpGateway][ToolBroker] readResource returned ${result.contents.length} content(s)`); + this._logService.debug(`[McpGateway][ToolBroker] listServers result: ${result.length} server(s): [${result.map(s => s.label).join(', ')}]`); return result; } - private async _listResourceTemplates(): Promise { - const results: IGatewayServerResourceTemplates[] = []; - const servers = this._mcpService.servers.get(); - this._logService.debug(`[McpGateway][ToolBroker] listResourceTemplates: ${servers.length} server(s) known`); + private async _listToolsForServer(serverId: string): Promise { + const server = this._getServerById(serverId); + if (!server) { + this._logService.warn(`[McpGateway][ToolBroker] listToolsForServer: unknown server '${serverId}'`); + return []; + } + if (!await this._shouldUseCachedData(server)) { + this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' not ready, skipping tool listing`); + return []; + } + const tools = server.tools.get() + .filter(t => t.visibility & McpToolVisibility.Model) + .map(t => t.definition); + this._logService.debug(`[McpGateway][ToolBroker] listToolsForServer '${serverId}': ${tools.length} tool(s)`); + return tools; + } - await Promise.all(servers.map(async server => { - if (!await this._shouldUseCachedData(server)) { - return; - } + private async _callToolForServer(serverId: string, name: string, args: Record, token: CancellationToken = CancellationToken.None): Promise { + this._logService.debug(`[McpGateway][ToolBroker] callToolForServer '${serverId}' tool '${name}' with args: ${JSON.stringify(args)}`); - const capabilities = server.capabilities.get(); - if (!capabilities || !(capabilities & McpCapability.Resources)) { - return; - } + const server = this._getServerById(serverId); + if (!server) { + throw new Error(`Unknown server: ${serverId}`); + } - try { - const resourceTemplates = await McpServer.callOn(server, h => h.listResourceTemplates()); - this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' (index=${this._getServerIndex(server)}) listed ${resourceTemplates.length} resource template(s)`); - results.push({ serverIndex: this._getServerIndex(server), resourceTemplates }); - } catch (error) { - this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to list resource templates`, error); - } - })); + const tool = server.tools.get().find(t => + t.definition.name === name && (t.visibility & McpToolVisibility.Model) + ); + if (!tool) { + throw new Error(`Unknown tool '${name}' on server '${serverId}'`); + } - this._logService.debug(`[McpGateway][ToolBroker] listResourceTemplates result: ${results.length} server(s) with templates`); - return results; + const result = await tool.call(args, undefined, token); + this._logService.debug(`[McpGateway][ToolBroker] Tool '${name}' on '${serverId}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`); + return result; + } + + private async _listResourcesForServer(serverId: string): Promise { + const server = this._getServerById(serverId); + if (!server) { + this._logService.warn(`[McpGateway][ToolBroker] listResourcesForServer: unknown server '${serverId}'`); + return []; + } + if (!await this._shouldUseCachedData(server)) { + return []; + } + + const capabilities = server.capabilities.get(); + if (!capabilities || !(capabilities & McpCapability.Resources)) { + this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' has no resource capability`); + return []; + } + + try { + const resources = await McpServer.callOn(server, h => h.listResources()); + this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' listed ${resources.length} resource(s)`); + return resources; + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${serverId}' failed to list resources`, error); + return []; + } + } + + private async _readResourceForServer(serverId: string, uri: string, token: CancellationToken = CancellationToken.None): Promise { + const server = this._getServerById(serverId); + if (!server) { + throw new Error(`Unknown server: ${serverId}`); + } + + this._logService.debug(`[McpGateway][ToolBroker] readResourceForServer '${uri}' from server '${serverId}'`); + const result = await McpServer.callOn(server, h => h.readResource({ uri }, token), token); + this._logService.debug(`[McpGateway][ToolBroker] readResourceForServer returned ${result.contents.length} content(s)`); + return result; + } + + private async _listResourceTemplatesForServer(serverId: string): Promise { + const server = this._getServerById(serverId); + if (!server) { + this._logService.warn(`[McpGateway][ToolBroker] listResourceTemplatesForServer: unknown server '${serverId}'`); + return []; + } + if (!await this._shouldUseCachedData(server)) { + return []; + } + + const capabilities = server.capabilities.get(); + if (!capabilities || !(capabilities & McpCapability.Resources)) { + return []; + } + + try { + const resourceTemplates = await McpServer.callOn(server, h => h.listResourceTemplates()); + this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' listed ${resourceTemplates.length} resource template(s)`); + return resourceTemplates; + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${serverId}' failed to list resource templates`, error); + return []; + } } private async _ensureServerReady(server: IMcpServer): Promise { diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts index 71d0dcdc64b..5bf8d9cde62 100644 --- a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; -import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { IChannel, ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; +import { IMcpGatewayServerInfo, IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; +import { IMcpGatewayResult, IMcpGatewayResultServer, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; /** * Electron workbench implementation of the MCP Gateway Service. @@ -21,15 +22,15 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { declare readonly _serviceBrand: undefined; private readonly _localPlatformService: IMcpGatewayService; + private readonly _localChannel: IChannel; constructor( @IMainProcessService mainProcessService: IMainProcessService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @ILogService private readonly _logService: ILogService, ) { - this._localPlatformService = ProxyChannel.toService( - mainProcessService.getChannel(McpGatewayChannelName) - ); + this._localChannel = mainProcessService.getChannel(McpGatewayChannelName); + this._localPlatformService = ProxyChannel.toService(this._localChannel); } async createGateway(inRemote: boolean): Promise { @@ -44,11 +45,20 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { private async _createLocalGateway(): Promise { this._logService.info('[McpGateway][Workbench] Creating local gateway via main process'); const info = await this._localPlatformService.createGateway(undefined); - const address = URI.revive(info.address); - this._logService.info(`[McpGateway][Workbench] Local gateway created: ${address}`); + const servers = reviveServers(info.servers); + this._logService.info(`[McpGateway][Workbench] Local gateway created with ${servers.length} server(s)`); + + const onDidChangeServers = Event.map( + Event.filter( + this._localChannel.listen<{ gatewayId: string; servers: readonly IMcpGatewayServerInfo[] }>('onDidChangeGatewayServers'), + e => e.gatewayId === info.gatewayId, + ), + e => reviveServers(e.servers), + ); return { - address, + servers, + onDidChangeServers, dispose: () => { this._logService.info(`[McpGateway][Workbench] Disposing local gateway: ${info.gatewayId}`); this._localPlatformService.disposeGateway(info.gatewayId); @@ -67,11 +77,20 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { return connection.withChannel(McpGatewayChannelName, async channel => { const service = ProxyChannel.toService(channel); const info = await service.createGateway(undefined); - const address = URI.revive(info.address); - this._logService.info(`[McpGateway][Workbench] Remote gateway created: ${address}`); + const servers = reviveServers(info.servers); + this._logService.info(`[McpGateway][Workbench] Remote gateway created with ${servers.length} server(s)`); + + const onDidChangeServers = Event.map( + Event.filter( + channel.listen<{ gatewayId: string; servers: readonly IMcpGatewayServerInfo[] }>('onDidChangeGatewayServers'), + e => e.gatewayId === info.gatewayId, + ), + e => reviveServers(e.servers), + ); return { - address, + servers, + onDidChangeServers, dispose: () => { this._logService.info(`[McpGateway][Workbench] Disposing remote gateway: ${info.gatewayId}`); service.disposeGateway(info.gatewayId); @@ -80,3 +99,7 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { }); } } + +function reviveServers(servers: readonly IMcpGatewayServerInfo[]): IMcpGatewayResultServer[] { + return servers.map(s => ({ label: s.label, address: URI.revive(s.address) })); +} diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index e0dcab8d7f1..be81ff88ad3 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -10,7 +10,7 @@ import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelSc import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ContributionEnablementState } from '../../../chat/common/enablement.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { IGatewayCallToolResult } from '../../../../../platform/mcp/common/mcpGateway.js'; +import { IMcpGatewayServerDescriptor } from '../../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../common/modelContextProtocol.js'; import { McpGatewayToolBrokerChannel } from '../../common/mcpGatewayToolBrokerChannel.js'; import { IMcpIcons, IMcpServer, IMcpTool, McpConnectionState, McpServerCacheState, McpToolVisibility } from '../../common/mcpTypes.js'; @@ -19,7 +19,7 @@ import { TestMcpService } from './testMcpService.js'; suite('McpGatewayToolBrokerChannel', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('lists model-visible tools with namespaced identities', async () => { + test('lists model-visible tools for a specific server', async () => { const mcpService = new TestMcpService(); const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); @@ -33,18 +33,16 @@ suite('McpGatewayToolBrokerChannel', () => { mcpService.servers.set([serverA, serverB], undefined); - const result = await channel.call(undefined, 'listTools'); - const names = result.map(tool => tool.name).sort(); + const resultA = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); + assert.deepStrictEqual(resultA.map(t => t.name), ['mcp_serverA_echo']); - assert.deepStrictEqual(names, [ - 'mcp_serverA_echo', - 'mcp_serverB_echo', - ]); + const resultB = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverB' }); + assert.deepStrictEqual(resultB.map(t => t.name), ['mcp_serverB_echo']); channel.dispose(); }); - test('routes tool calls by namespaced identity', async () => { + test('routes tool calls to specific server', async () => { const mcpService = new TestMcpService(); const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); @@ -64,18 +62,20 @@ suite('McpGatewayToolBrokerChannel', () => { mcpService.servers.set([serverA, serverB], undefined); - const resultA = await channel.call(undefined, 'callTool', { + const resultA = await channel.call(undefined, 'callToolForServer', { + serverId: 'serverA', name: 'mcp_serverA_echo', args: { name: 'one' }, }); - const resultB = await channel.call(undefined, 'callTool', { + const resultB = await channel.call(undefined, 'callToolForServer', { + serverId: 'serverB', name: 'mcp_serverB_echo', args: { name: 'two' }, }); assert.deepStrictEqual(invoked, ['A:one', 'B:two']); - assert.strictEqual((resultA.result.content[0] as MCP.TextContent).text, 'from A'); - assert.strictEqual((resultB.result.content[0] as MCP.TextContent).text, 'from B'); + assert.strictEqual((resultA.content[0] as MCP.TextContent).text, 'from A'); + assert.strictEqual((resultB.content[0] as MCP.TextContent).text, 'from B'); channel.dispose(); }); @@ -117,7 +117,7 @@ suite('McpGatewayToolBrokerChannel', () => { ); mcpService.servers.set([server], undefined); - await channel.call(undefined, 'listTools'); + await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.strictEqual(server.startCalls, 0); channel.dispose(); @@ -135,7 +135,7 @@ suite('McpGatewayToolBrokerChannel', () => { ); mcpService.servers.set([server], undefined); - const tools = await channel.call(undefined, 'listTools'); + const tools = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); // Server started during the grace period; tools are now available. assert.strictEqual(server.startCalls, 1); @@ -155,7 +155,7 @@ suite('McpGatewayToolBrokerChannel', () => { ); mcpService.servers.set([server], undefined); - const tools = await channel.call(undefined, 'listTools'); + const tools = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); // Outdated server gets the same grace period as Unknown — started and tools returned. assert.strictEqual(server.startCalls, 1); @@ -177,11 +177,11 @@ suite('McpGatewayToolBrokerChannel', () => { mcpService.servers.set([server], undefined); // First call: waits up to the grace period, server never starts → empty result. - const tools = await channel.call(undefined, 'listTools'); + const tools = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.deepStrictEqual(tools, []); // Second call: grace-period promise already resolved; returns immediately without re-waiting. - const tools2 = await channel.call(undefined, 'listTools'); + const tools2 = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.deepStrictEqual(tools2, []); channel.dispose(); @@ -202,7 +202,7 @@ suite('McpGatewayToolBrokerChannel', () => { mcpService.servers.set([server], undefined); // First call: grace period elapses, server never starts → empty. - const tools1 = await channel.call(undefined, 'listTools'); + const tools1 = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.deepStrictEqual(tools1, []); assert.strictEqual(server.startCalls, 1); @@ -214,7 +214,7 @@ suite('McpGatewayToolBrokerChannel', () => { // Second call: stale grace entry should be discarded, a new grace race starts, // and the server successfully starts → tools returned. - const tools2 = await channel.call(undefined, 'listTools'); + const tools2 = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.deepStrictEqual(tools2.map(t => t.name), ['echo']); assert.strictEqual(server.startCalls, 2); @@ -237,19 +237,71 @@ suite('McpGatewayToolBrokerChannel', () => { mcpService.servers.set([server], undefined); // First call: server starts successfully during grace period. - const tools1 = await channel.call(undefined, 'listTools'); + const tools1 = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.deepStrictEqual(tools1.map(t => t.name), ['echo']); assert.strictEqual(server.startCalls, 1); // Second call: cacheState is now Live (server started), grace entry should NOT // be invalidated, so no additional start call is made. - const tools2 = await channel.call(undefined, 'listTools'); + const tools2 = await channel.call(undefined, 'listToolsForServer', { serverId: 'serverA' }); assert.deepStrictEqual(tools2.map(t => t.name), ['echo']); assert.strictEqual(server.startCalls, 1); channel.dispose(); }); }); + + test('listServers returns all servers regardless of cache state', async () => { + const mcpService = new TestMcpService(); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); + + const liveServer = createServer('collectionA', 'serverA', [], McpServerCacheState.Live); + const unknownServer = createServer('collectionB', 'serverB', [], McpServerCacheState.Unknown); + + mcpService.servers.set([liveServer, unknownServer], undefined); + + const servers = await channel.call(undefined, 'listServers'); + assert.deepStrictEqual(servers, [ + { id: 'serverA', label: 'serverA' }, + { id: 'serverB', label: 'serverB' }, + ]); + + channel.dispose(); + }); + + test('emits onDidChangeServers with descriptors when servers change', () => { + const mcpService = new TestMcpService(); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); + const serverA = createServer('collectionA', 'serverA', []); + + mcpService.servers.set([serverA], undefined); + + const received: (readonly IMcpGatewayServerDescriptor[])[] = []; + const disposable = channel.listen(undefined, 'onDidChangeServers')(e => { + received.push(e); + }); + + // Add a second server + const serverB = createServer('collectionB', 'serverB', []); + mcpService.servers.set([serverA, serverB], undefined); + + assert.strictEqual(received.length, 1); + assert.deepStrictEqual(received[0], [ + { id: 'serverA', label: 'serverA' }, + { id: 'serverB', label: 'serverB' }, + ]); + + // Remove the first server + mcpService.servers.set([serverB], undefined); + + assert.strictEqual(received.length, 2); + assert.deepStrictEqual(received[1], [ + { id: 'serverB', label: 'serverB' }, + ]); + + disposable.dispose(); + channel.dispose(); + }); }); function createServer( diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts index b051bac13ef..c74df032d41 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts @@ -407,9 +407,10 @@ suite.skip('Workbench - MCP - McpTask', () => { // TODO@connor4312 https://githu } test('should resolve when task completes', async () => { + const getTaskResultStub = sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }); const mockHandler = upcastPartial({ getTask: sinon.stub().resolves(createTask({ status: 'completed' })), - getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + getTaskResult: getTaskResultStub }); const task = store.add(new McpTask(createTask())); @@ -423,7 +424,7 @@ suite.skip('Workbench - MCP - McpTask', () => { // TODO@connor4312 https://githu const result = await task.result; assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] }); - assert.ok((mockHandler.getTaskResult as sinon.SinonStub).calledWith({ taskId: 'task1' })); + assert.ok(getTaskResultStub.calledWith({ taskId: 'task1' })); }); test('should poll for task updates', async () => { diff --git a/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts b/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts index 004220e5876..7d4c7ebc29d 100644 --- a/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts +++ b/src/vs/workbench/contrib/mcp/test/common/testMcpService.ts @@ -11,6 +11,7 @@ export class TestEnablementModel implements IEnablementModel { readEnabled(_key: string): ContributionEnablementState { return ContributionEnablementState.EnabledProfile; } + remove(_key: string): void { } setEnabled(_key: string, _state: ContributionEnablementState): void { } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts index 2bd6be05fdc..18413330237 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts @@ -126,31 +126,43 @@ export class TextModelDiffs extends Disposable { this.ensureUpToDate(); diffToRemoves.sort(compareBy((d) => d.inputRange.startLineNumber, numberComparator)); - diffToRemoves.reverse(); + diffToRemoves.reverse(); // process from bottom of document upward - let diffs = this._diffs.get(); + const diffs = this._diffs.get(); - for (const diffToRemove of diffToRemoves) { - // TODO improve performance - const len = diffs.length; - diffs = diffs.filter((d) => d !== diffToRemove); - if (len === diffs.length) { + // Validate all diffs-to-remove exist using Set for O(1) lookup + const toRemoveSet = new Set(diffToRemoves); + if (toRemoveSet.size !== diffToRemoves.length) { + throw new BugIndicatingError(); // duplicate entries + } + const diffsSet = new Set(diffs); + for (const d of diffToRemoves) { + if (!diffsSet.has(d)) { throw new BugIndicatingError(); } + } + // Apply text model edits in reverse document order (bottom-up, safe for line shifting) + for (const diffToRemove of diffToRemoves) { this._barrier.runExclusivelyOrThrow(() => { const edits = diffToRemove.getReverseLineEdit().toEdits(this.textModel.getLineCount()); this.textModel.pushEditOperations(null, edits, () => null, group); }); - - diffs = diffs.map((d) => - d.outputRange.isAfter(diffToRemove.outputRange) - ? d.addOutputLineDelta(diffToRemove.inputRange.length - diffToRemove.outputRange.length) - : d - ); } - this._diffs.set(diffs, transaction, TextModelDiffChangeReason.other); + // Single forward pass: accumulate delta from removed diffs above, apply to remaining diffs below + let cumulativeDelta = 0; + const newDiffs: DetailedLineRangeMapping[] = []; + + for (const d of diffs) { + if (toRemoveSet.has(d)) { + cumulativeDelta += d.inputRange.length - d.outputRange.length; + } else { + newDiffs.push(cumulativeDelta !== 0 ? d.addOutputLineDelta(cumulativeDelta) : d); + } + } + + this._diffs.set(newDiffs, transaction, TextModelDiffChangeReason.other); } /** diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 9a95995f5a1..ec476411052 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -679,7 +679,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cellIndex = edit.index; } else if (hasKey(edit, { handle: true })) { cellIndex = this._getCellIndexByHandle(edit.handle); - this._assertIndex(cellIndex); + this._assertIndex(cellIndex, `editType: ${edit.editType}, key: handle`); } else if (hasKey(edit, { outputId: true })) { cellIndex = this._getCellIndexWithOutputIdHandle(edit.outputId); if (this._indexIsInvalid(cellIndex)) { @@ -714,7 +714,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } if (b.end === undefined) { - return -1; + return 1; } return b.end - a.end || b.originalIndex - a.originalIndex; @@ -1298,9 +1298,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } - private _assertIndex(index: number) { + private _assertIndex(index: number, context?: string) { if (this._indexIsInvalid(index)) { - throw new Error(`model index out of range ${index}`); + throw new Error(`model index out of range ${index} (cellCount: ${this._cells.length}${context ? `, ${context}` : ''})`); } } diff --git a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts index b2a4b030a6e..ea1143872e4 100644 --- a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts +++ b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts @@ -52,7 +52,7 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib } type InputLatencyStatisticFragment = { - owner: 'tyriar'; + owner: 'hediet'; comment: 'Represents a set of statistics collected about input latencies'; average: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The average time it took to execute.' }; max: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The maximum time it took to execute.' }; @@ -60,7 +60,7 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib }; type PerformanceInputLatencyClassification = { - owner: 'tyriar'; + owner: 'hediet'; comment: 'This is a set of samples of the time (in milliseconds) that various events took when typing in the editor'; keydown: InputLatencyStatisticFragment; input: InputLatencyStatisticFragment; diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index b56d0efeeb9..e7202fc4764 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -215,7 +215,7 @@ class PerfModelContentProvider implements ITextModelContentProvider { table.push(['restore secondary viewlet', metrics.timers.ellapsedAuxiliaryViewletRestore, '[renderer]', metrics.auxiliaryViewletId]); table.push(['restore panel', metrics.timers.ellapsedPanelRestore, '[renderer]', metrics.panelId]); table.push(['restore & resolve visible editors', metrics.timers.ellapsedEditorRestore, '[renderer]', `${metrics.editorIds.length}: ${metrics.editorIds.join(', ')}`]); - table.push(['create workbench contributions', metrics.timers.ellapsedWorkbenchContributions, '[renderer]', `${(contribTimings.get(LifecyclePhase.Starting)?.length ?? 0) + (contribTimings.get(LifecyclePhase.Starting)?.length ?? 0)} blocking startup`]); + table.push(['create workbench contributions', metrics.timers.ellapsedWorkbenchContributions, '[renderer]', `${(contribTimings.get(LifecyclePhase.Starting)?.length ?? 0) + (contribTimings.get(LifecyclePhase.Ready)?.length ?? 0)} blocking startup`]); table.push(['overall workbench load', metrics.timers.ellapsedWorkbench, '[renderer]', undefined]); table.push(['workbench ready', metrics.ellapsed, '[main->renderer]', undefined]); table.push(['renderer ready', metrics.timers.ellapsedRenderer, '[renderer]', undefined]); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 17e5663deb8..3e4a5be82ab 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -192,8 +192,6 @@ export class SettingsEditor2 extends EditorPane { private searchInProgress: CancellationTokenSource | null = null; private aiSearchPromise: CancelablePromise | null = null; - private stopWatch: StopWatch; - private showAiResultsAction: Action | null = null; private searchInputDelayer: Delayer; @@ -283,7 +281,6 @@ export class SettingsEditor2 extends EditorPane { this.aiResultsAvailable = CONTEXT_AI_SETTING_RESULTS_AVAILABLE.bindTo(contextKeyService); this.scheduledRefreshes = new Map(); - this.stopWatch = new StopWatch(false); this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY); @@ -1966,9 +1963,9 @@ export class SettingsEditor2 extends EditorPane { return null; } - this.stopWatch.reset(); + const stopWatch = new StopWatch(false); const result = await aiSearchProvider.getLLMRankedResults(token); - this.stopWatch.stop(); + stopWatch.stop(); if (token.isCancellationRequested) { return null; @@ -1976,7 +1973,7 @@ export class SettingsEditor2 extends EditorPane { // Only log the elapsed time if there are actual results. if (result && result.filterMatches.length > 0) { - const elapsed = this.stopWatch.elapsed(); + const elapsed = stopWatch.elapsed(); this.logSearchPerformance(LLM_RANKED_SEARCH_PROVIDER_NAME, elapsed); } @@ -1985,9 +1982,9 @@ export class SettingsEditor2 extends EditorPane { } private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, providerName: string, token: CancellationToken): Promise { - this.stopWatch.reset(); + const stopWatch = new StopWatch(false); const result = await this._searchPreferencesModel(this.defaultSettingsEditorModel, searchProvider, token); - this.stopWatch.stop(); + stopWatch.stop(); if (token.isCancellationRequested) { // Handle cancellation like this because cancellation is lost inside the search provider due to async/await @@ -2001,7 +1998,7 @@ export class SettingsEditor2 extends EditorPane { // Only log the elapsed time if there are actual results. if (result && result.filterMatches.length > 0) { - const elapsed = this.stopWatch.elapsed(); + const elapsed = stopWatch.elapsed(); this.logSearchPerformance(providerName, elapsed); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 7c5d7df73e5..5d1c11fac90 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -156,6 +156,11 @@ export const tocData: ITOCEntry = { id: 'workbench/screencastmode', label: localize('screencastMode', "Screencast Mode"), settings: ['screencastMode.*'] + }, + { + id: 'workbench/browser', + label: localize('browser', "Browser"), + settings: ['workbench.browser.*'] } ] }, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 9e2704a79b6..eaf1dfe189a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -751,8 +751,8 @@ export function inspectSetting(key: string, target: SettingsTarget, languageFilt return { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector: languageFilter }; } -function sanitizeId(id: string): string { - return id.replace(/[\.\/]/, '_'); +export function sanitizeId(id: string): string { + return id.replace(/[\.\/]/g, '_'); } export function settingKeyToDisplayFormat(key: string, groupId: string = '', isLanguageTagSetting: boolean = false): { category: string; label: string } { diff --git a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts index 98421c6ea63..0fd6956d676 100644 --- a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts +++ b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { settingKeyToDisplayFormat, parseQuery, IParsedQuery } from '../../browser/settingsTreeModels.js'; +import { settingKeyToDisplayFormat, parseQuery, IParsedQuery, sanitizeId } from '../../browser/settingsTreeModels.js'; suite('SettingsTree', () => { test('settingKeyToDisplayFormat', () => { @@ -329,5 +329,22 @@ suite('SettingsTree', () => { }); }); + test('sanitizeId replaces all dots and slashes', () => { + assert.deepStrictEqual( + [ + sanitizeId('root.editor.font.size'), + sanitizeId('group/subgroup/setting.key'), + sanitizeId('no-special-chars'), + sanitizeId('single.dot'), + ], + [ + 'root_editor_font_size', + 'group_subgroup_setting_key', + 'no-special-chars', + 'single_dot', + ] + ); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 530b0c4ae0f..2d7e4bb96ef 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -1003,7 +1003,6 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I if (e.handled) { logService.info(`Error handled: Not showing a notification for the error.`); - console.log(`Error handled: Not showing a notification for the error.`); } else if (!this._reloadWindowShown) { this._reloadWindowShown = true; dialogService.confirm({ diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index d38a51b0654..1dc0f055c9c 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -15,7 +15,6 @@ import { IStatusbarEntry, IStatusbarService, StatusbarAlignment as MainThreadSta import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { Iterable } from '../../../../base/common/iterator.js'; import { ITitleService } from '../../../services/title/browser/titleService.js'; import { IEditorGroupContextKeyProvider, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -30,7 +29,7 @@ const ActiveRepositoryContextKeys = { }; export class SCMActiveRepositoryController extends Disposable implements IWorkbenchContribution { - private readonly _repositories: IObservable>; + private readonly _visibleRepositories: IObservable; private readonly _activeRepositoryHistoryItemRefName: IObservable; private readonly _countBadgeConfig: IObservable<'all' | 'focused' | 'off'>; private readonly _countBadgeRepositories: IObservable }[]>; @@ -60,9 +59,9 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe this._countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); - this._repositories = observableFromEvent(this, - Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), - () => Iterable.filter(this.scmService.repositories, r => r.provider.isHidden !== true)); + this._visibleRepositories = observableFromEvent(this, + Event.any(this.scmViewService.onDidChangeVisibleRepositories, this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmViewService.visibleRepositories); this._activeRepositoryHistoryItemRefName = derived(reader => { const activeRepository = this.scmViewService.activeRepository.read(reader); @@ -75,8 +74,8 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe this._countBadgeRepositories = derived(this, reader => { switch (this._countBadgeConfig.read(reader)) { case 'all': { - const repositories = this._repositories.read(reader); - return [...Iterable.map(repositories, r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; + const repositories = this._visibleRepositories.read(reader); + return repositories.map(r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) })); } case 'focused': { const activeRepository = this.scmViewService.activeRepository.read(reader); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index ab1900fc1db..88c5c132598 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -1706,7 +1706,7 @@ export class SCMHistoryViewPane extends ViewPane { compact: true, showPointer: true }, - content: new MarkdownString(localize('scmGraphViewOutdated', "Please refresh the graph using the refresh action ($(refresh))."), { supportThemeIcons: true }), + content: new MarkdownString(localize('scmGraphViewOutdated', "Please refresh the graph using the refresh action ({0}).", '$(refresh)'), { supportThemeIcons: true }), position: { hoverPosition: HoverPosition.BELOW } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 8d449a07b3e..0c4af97e65e 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -55,7 +55,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ASK_QUICK_QUESTION_ACTION_ID } from '../../chat/browser/actions/chatQuickInputActions.js'; -import { IQuickChatService } from '../../chat/browser/chat.js'; +import { IChatWidgetService, IQuickChatService } from '../../chat/browser/chat.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ICustomEditorLabelService } from '../../../services/editor/common/customEditorLabelService.js'; @@ -140,7 +140,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, preserveFocus: event.inBackground, forcePinned: event.inBackground }) + accept: (keyMods, event) => this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, preserveFocus: event.inBackground, forcePinned: event.inBackground }), + attach: (keyMods, event) => { + // Only support adding context to chat when shift is pressed + if (keyMods.shift) { + const widget = this.chatWidgetService.lastFocusedWidget; + if (widget && resource) { + widget.attachmentModel.addContext(widget.attachmentModel.asFileVariableEntry(resource)); + } + return; + } + + // Fallback to accept behavior. + this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, preserveFocus: event.inBackground, forcePinned: event.inBackground }); + } }; } diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 84a3c98a421..8edf55d5cec 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -24,6 +24,8 @@ import { prepareQuery, IPreparedQuery, scoreFuzzy2, pieceToQuery } from '../../. import { IMatch } from '../../../../base/common/filters.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IChatWidgetService } from '../../chat/browser/chat.js'; +import { ISymbolVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; export interface ISymbolQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { score?: number; @@ -64,7 +66,8 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider this.openSymbol(provider, symbol, token, { keyMods, preserveFocus: event.inBackground, forcePinned: event.inBackground }), + attach: (keyMods, event) => { + // Only support adding context to chat when shift is pressed + if (keyMods.shift) { + const widget = this.chatWidgetService.lastFocusedWidget; + if (widget) { + const entry: ISymbolVariableEntry = { + kind: 'symbol', + id: JSON.stringify({ uri: symbolUri.toString(), range: symbol.location.range }), + name: symbol.name, + value: symbol.location, + symbolKind: symbol.kind, + }; + widget.attachmentModel.addContext(entry); + } + return; + } + + // Fallback to accept behavior. + this.openSymbol(provider, symbol, token, { keyMods, preserveFocus: event.inBackground, forcePinned: event.inBackground }); + }, }); } diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index b10f117bc6e..15cd622c7cd 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -360,7 +360,7 @@ export class TaskQuickPick extends Disposable { public static getSettingEntry(configurationService: IConfigurationService, type: string): (ITaskTwoLevelQuickPickEntry & { settingType: string }) | undefined { if (configurationService.getValue(`${type}.autoDetect`) === 'off') { return { - label: nls.localize('TaskQuickPick.changeSettingsOptions', "$(gear) {0} task detection is turned off. Enable {1} task detection...", + label: '$(gear) ' + nls.localize('TaskQuickPick.changeSettingsOptions', "{0} task detection is turned off. Enable {1} task detection...", type[0].toUpperCase() + type.slice(1), type), task: null, settingType: type, diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 2cf3ac03c1e..348a6988d91 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -177,6 +177,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { private _terminalTabActions = [{ id: RerunForActiveTerminalCommandId, label: nls.localize('rerunTask', 'Rerun Task'), icon: rerunTaskIcon }]; private _taskTerminalActive: IContextKey; private readonly _taskStartTimes = new Map(); + private readonly _capturedTaskVariables = new Map(); taskShellIntegrationStartSequence(cwd: string | URI | undefined): string { return ( @@ -898,6 +899,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (this._busyTasks[mapKey]) { delete this._busyTasks[mapKey]; } + if (event.capturedVariables) { + this._registerCapturedVariables(event.capturedVariables); + } this._fireTaskEvent(TaskEvent.inactive(task, terminal?.instanceId, this._takeTaskDuration(terminal?.instanceId))); if (eventCounter === 0) { if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && @@ -1170,6 +1174,15 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return Date.now() - startTime; } + private _registerCapturedVariables(capturedVariables: ReadonlyMap): void { + for (const [name, value] of capturedVariables) { + this._capturedTaskVariables.set(name, value); + if (!this._configurationResolverService.resolvableVariables.has(`taskVar:${name}`)) { + this._configurationResolverService.contributeVariable(`taskVar:${name}`, async () => this._capturedTaskVariables.get(name)); + } + } + } + private _createTerminalName(task: CustomTask | ContributedTask): string { const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; return needsFolderQualification ? task.getQualifiedLabel() : (task.configurationProperties.name || ''); diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index 8d937a1e5b5..56eb132182a 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -384,8 +384,8 @@ const runOptions: IJSONSchema = { }, runOn: { type: 'string', - enum: ['default', 'folderOpen'], - description: nls.localize('JsonSchema.tasks.runOn', 'Configures when the task should be run. If set to folderOpen, then the task will be run automatically when the folder is opened.'), + enum: ['default', 'folderOpen', 'worktreeCreated'], + description: nls.localize('JsonSchema.tasks.runOn', 'Configures when the task should be run. If set to folderOpen, then the task will be run automatically when the folder is opened. If set to worktreeCreated, then the task will be run automatically when an Agent Session worktree is created.'), default: 'default' }, instanceLimit: { diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index 8ede878caca..71b352858dd 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -24,11 +24,12 @@ export const enum ProblemCollectorEventKind { export interface IProblemCollectorEvent { kind: ProblemCollectorEventKind; + capturedVariables?: ReadonlyMap; } namespace IProblemCollectorEvent { - export function create(kind: ProblemCollectorEventKind) { - return Object.freeze({ kind }); + export function create(kind: ProblemCollectorEventKind, capturedVariables?: ReadonlyMap) { + return Object.freeze({ kind, capturedVariables }); } } @@ -563,7 +564,8 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement } if (this._activeBackgroundMatchers.delete(background.key)) { this.resetCurrentResource(); - this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingEnds)); + const capturedVariables = matches.groups ? new Map(Object.entries(matches.groups)) : undefined; + this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingEnds, capturedVariables)); result = true; this.lines.push(line); const owner = background.matcher.owner; diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index e90a90ea3b9..a6f14097d8f 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -716,6 +716,8 @@ export namespace RunOnOptions { switch (value.toLowerCase()) { case 'folderopen': return Tasks.RunOnOptions.folderOpen; + case 'worktreecreated': + return Tasks.RunOnOptions.worktreeCreated; case 'default': default: return Tasks.RunOnOptions.default; diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index c7b6427e551..5e6ab552216 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -586,7 +586,8 @@ export interface IConfigurationProperties { export enum RunOnOptions { default = 1, - folderOpen = 2 + folderOpen = 2, + worktreeCreated = 3 } export const enum InstancePolicy { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index b54f86b3ad3..8bfd7b0d910 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -1154,7 +1154,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { */ sendPath(originalPath: string | URI, shouldExecute: boolean): Promise; - runCommand(command: string, shouldExecute?: boolean, commandId?: string): Promise; + runCommand(command: string, shouldExecute?: boolean, commandId?: string, bracketedPasteMode?: boolean): Promise; /** * Takes a path and returns the properly escaped path to send to a given shell. On Windows, this diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index b158684ae0e..8b40d469375 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -13,7 +13,6 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -61,7 +60,6 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -158,7 +156,7 @@ export class TerminalEditor extends EditorPane { private _updateTabActionBar(profiles: ITerminalProfile[]): void { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); + const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -182,7 +180,7 @@ export class TerminalEditor extends EditorPane { if (action instanceof MenuItemAction) { const location = { viewColumn: ACTIVE_GROUP }; this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); + const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate }); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); return this._newDropdown.value; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 93a50146bc1..ca7d1e3edeb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -877,6 +877,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { lineDataEventAddon.setOperatingSystem(this._processManager.os); } xterm.raw.options.windowsPty = processTraits.windowsPty; + // Enable reflow cursor to avoid prompt loss: https://github.com/microsoft/vscode/issues/274372 + xterm.raw.options.reflowCursorLine = processTraits?.windowsPty?.backend === 'conpty' && !!this._terminalConfigurationService.config.windowsUseConptyDll; })); this._register(this._processManager.onRestoreCommands(e => this.xterm?.shellIntegration.deserialize(e))); @@ -972,7 +974,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); } - async runCommand(commandLine: string, shouldExecute: boolean, commandId?: string): Promise { + async runCommand(commandLine: string, shouldExecute: boolean, commandId?: string, forceBracketedPasteMode?: boolean): Promise { let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection); const siInjectionEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) === true; const timeoutMs = getShellIntegrationTimeout( @@ -1018,8 +1020,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // is being evaluated await timeout(100); } - // Use bracketed paste mode only when not running the command - await this.sendText(commandLine, shouldExecute, !shouldExecute); + // By default, use bracketed paste mode only when not running the command; callers can override + // this by explicitly enabling it via the bracketedPasteMode argument. + await this.sendText(commandLine, shouldExecute, !shouldExecute || forceBracketedPasteMode); } detachFromElement(): void { @@ -1129,10 +1132,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Skip processing by xterm.js of keyboard events that resolve to commands defined in - // the commandsToSkipShell setting. Ensure sendKeybindingsToShell is respected here - // which will disable this special handling and always opt to send the keystroke to the - // shell process - if (!this._terminalConfigurationService.config.sendKeybindingsToShell && resolveResult.kind === ResultKind.KbFound && resolveResult.commandId && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { + // the commandsToSkipShell setting, or that use the Meta. + // The metaKey check is needed because when a shell like fish enables the kitty + // keyboard protocol, xterm.js encodes Meta-modified keys as CSI u sequences and + // consumes them via preventDefault. The (non-kitty) traditional xterm.js handler already skips + // Meta keys so they bubble up naturally, but the kitty handler does not. + if (!this._terminalConfigurationService.config.sendKeybindingsToShell && resolveResult.kind === ResultKind.KbFound && resolveResult.commandId && (event.metaKey || this._skipTerminalCommands.some(k => k === resolveResult.commandId))) { event.preventDefault(); return false; } @@ -1340,10 +1345,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.focus(force); } - async sendText(text: string, shouldExecute: boolean, bracketedPasteMode?: boolean): Promise { + async sendText(text: string, shouldExecute: boolean, forceBracketedPasteMode?: boolean): Promise { // Apply bracketed paste sequences if the terminal has the mode enabled, this will prevent // the text from triggering keybindings and ensure new lines are handled properly - if (bracketedPasteMode && this.xterm?.raw.modes.bracketedPasteMode) { + if (forceBracketedPasteMode && this.xterm?.raw.modes.bracketedPasteMode) { text = `\x1b[200~${text}\x1b[201~`; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 281d624fcec..4e645b1cd3f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -8,7 +8,6 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Schemas } from '../../../../base/common/network.js'; import { localize, localize2 } from '../../../../nls.js'; import { IMenu, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionTerminalProfile, ITerminalProfile, TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { ResourceContextKey } from '../../../common/contextkeys.js'; @@ -782,20 +781,15 @@ export function setupTerminalMenus(): void { } } -export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore, configurationService: IConfigurationService): { +export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore): { dropdownAction: IAction; dropdownMenuActions: IAction[]; className: string; dropdownIcon?: string; } { - const shouldElevateAiProfiles = configurationService.getValue(TerminalSettingId.ExperimentalAiProfileGrouping); profiles = profiles.filter(e => !e.isAutoDetected); - const [aiProfiles, otherProfiles] = shouldElevateAiProfiles - ? splitProfiles(profiles) - : [[], profiles]; - const [aiContributedProfiles, otherContributedProfiles] = shouldElevateAiProfiles - ? splitContributedProfiles(contributedProfiles) - : [[], contributedProfiles]; + const [aiProfiles, otherProfiles] = splitProfiles(profiles); + const [aiContributedProfiles, otherContributedProfiles] = splitContributedProfiles(contributedProfiles); const dropdownActions: IAction[] = []; const submenuActions: IAction[] = []; const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && hasKey(location, { viewColumn: true }) && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 1733e24d83e..72c54a5518d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -453,6 +453,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const workspaceFolder = terminalEnvironment.getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); + this._logService.debug(`Resolving environment (useShellEnvironment=${shellLaunchConfig.useShellEnvironment}, platformKey=${platformKey}, envFromConfig=${envFromConfigValue ? Object.keys(envFromConfigValue).join(',') : 'none'})`); let baseEnv: IProcessEnvironment; if (shellLaunchConfig.useShellEnvironment) { @@ -460,11 +461,14 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce if (!shellEnv) { throw new BugIndicatingError('Cannot fetch shell environment to use'); } + this._logService.debug(`Shell environment resolved with ${Object.keys(shellEnv).length} variables: ${Object.keys(shellEnv).sort().join(', ')}`); baseEnv = shellEnv; } else { baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); + this._logService.debug(`Profile environment resolved with ${Object.keys(baseEnv).length} variables`); } const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._terminalConfigurationService.config.detectLocale, baseEnv); + this._logService.debug(`Terminal environment created with ${Object.keys(env).length} variables: ${Object.keys(env).sort().join(', ')}`); if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts b/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts index 45914710aaa..efa29b29e03 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts @@ -85,7 +85,7 @@ export class TerminalTelemetryContribution extends Disposable implements IWorkbe terminalSessionId: string; }; type TerminalCreationTelemetryClassification = { - owner: 'tyriar'; + owner: 'anthonykim1'; comment: 'Track details about terminal creation, such as the shell type'; location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location of the terminal.' }; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 01632bee153..9a745727415 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -289,7 +289,7 @@ export class TerminalViewPane extends ViewPane { case TerminalCommandId.New: { if (action instanceof MenuItemAction) { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate, getKeyBinding: (action: IAction) => this._keybindingService.lookupKeybinding(action.id, this._contextKeyService) @@ -318,15 +318,8 @@ export class TerminalViewPane extends ViewPane { private _updateTabActionBar(profiles: ITerminalProfile[]): void { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); - - this._disposableStore.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSettingId.ExperimentalAiProfileGrouping)) { - const updatedActions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); - this._newDropdown.value?.update(updatedActions.dropdownAction, updatedActions.dropdownMenuActions); - } - })); } override focus() { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index ff974695f69..c5729c39941 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -197,7 +197,7 @@ fi # Allow verifying $BASH_COMMAND doesn't have aliases resolved via history when the right HISTCONTROL # configuration is used -__vsc_regex_histcontrol=".*(erasedups|ignoreboth|ignoredups).*" +__vsc_regex_histcontrol=".*(erasedups|ignoreboth|ignoredups|ignorespace).*" if [[ "${HISTCONTROL:-}" =~ $__vsc_regex_histcontrol ]]; then __vsc_history_verify=0 else diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index afeac192274..72a329b8e39 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -179,11 +179,13 @@ if ($Global:__VSCodeState.IsA11yMode -eq "1") { # Check if the loaded PSReadLine already supports EnableScreenReaderMode $hasScreenReaderParam = (Get-Module -Name PSReadLine) -and (Get-Command Set-PSReadLineOption).Parameters.ContainsKey('EnableScreenReaderMode') - if (-not $hasScreenReaderParam) { + if (-not $hasScreenReaderParam -and $PSVersionTable.PSVersion -ge "7.0") { # The loaded PSReadLine lacks EnableScreenReaderMode (only available in 2.4.4-beta4+). # PowerShell 7.0+ skips autoloading PSReadLine when the OS reports a screen reader active. # When only VS Code's accessibility mode is enabled (no OS screen reader), # it's still loaded and must be removed to load our bundled copy. + # Skip this on Windows PowerShell 5.1 where removing the built-in PSReadLine 2.0.0 + # and replacing it can cause input handling issues (e.g. repeated Enter key presses). if (Get-Module -Name PSReadLine) { Remove-Module PSReadLine -Force } diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 132db6d2982..6f915a27195 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -476,7 +476,7 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.WindowsUseConptyDll]: { restricted: true, - markdownDescription: localize('terminal.integrated.windowsUseConptyDll', "Whether to use the experimental conpty.dll (v1.23.251008001) shipped with VS Code, instead of the one bundled with Windows."), + markdownDescription: localize('terminal.integrated.windowsUseConptyDll', "Whether to use the experimental conpty.dll (v1.25.260303002) shipped with VS Code, instead of the one bundled with Windows."), type: 'boolean', tags: ['preview'], default: false, @@ -604,15 +604,6 @@ const terminalConfiguration: IStringDictionary = { mode: 'auto' } }, - [TerminalSettingId.ExperimentalAiProfileGrouping]: { - markdownDescription: localize('terminal.integrated.experimental.aiProfileGrouping', "Whether to elevate AI-contributed terminal profiles (for example Copilot CLI and Claude Agent) in the new terminal dropdown."), - type: 'boolean', - default: false, - tags: ['experimental'], - experiment: { - mode: 'auto' - } - }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh, git bash\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", '`#terminal.integrated.shellIntegration.decorationsEnabled#`', '`#editor.accessibilitySupport#`'), @@ -634,7 +625,7 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.ShellIntegrationTimeout]: { restricted: true, - markdownDescription: localize('terminal.integrated.shellIntegration.timeout', "Configures the duration in milliseconds to wait for shell integration after launch before declaring it's not there. Set to {0} to wait the minimum time (500ms), the default value {1} means the wait time is variable based on whether shell integration injection is enabled and whether it's a remote window. Consider setting this to a small value if you intentionally disabled shell integration, or a large value if your shell starts very slowly.", '`0`', '`-1`'), + markdownDescription: localize('terminal.integrated.shellIntegration.timeout', "Configures the duration in milliseconds to wait for shell integration after launch before declaring it's not there. Set to {0} to skip the wait entirely. The default value {1} uses a variable wait time based on whether shell integration injection is enabled and whether it's a remote window. Values between 1 and 499 are clamped to 500ms. Consider setting this to {0} if you intentionally disabled shell integration, or a large value if your shell starts very slowly.", '`0`', '`-1`'), type: 'integer', minimum: -1, maximum: 60000, @@ -734,3 +725,22 @@ Registry.as(WorkbenchExtensions.ConfigurationMi return configurationKeyValuePairs; } }]); + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: TerminalContribSettingId.DeprecatedTerminalSandboxNetwork, + migrateFn: (value: { allowedDomains?: string[]; deniedDomains?: string[]; allowTrustedDomains?: boolean }, valueAccessor) => { + const configurationKeyValuePairs: ConfigurationKeyValuePairs = []; + if (value?.allowedDomains !== undefined && valueAccessor(TerminalContribSettingId.TerminalSandboxNetworkAllowedDomains) === undefined) { + configurationKeyValuePairs.push([TerminalContribSettingId.TerminalSandboxNetworkAllowedDomains, { value: value.allowedDomains }]); + } + if (value?.deniedDomains !== undefined && valueAccessor(TerminalContribSettingId.TerminalSandboxNetworkDeniedDomains) === undefined) { + configurationKeyValuePairs.push([TerminalContribSettingId.TerminalSandboxNetworkDeniedDomains, { value: value.deniedDomains }]); + } + if (value?.allowTrustedDomains !== undefined && valueAccessor(TerminalContribSettingId.TerminalSandboxNetworkAllowTrustedDomains) === undefined) { + configurationKeyValuePairs.push([TerminalContribSettingId.TerminalSandboxNetworkAllowTrustedDomains, { value: value.allowTrustedDomains }]); + } + configurationKeyValuePairs.push([TerminalContribSettingId.DeprecatedTerminalSandboxNetwork, { value: undefined }]); + return configurationKeyValuePairs; + } + }]); diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index a724394106a..b797a6d549a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -425,6 +425,8 @@ export function getShellIntegrationTimeout( if (!isNumber(timeoutValue) || timeoutValue < 0) { timeoutMs = siInjectionEnabled ? 5000 : (isRemote ? 3000 : 2000); + } else if (timeoutValue === 0) { + timeoutMs = 0; } else { timeoutMs = Math.max(timeoutValue, 500); } diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index a24b204a899..babbac632fc 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -46,6 +46,10 @@ export const enum TerminalContribSettingId { EnableAutoApprove = TerminalChatAgentToolsSettingId.EnableAutoApprove, ShellIntegrationTimeout = TerminalChatAgentToolsSettingId.ShellIntegrationTimeout, OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation, + DeprecatedTerminalSandboxNetwork = TerminalChatAgentToolsSettingId.DeprecatedTerminalSandboxNetwork, + TerminalSandboxNetworkAllowedDomains = TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, + TerminalSandboxNetworkDeniedDomains = TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, + TerminalSandboxNetworkAllowTrustedDomains = TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, } // HACK: Export some context key strings from `terminalContrib/` that are depended upon elsewhere. diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index 729ef0a01dc..c8bab580ee5 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { Terminal } from '@xterm/xterm'; -import { deepStrictEqual, ok } from 'assert'; +import { deepStrictEqual, ok, strictEqual } from 'assert'; import { importAMDNodeModule } from '../../../../../../amdX.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ITerminalCommand } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; @@ -133,4 +133,16 @@ suite('CommandDetectionCapability', () => { ]); }); }); + + test('should preserve explicit newlines at 80-column wrap boundaries in command output', async () => { + const boundaryWidthLine = 'A'.repeat(80); + await printStandardCommand('$ ', 'cat content.txt', `${boundaryWidthLine}\r\nafter`, undefined, 0); + await printCommandStart('$ '); + + strictEqual(capability.commands.length, 1); + const output = capability.commands[0].getOutput(); + ok(!!output); + ok(output.includes(`${boundaryWidthLine}\nafter\n`)); + ok(!output.includes(`${boundaryWidthLine}after`)); + }); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 8c3c2d9982a..b5f566ce0de 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -134,6 +134,7 @@ suite('Workbench - TerminalInstance', () => { fastScrollSensitivity: 2, mouseWheelScrollSensitivity: 1, unicodeVersion: '6', + commandsToSkipShell: [], shellIntegration: { enabled: true } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.integrationTest.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.integrationTest.ts index 5acf81b2e0b..18c8f428e74 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.integrationTest.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.integrationTest.ts @@ -363,14 +363,14 @@ suite('TerminalProfileService', () => { test('createInstance', async () => { mockTerminalProfileService.setDefaultProfileName(powershellProfile.profileName); - const pick = { ...powershellPick, keyMods: { alt: true, ctrlCmd: false } }; + const pick = { ...powershellPick, keyMods: { alt: true, ctrlCmd: false, shift: false } }; quickInputService.setPick(pick); const result = await terminalProfileQuickpick.showAndGetResult('createInstance'); - deepStrictEqual(result, { config: powershellProfile, keyMods: { alt: true, ctrlCmd: false } }); + deepStrictEqual(result, { config: powershellProfile, keyMods: { alt: true, ctrlCmd: false, shift: false } }); }); test('createInstance with contributed', async () => { - const pick = { ...jsdebugPick, keyMods: { alt: true, ctrlCmd: false } }; + const pick = { ...jsdebugPick, keyMods: { alt: true, ctrlCmd: false, shift: true } }; quickInputService.setPick(pick); const result = await terminalProfileQuickpick.showAndGetResult('createInstance'); const expected = { @@ -380,7 +380,7 @@ suite('TerminalProfileService', () => { options: { color: undefined, icon: 'debug' }, title: jsdebugProfile.title, }, - keyMods: { alt: true, ctrlCmd: false } + keyMods: { alt: true, ctrlCmd: false, shift: true } }; deepStrictEqual(result, expected); }); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index de2f1ae7056..c99f166ad62 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -8,12 +8,14 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../../../base/common/types.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import type { ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; import { trackIdleOnPrompt, waitForIdle, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; -import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker, stripCommandEchoAndPrompt } from './strategyHelpers.js'; +import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; /** * This strategy is used when shell integration is enabled, but rich command detection was not @@ -49,6 +51,7 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute private readonly _instance: ITerminalInstance, private readonly _hasReceivedUserInput: () => boolean, private readonly _commandDetection: ICommandDetectionCapability, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { super(); @@ -58,7 +61,9 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute const store = new DisposableStore(); try { - const idlePromptPromise = trackIdleOnPrompt(this._instance, 1000, store); + const idlePollInterval = this._configurationService.getValue(TerminalChatAgentToolsSettingId.IdlePollInterval) ?? 1000; + + const idlePromptPromise = trackIdleOnPrompt(this._instance, idlePollInterval, store, idlePollInterval); const onDone = Promise.race([ Event.toPromise(this._commandDetection.onCommandFinished, store).then(e => { // When shell integration is basic, it means that the end execution event is @@ -82,7 +87,7 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute }), // A longer idle prompt event is used here as a catch all for unexpected cases where // the end event doesn't fire for some reason. - trackIdleOnPrompt(this._instance, 3000, store).then(() => { + trackIdleOnPrompt(this._instance, idlePollInterval * 3, store, idlePollInterval).then(() => { this._log('onDone long idle prompt'); }), ]); @@ -97,9 +102,9 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute // Wait for the terminal to idle before executing the command this._log('Waiting for idle'); - await waitForIdle(this._instance.onData, 1000); + await waitForIdle(this._instance.onData, idlePollInterval); - setupRecreatingStartMarker( + const markerRecreation = setupRecreatingStartMarker( xterm, this._startMarker, m => this._onDidCreateStartMarker.fire(m), @@ -123,7 +128,8 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute // ^C being sent and also to return the exit code of 130 when from the shell when that // occurs. this._log(`Executing command line \`${commandLine}\``); - this._instance.sendText(commandLine, true); + markerRecreation.dispose(); + this._instance.sendText(commandLine, true, true); // Wait for the next end execution event - note that this may not correspond to the actual // execution requested @@ -150,7 +156,7 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute // Wait for the terminal to idle this._log('Waiting for idle'); - await waitForIdle(this._instance.onData, 1000); + await waitForIdle(this._instance.onData, idlePollInterval); if (token.isCancellationRequested) { throw new CancellationError(); } @@ -163,13 +169,19 @@ export class BasicExecuteStrategy extends Disposable implements ITerminalExecute const commandOutput = finishedCommand?.getOutput(); if (commandOutput !== undefined) { this._log('Fetched output via finished command'); - output = commandOutput; + output = stripCommandEchoAndPrompt(commandOutput, commandLine, this._log.bind(this)); } } if (output === undefined) { try { output = xterm.getContentsAsText(this._startMarker.value, endMarker); this._log('Fetched output via markers'); + + // The marker-based output includes the command echo and trailing + // prompt lines. Strip them to isolate the actual command output. + if (output !== undefined) { + output = stripCommandEchoAndPrompt(output, commandLine, this._log.bind(this)); + } } catch { this._log('Failed to fetch output via markers'); additionalInformationLines.push('Failed to retrieve command output'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts index f95f297a958..805223b03f6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts @@ -160,6 +160,7 @@ export async function trackIdleOnPrompt( instance: ITerminalInstance, idleDurationMs: number, store: DisposableStore, + promptFallbackMs?: number, ): Promise { const idleOnPrompt = new DeferredPromise(); const onData = instance.onData; @@ -176,7 +177,7 @@ export async function trackIdleOnPrompt( } state = TerminalState.PromptAfterExecuting; scheduler.schedule(); - }, 1000)); + }, promptFallbackMs ?? 1000)); // Only schedule when a prompt sequence (A) is seen after an execute sequence (C). This prevents // cases where the command is executed before the prompt is written. While not perfect, sitting // on an A without a C following shortly after is a very good indicator that the command is done diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index ddc2a7f8939..c1f1b210253 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -7,11 +7,13 @@ import type { CancellationToken } from '../../../../../../base/common/cancellati import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; import { waitForIdle, waitForIdleWithPromptHeuristics, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; -import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker, stripCommandEchoAndPrompt } from './strategyHelpers.js'; +import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; /** * This strategy is used when no shell integration is available. There are very few extension APIs @@ -30,6 +32,7 @@ export class NoneExecuteStrategy extends Disposable implements ITerminalExecuteS constructor( private readonly _instance: ITerminalInstance, private readonly _hasReceivedUserInput: () => boolean, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { super(); @@ -50,14 +53,16 @@ export class NoneExecuteStrategy extends Disposable implements ITerminalExecuteS } const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); + const idlePollInterval = this._configurationService.getValue(TerminalChatAgentToolsSettingId.IdlePollInterval) ?? 1000; + // Wait for the terminal to idle before executing the command this._log('Waiting for idle'); - await waitForIdle(this._instance.onData, 1000); + await waitForIdle(this._instance.onData, idlePollInterval); if (token.isCancellationRequested) { throw new CancellationError(); } - setupRecreatingStartMarker( + const markerRecreation = setupRecreatingStartMarker( xterm, this._startMarker, m => this._onDidCreateStartMarker.fire(m), @@ -77,12 +82,45 @@ export class NoneExecuteStrategy extends Disposable implements ITerminalExecuteS // is used as sending ctrl+c before a shell is initialized (eg. PSReadLine) can result // in failure (https://github.com/microsoft/vscode/issues/258989) this._log(`Executing command line \`${commandLine}\``); - this._instance.sendText(commandLine, true); + markerRecreation.dispose(); + const startLine = this._startMarker.value?.line; + this._instance.sendText(commandLine, true, true); + + // Wait for the cursor to move past the command line before + // starting idle detection. Without this, the idle poll may + // resolve immediately on the existing prompt if the shell + // hasn't started processing the command yet. + if (startLine !== undefined) { + this._log('Waiting for cursor to move past start line'); + const cursorMovedPromise = new Promise(resolve => { + const check = () => { + const buffer = xterm.raw.buffer.active; + const cursorLine = buffer.baseY + buffer.cursorY; + if (cursorLine > startLine) { + resolve(); + } + }; + const listener = this._instance.onData(() => check()); + store.add(listener); + check(); + }); + + const cursorMoveTimeout = new Promise<'timeout'>(resolve => { + const handle = setTimeout(() => resolve('timeout'), 5000); + store.add({ dispose: () => clearTimeout(handle) }); + }); + + const raceResult = await Promise.race([cursorMovedPromise, cursorMoveTimeout]); + if (raceResult === 'timeout') { + this._log('Cursor did not move past start line before timeout, proceeding with idle detection'); + } + } + // Assume the command is done when it's idle this._log('Waiting for idle with prompt heuristics'); const promptResultOrAltBuffer = await Promise.race([ - waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000, 10000), + waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, idlePollInterval, idlePollInterval * 10), alternateBufferPromise.then(() => 'alternateBuffer' as const) ]); if (promptResultOrAltBuffer === 'alternateBuffer') { @@ -109,10 +147,24 @@ export class NoneExecuteStrategy extends Disposable implements ITerminalExecuteS try { output = xterm.getContentsAsText(this._startMarker.value, endMarker); this._log('Fetched output via markers'); + + // The marker-based output includes the command echo (the line where the + // command was typed) and the next prompt line. Strip them to isolate + // only the actual command output. The first line always contains the + // command echo (since the start marker is placed at the cursor before + // sendText), and trailing lines that look like shell prompts are removed. + if (output !== undefined) { + output = stripCommandEchoAndPrompt(output, commandLine, this._log.bind(this)); + } } catch { this._log('Failed to fetch output via markers'); additionalInformationLines.push('Failed to retrieve command output'); } + + if (output !== undefined && output.trim().length === 0) { + additionalInformationLines.push('Command produced no output'); + } + return { output, additionalInformation: additionalInformationLines.length > 0 ? additionalInformationLines.join('\n') : undefined, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index b68f7a499d4..cd8ac8edaf2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -8,12 +8,15 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../../../base/common/types.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { isCI } from '../../../../../../base/common/platform.js'; import type { ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { trackIdleOnPrompt, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; -import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker, stripCommandEchoAndPrompt } from './strategyHelpers.js'; +import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; /** * This strategy is used when the terminal has rich shell integration/command detection is @@ -32,6 +35,7 @@ export class RichExecuteStrategy extends Disposable implements ITerminalExecuteS constructor( private readonly _instance: ITerminalInstance, private readonly _commandDetection: ICommandDetectionCapability, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { super(); @@ -48,6 +52,8 @@ export class RichExecuteStrategy extends Disposable implements ITerminalExecuteS } const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); + const idlePollInterval = this._configurationService.getValue(TerminalChatAgentToolsSettingId.IdlePollInterval) ?? 1000; + const onDone = Promise.race([ Event.toPromise(this._commandDetection.onCommandFinished, store).then(e => { this._log('onDone via end event'); @@ -63,12 +69,12 @@ export class RichExecuteStrategy extends Disposable implements ITerminalExecuteS this._log('onDone via terminal disposal'); return { type: 'disposal' } as const; }), - trackIdleOnPrompt(this._instance, 1000, store).then(() => { + trackIdleOnPrompt(this._instance, idlePollInterval, store, idlePollInterval).then(() => { this._log('onDone via idle prompt'); }), ]); - setupRecreatingStartMarker( + const markerRecreation = setupRecreatingStartMarker( xterm, this._startMarker, m => this._onDidCreateStartMarker.fire(m), @@ -78,7 +84,8 @@ export class RichExecuteStrategy extends Disposable implements ITerminalExecuteS // Execute the command this._log(`Executing command line \`${commandLine}\``); - this._instance.runCommand(commandLine, true, commandId); + markerRecreation.dispose(); + this._instance.runCommand(commandLine, true, commandId, true); // Wait for the terminal to idle this._log('Waiting for done event'); @@ -109,13 +116,23 @@ export class RichExecuteStrategy extends Disposable implements ITerminalExecuteS const commandOutput = finishedCommand?.getOutput(); if (commandOutput !== undefined) { this._log('Fetched output via finished command'); - output = commandOutput; + // On some platforms (e.g. Windows/PowerShell), shell integration + // markers can misfire and getOutput() includes the command echo. + // Strip it defensively — the function is a no-op when the output + // is already clean. + output = stripCommandEchoAndPrompt(commandOutput, commandLine, this._log.bind(this)); } } if (output === undefined) { try { output = xterm.getContentsAsText(this._startMarker.value, endMarker); this._log('Fetched output via markers'); + + // The marker-based output includes the command echo and trailing + // prompt lines. Strip them to isolate the actual command output. + if (output !== undefined) { + output = stripCommandEchoAndPrompt(output, commandLine, this._log.bind(this)); + } } catch { this._log('Failed to fetch output via markers'); additionalInformationLines.push('Failed to retrieve command output'); @@ -142,6 +159,11 @@ export class RichExecuteStrategy extends Disposable implements ITerminalExecuteS } private _log(message: string) { - this._logService.debug(`RunInTerminalTool#Rich: ${message}`); + const msg = `RunInTerminalTool#Rich: ${message}`; + if (isCI) { + this._logService.info(msg); + } else { + this._logService.debug(msg); + } } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts index 5c63b233ec2..c372a8c78af 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts @@ -18,7 +18,7 @@ export function setupRecreatingStartMarker( fire: (marker: IXtermMarker | undefined) => void, store: DisposableStore, log?: (message: string) => void, -): void { +): IDisposable { const markerListener = new MutableDisposable(); const recreateStartMarker = () => { if (store.isDisposed) { @@ -43,6 +43,12 @@ export function setupRecreatingStartMarker( fire(undefined); })); store.add(startMarker); + + // Return a disposable that stops the recreation loop without clearing + // the current marker. Callers should dispose this before sending a + // command so that prompt re-renders (e.g. PSReadLine transient prompts) + // don't move the start marker past the command output. + return toDisposable(() => markerListener.dispose()); } export function createAltBufferPromise( @@ -70,3 +76,204 @@ export function createAltBufferPromise( return deferred.p; } + +/** + * Strips the command echo and trailing prompt lines from marker-based terminal output. + * Without shell integration (or when `getOutput()` is unavailable), `getContentsAsText` + * captures the entire terminal buffer between the start and end markers, which includes: + * 1. The command echo line (what `sendText` wrote) + * 2. The actual command output + * 3. The next shell prompt line(s) + * + * This function removes (1) and (3) to isolate the actual output. + */ +export function stripCommandEchoAndPrompt(output: string, commandLine: string, log?: (message: string) => void): string { + log?.(`stripCommandEchoAndPrompt input: output length=${output.length}, commandLine length=${commandLine.length}`); + + const result = _stripCommandEchoAndPromptOnce(output, commandLine, log); + + // After stripping the first command echo and trailing prompt, the remaining + // content may still contain the command re-echoed by the shell (prompt + echo). + // This happens when the terminal buffer captures both the raw sendText output + // and the shell's subsequent prompt + command echo. If the command appears again + // in the remaining text, strip it one more time. + if (result.trim().length > 0 && findCommandEcho(result, commandLine)) { + return _stripCommandEchoAndPromptOnce(result, commandLine, log); + } + + return result; +} + +function _stripCommandEchoAndPromptOnce(output: string, commandLine: string, log?: (message: string) => void): string { + // Strip leading lines that are part of the command echo using findCommandEcho. + // Allow suffix matching to handle partial command echoes from getOutput() + // where the prompt line is not included. + const echoResult = findCommandEcho(output, commandLine, /*allowSuffixMatch*/ true); + const lines = echoResult ? echoResult.linesAfter : output.split('\n'); + const startIndex = 0; + + // Use evidence from the prompt prefix (content before the command echo) + // to narrow down which trailing prompt patterns to check. + const promptBefore = echoResult?.contentBefore ?? ''; + const isUnixAt = /\w+@[\w.-]+:/.test(promptBefore); + const isUnixHost = !isUnixAt && /[\w.-]+:\S/.test(promptBefore); + const isUnix = isUnixAt || isUnixHost; + const isPowerShell = /^PS\s/i.test(promptBefore); + const isCmd = !isPowerShell && /^[A-Z]:\\/.test(promptBefore); + const isStarship = /\u276f/.test(promptBefore); + const isPython = />>>/.test(promptBefore); + const knownPrompt = isUnix || isPowerShell || isCmd || isStarship || isPython; + + // Strip trailing lines that are part of the next shell prompt. Prompts may + // span multiple lines due to terminal column wrapping. We strip from the + // bottom any line that matches a known prompt pattern. Patterns are + // intentionally anchored and specific to avoid stripping legitimate output + // that happens to end with characters like $, #, %, or >. + let endIndex = lines.length; + let trailingStrippedCount = 0; + const maxTrailingPromptLines = 2; + while (endIndex > startIndex) { + const line = lines[endIndex - 1].trimEnd(); + if (line.length === 0) { + endIndex--; + continue; + } + if (trailingStrippedCount >= maxTrailingPromptLines) { + break; + } + + // Complete (self-contained) prompt patterns: these have a recognizable + // prefix and a trailing marker ($, #, >). After stripping one complete + // prompt line, stop — lines above it are command output, not wrapped + // prompt continuation lines. + const isCompletePrompt = + // Bash/zsh: user@host:path ending with $ or # + // e.g., "user@host:~/src $ " or "root@server:/var/log# " + ((!knownPrompt || isUnixAt) && /^\s*\w+@[\w.-]+:.*[#$]\s*$/.test(line)) || + // hostname:path user$ or hostname:path user# + // e.g., "dsm12-be220-abc:testWorkspace runner$" + ((!knownPrompt || isUnixHost) && /^\s*[\w.-]+:\S.*\s\w+[#$]\s*$/.test(line)) || + // PowerShell: PS C:\path> + ((!knownPrompt || isPowerShell) && /^PS\s+[A-Z]:\\.*>\s*$/.test(line)) || + // Windows cmd: C:\path> + ((!knownPrompt || isCmd) && /^[A-Z]:\\.*>\s*$/.test(line)) || + // Starship prompt character + // allow-any-unicode-next-line + ((!knownPrompt || isStarship) && /\u276f\s*$/.test(line)) || + // Python REPL + ((!knownPrompt || isPython) && /^>>>\s*$/.test(line)); + + // Fragment/partial prompt patterns: these represent pieces of a prompt + // that wraps across multiple terminal lines due to column width. + const isPromptFragment = + // Wrapped fragment ending with $ or # (e.g. "er$", "ts/testWorkspace$") + ((!knownPrompt || isUnix) && /^\s*[\w/.-]+[#$]\s*$/.test(line)) || + // Bracketed prompt start: [ hostname:/path or [ user@host:/path + // e.g., "[ alex@MacBook-Pro:/Users/alex/src/vscode4/extensions/vscode-api-test" + // e.g., "[W007DV9PF9-1:~/vss/_work/1/s/extensions/vscode-api-tests/testWorkspace] cloudte" + ((!knownPrompt || isUnix) && /^\[\s*[\w.-]+(@[\w.-]+)?:[~\/]/.test(line)) || + // Wrapped continuation: user@host:path or hostname:path (no trailing $) + // Only matched after we've already stripped a prompt fragment below. + // e.g., "cloudtest@host:/mnt/vss/.../vscode-api-tes" or "dsm12-abc:testWorkspace runn" + ((!knownPrompt || isUnix) && trailingStrippedCount > 0 && /^\s*[\w][-\w.]*(@[\w.-]+)?:\S/.test(line)) || + // Bracketed prompt end: ...] $ or ...] # + // e.g., "s/testWorkspace (main**) ] $ " + ((!knownPrompt || isUnix) && /\]\s*[#$]\s*$/.test(line)); + + if (isCompletePrompt) { + endIndex--; + trailingStrippedCount++; + break; // Complete prompt = nothing above can be prompt wrap + } else if (isPromptFragment) { + endIndex--; + trailingStrippedCount++; + } else { + break; + } + } + + const result = lines.slice(startIndex, endIndex).join('\n'); + log?.(`stripCommandEchoAndPrompt result: length=${result.length} (startIndex=${startIndex}, endIndex=${endIndex}, totalLines=${lines.length})`); + return result; +} + +export function findCommandEcho(output: string, commandLine: string, allowSuffixMatch?: boolean): { contentBefore: string; linesAfter: string[] } | undefined { + const trimmedCommand = commandLine.trim(); + if (trimmedCommand.length === 0) { + return undefined; + } + + // Strip newlines from the output so we can find the command as a + // contiguous substring even when terminal wrapping splits it across lines. + const { strippedOutput, indexMapping } = stripNewLinesAndBuildMapping(output); + const matchIndex = strippedOutput.indexOf(trimmedCommand); + + let matchEndInStripped: number; + let contentBefore: string; + + if (matchIndex !== -1) { + // Full command found in the output + contentBefore = strippedOutput.substring(0, matchIndex).trim(); + matchEndInStripped = matchIndex + trimmedCommand.length - 1; + } else if (allowSuffixMatch) { + // If the full command wasn't found, check if the output starts with a + // suffix of the command. This happens when getOutput() doesn't include + // the prompt line, so only the wrapped continuation of the command echo + // appears at the beginning of the output. + let suffixLen = 0; + for (let len = trimmedCommand.length - 1; len >= 1; len--) { + const suffix = trimmedCommand.substring(trimmedCommand.length - len); + if (strippedOutput.startsWith(suffix)) { + // Require the suffix to start mid-word in the command (not at + // a word boundary). A word-boundary match like "MARKER_123" + // matching the tail of "echo MARKER_123" is almost certainly + // actual output, not a wrapped command continuation. + const charBefore = trimmedCommand[trimmedCommand.length - len - 1]; + if (charBefore !== undefined && charBefore !== ' ' && charBefore !== '\t') { + suffixLen = len; + } + break; + } + } + if (suffixLen === 0) { + return undefined; + } + contentBefore = ''; + matchEndInStripped = suffixLen - 1; + } else { + return undefined; + } + + // Map the match end back to the original output position and determine + // which line it falls on to split linesAfter. + const originalEnd = indexMapping[matchEndInStripped]; + + const lines = output.split('\n'); + let echoEndLine = 0; + let offset = 0; + for (let i = 0; i < lines.length; i++) { + const lineEnd = offset + lines[i].length; // excludes the \n + if (offset <= originalEnd && originalEnd <= lineEnd) { + echoEndLine = i + 1; + break; + } + offset = lineEnd + 1; // +1 for the \n + } + + return { + contentBefore, + linesAfter: lines.slice(echoEndLine), + }; +} + +export function stripNewLinesAndBuildMapping(output: string): { strippedOutput: string; indexMapping: number[] } { + const indexMapping: number[] = []; + const strippedChars: string[] = []; + for (let i = 0; i < output.length; i++) { + if (output[i] !== '\n') { + strippedChars.push(output[i]); + indexMapping.push(i); + } + } + return { strippedOutput: strippedChars.join(''), indexMapping }; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts index 8741a9f1d39..184e49eb75a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts @@ -14,13 +14,29 @@ export function getOutput(instance: ITerminalInstance, startMarker?: IXtermMarke return ''; } const buffer = instance.xterm.raw.buffer.active; - const startLine = Math.max(startMarker?.line ?? 0, 0); + let startLine = Math.max(startMarker?.line ?? 0, 0); + while (startLine > 0 && buffer.getLine(startLine)?.isWrapped) { + startLine--; + } const endLine = buffer.length; - const lines: string[] = new Array(endLine - startLine); + const lines: string[] = []; + let currentLine = ''; for (let y = startLine; y < endLine; y++) { const line = buffer.getLine(y); - lines[y - startLine] = line ? line.translateToString(true) : ''; + if (!line) { + continue; + } + // NOTE: xterm stores wrapping state on the *next* line, not the current one. + const isWrapped = !!buffer.getLine(y + 1)?.isWrapped; + currentLine += line.translateToString(!isWrapped); + if (!isWrapped) { + lines.push(currentLine); + currentLine = ''; + } + } + if (currentLine) { + lines.push(currentLine); } let output = lines.join('\n'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 7cbf89ca9f3..4edc69a5fad 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -77,6 +77,14 @@ export function sanitizeTerminalOutput(output: string): string { return sanitized; } +/** + * Normalizes command text for UI display by removing unnecessary quote and forward slash + * escaping artifacts (for example: \" \' \/) commonly produced in streamed tool-call JSON. + */ +export function normalizeTerminalCommandForDisplay(commandLine: string): string { + return commandLine.replace(/\\(["'\/])/g, '$1'); +} + export function generateAutoApproveActions(commandLine: string, subCommands: string[], autoApproveResult: { subCommandResults: ICommandApprovalResultWithReason[]; commandLineResult: ICommandApprovalResultWithReason }): ToolConfirmationAction[] { const actions: ToolConfirmationAction[] = []; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalToolTelemetry.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalToolTelemetry.ts index 047c59fab39..2267111467e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalToolTelemetry.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalToolTelemetry.ts @@ -65,7 +65,7 @@ export class RunInTerminalToolTelemetry { autoApproveDefault: boolean | undefined; }; type TelemetryClassification = { - owner: 'tyriar'; + owner: 'meganrogge'; comment: 'Understanding the auto approve behavior of the runInTerminal tool'; terminalToolSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The session ID for this particular terminal tool invocation.' }; @@ -94,6 +94,7 @@ export class RunInTerminalToolTelemetry { error: string | undefined; isBackground: boolean; isNewSession: boolean; + isSandboxWrapped: boolean; shellIntegrationQuality: ShellIntegrationQuality; outputLineCount: number; timingConnectMs: number; @@ -122,6 +123,7 @@ export class RunInTerminalToolTelemetry { toolEditedCommand: 0 | 1; isBackground: 0 | 1; isNewSession: 0 | 1; + isSandbox: 0 | 1; outputLineCount: number; nonZeroExitCode: -1 | 0 | 1; timingConnectMs: number; @@ -139,7 +141,7 @@ export class RunInTerminalToolTelemetry { inputToolFreeFormInputCount: number; }; type TelemetryClassification = { - owner: 'tyriar'; + owner: 'meganrogge'; comment: 'Understanding the usage of the runInTerminal tool'; terminalSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The session ID of the terminal instance.' }; @@ -151,6 +153,7 @@ export class RunInTerminalToolTelemetry { toolEditedCommand: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the tool edited the command' }; isBackground: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the command is a background command' }; isNewSession: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether this was the first execution for the terminal session' }; + isSandbox: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the command was run inside the terminal sandbox' }; outputLineCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'How many lines of output were produced, this is -1 when isBackground is true or if there\'s an error' }; nonZeroExitCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the command exited with a non-zero code (-1=error/unknown, 0=zero exit code, 1=non-zero)' }; timingConnectMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'How long the terminal took to start up and connect to' }; @@ -177,6 +180,7 @@ export class RunInTerminalToolTelemetry { toolEditedCommand: state.didToolEditCommand ? 1 : 0, isBackground: state.isBackground ? 1 : 0, isNewSession: state.isNewSession ? 1 : 0, + isSandbox: state.isSandboxWrapped ? 1 : 0, outputLineCount: state.outputLineCount, nonZeroExitCode: state.exitCode === undefined ? -1 : state.exitCode === 0 ? 0 : 1, timingConnectMs: state.timingConnectMs, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index eda700886bd..fa39d76ec7d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -23,6 +23,7 @@ import { IExecution, IPollingResult, OutputMonitorState } from './tools/monitori import { Event } from '../../../../../base/common/event.js'; import { IReconnectionTaskData } from '../../../tasks/browser/terminalTaskSystem.js'; import { isString } from '../../../../../base/common/types.js'; +import type { IMarker as IXtermMarker } from '@xterm/xterm'; export function getTaskDefinition(id: string) { @@ -180,7 +181,8 @@ export async function collectTerminalResults( disposableStore: DisposableStore, isActive?: (task: Task) => Promise, dependencyTasks?: Task[], - taskService?: ITaskService + taskService?: ITaskService, + startMarkersByTerminalInstanceId?: Map ): Promise `\`${n}\``).join(', ')}`) }); const terminalPromises = terminals.map(async (instance) => { + const startMarker = startMarkersByTerminalInstanceId + ? startMarkersByTerminalInstanceId.get(instance.instanceId) + : instance.registerMarker(); let terminalTask = task; // For composite tasks, find the actual dependency task running in this terminal @@ -237,7 +242,7 @@ export async function collectTerminalResults( } const execution: IExecution = { - getOutput: () => getOutput(instance) ?? '', + getOutput: (marker) => getOutput(instance, marker ?? startMarker) ?? '', task: terminalTask, isActive: isActive ? () => isActive(terminalTask) : undefined, instance, @@ -258,32 +263,47 @@ export async function collectTerminalResults( } } - const hasProblemMatchers = terminalTask.configurationProperties.problemMatchers && terminalTask.configurationProperties.problemMatchers.length > 0; - const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, hasProblemMatchers ? taskProblemPollFn : undefined, invocationContext, token, task._label)); - await Promise.race([ - Event.toPromise(outputMonitor.onDidFinishCommand), - Event.toPromise(token.onCancellationRequested as Event) - ]); - const pollingResult = outputMonitor.pollingResult; - return { - name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', - output: pollingResult?.output ?? '', - pollDurationMs: pollingResult?.pollDurationMs ?? 0, - resources: pollingResult?.resources, - state: pollingResult?.state || OutputMonitorState.Idle, - inputToolManualAcceptCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualAcceptCount ?? 0, - inputToolManualRejectCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualRejectCount ?? 0, - inputToolManualChars: outputMonitor.outputMonitorTelemetryCounters.inputToolManualChars ?? 0, - inputToolAutoAcceptCount: outputMonitor.outputMonitorTelemetryCounters.inputToolAutoAcceptCount ?? 0, - inputToolAutoChars: outputMonitor.outputMonitorTelemetryCounters.inputToolAutoChars ?? 0, - inputToolManualShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualShownCount ?? 0, - inputToolFreeFormInputShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputShownCount ?? 0, - inputToolFreeFormInputCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputCount ?? 0, - }; + try { + const hasProblemMatchers = terminalTask.configurationProperties.problemMatchers && terminalTask.configurationProperties.problemMatchers.length > 0; + const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, hasProblemMatchers ? taskProblemPollFn : undefined, invocationContext, token, task._label)); + await Promise.race([ + Event.toPromise(outputMonitor.onDidFinishCommand), + Event.toPromise(token.onCancellationRequested as Event) + ]); + const pollingResult = outputMonitor.pollingResult; + return { + name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', + output: pollingResult?.output ?? '', + pollDurationMs: pollingResult?.pollDurationMs ?? 0, + resources: pollingResult?.resources, + state: pollingResult?.state || OutputMonitorState.Idle, + inputToolManualAcceptCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualAcceptCount ?? 0, + inputToolManualRejectCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualRejectCount ?? 0, + inputToolManualChars: outputMonitor.outputMonitorTelemetryCounters.inputToolManualChars ?? 0, + inputToolAutoAcceptCount: outputMonitor.outputMonitorTelemetryCounters.inputToolAutoAcceptCount ?? 0, + inputToolAutoChars: outputMonitor.outputMonitorTelemetryCounters.inputToolAutoChars ?? 0, + inputToolManualShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualShownCount ?? 0, + inputToolFreeFormInputShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputShownCount ?? 0, + inputToolFreeFormInputCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputCount ?? 0, + }; + } finally { + startMarker?.dispose(); + } }); const parallelResults = await Promise.all(terminalPromises); results.push(...parallelResults); + + if (startMarkersByTerminalInstanceId) { + const activeInstanceIds = new Set(terminals.map(instance => instance.instanceId)); + for (const [instanceId, marker] of startMarkersByTerminalInstanceId) { + if (!activeInstanceIds.has(instanceId)) { + marker?.dispose(); + startMarkersByTerminalInstanceId.delete(instanceId); + } + } + } + return results; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts index d7e2dea9c4e..e8bd2742546 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -32,6 +32,7 @@ import { CreateAndRunTaskTool, CreateAndRunTaskToolData } from './tools/task/cre import { GetTaskOutputTool, GetTaskOutputToolData } from './tools/task/getTaskOutputTool.js'; import { RunTaskTool, RunTaskToolData } from './tools/task/runTaskTool.js'; import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; +import { ITrustedDomainService } from '../../../url/common/trustedDomainService.js'; import { ITerminalSandboxService, TerminalSandboxService } from '../common/terminalSandboxService.js'; // #region Services @@ -75,64 +76,104 @@ class OutputLocationMigrationContribution extends Disposable implements IWorkben } registerWorkbenchContribution2(OutputLocationMigrationContribution.ID, OutputLocationMigrationContribution, WorkbenchPhase.Eventually); -class ChatAgentToolsContribution extends Disposable implements IWorkbenchContribution { +export class ChatAgentToolsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'terminal.chatAgentTools'; + private readonly _runInTerminalToolRegistration = this._register(new MutableDisposable()); + private _runInTerminalToolRegistrationVersion = 0; + constructor( - @IInstantiationService instantiationService: IInstantiationService, - @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, ) { super(); // #region Terminal - const confirmTerminalCommandTool = instantiationService.createInstance(ConfirmTerminalCommandTool); - this._register(toolsService.registerTool(ConfirmTerminalCommandToolData, confirmTerminalCommandTool)); - const getTerminalOutputTool = instantiationService.createInstance(GetTerminalOutputTool); - this._register(toolsService.registerTool(GetTerminalOutputToolData, getTerminalOutputTool)); - this._register(toolsService.executeToolSet.addTool(GetTerminalOutputToolData)); + const confirmTerminalCommandTool = _instantiationService.createInstance(ConfirmTerminalCommandTool); + this._register(_toolsService.registerTool(ConfirmTerminalCommandToolData, confirmTerminalCommandTool)); + const getTerminalOutputTool = _instantiationService.createInstance(GetTerminalOutputTool); + this._register(_toolsService.registerTool(GetTerminalOutputToolData, getTerminalOutputTool)); + this._register(_toolsService.executeToolSet.addTool(GetTerminalOutputToolData)); - const awaitTerminalTool = instantiationService.createInstance(AwaitTerminalTool); - this._register(toolsService.registerTool(AwaitTerminalToolData, awaitTerminalTool)); - this._register(toolsService.executeToolSet.addTool(AwaitTerminalToolData)); + const awaitTerminalTool = _instantiationService.createInstance(AwaitTerminalTool); + this._register(_toolsService.registerTool(AwaitTerminalToolData, awaitTerminalTool)); + this._register(_toolsService.executeToolSet.addTool(AwaitTerminalToolData)); - const killTerminalTool = instantiationService.createInstance(KillTerminalTool); - this._register(toolsService.registerTool(KillTerminalToolData, killTerminalTool)); - this._register(toolsService.executeToolSet.addTool(KillTerminalToolData)); + const killTerminalTool = _instantiationService.createInstance(KillTerminalTool); + this._register(_toolsService.registerTool(KillTerminalToolData, killTerminalTool)); + this._register(_toolsService.executeToolSet.addTool(KillTerminalToolData)); - instantiationService.invokeFunction(createRunInTerminalToolData).then(runInTerminalToolData => { - const runInTerminalTool = instantiationService.createInstance(RunInTerminalTool); - this._register(toolsService.registerTool(runInTerminalToolData, runInTerminalTool)); - this._register(toolsService.executeToolSet.addTool(runInTerminalToolData)); - }); + this._registerRunInTerminalTool(); - const getTerminalSelectionTool = instantiationService.createInstance(GetTerminalSelectionTool); - this._register(toolsService.registerTool(GetTerminalSelectionToolData, getTerminalSelectionTool)); + const getTerminalSelectionTool = _instantiationService.createInstance(GetTerminalSelectionTool); + this._register(_toolsService.registerTool(GetTerminalSelectionToolData, getTerminalSelectionTool)); - const getTerminalLastCommandTool = instantiationService.createInstance(GetTerminalLastCommandTool); - this._register(toolsService.registerTool(GetTerminalLastCommandToolData, getTerminalLastCommandTool)); + const getTerminalLastCommandTool = _instantiationService.createInstance(GetTerminalLastCommandTool); + this._register(_toolsService.registerTool(GetTerminalLastCommandToolData, getTerminalLastCommandTool)); - this._register(toolsService.readToolSet.addTool(GetTerminalSelectionToolData)); - this._register(toolsService.readToolSet.addTool(GetTerminalLastCommandToolData)); + this._register(_toolsService.readToolSet.addTool(GetTerminalSelectionToolData)); + this._register(_toolsService.readToolSet.addTool(GetTerminalLastCommandToolData)); // #endregion // #region Tasks - const runTaskTool = instantiationService.createInstance(RunTaskTool); - this._register(toolsService.registerTool(RunTaskToolData, runTaskTool)); + const runTaskTool = _instantiationService.createInstance(RunTaskTool); + this._register(_toolsService.registerTool(RunTaskToolData, runTaskTool)); - const getTaskOutputTool = instantiationService.createInstance(GetTaskOutputTool); - this._register(toolsService.registerTool(GetTaskOutputToolData, getTaskOutputTool)); + const getTaskOutputTool = _instantiationService.createInstance(GetTaskOutputTool); + this._register(_toolsService.registerTool(GetTaskOutputToolData, getTaskOutputTool)); - const createAndRunTaskTool = instantiationService.createInstance(CreateAndRunTaskTool); - this._register(toolsService.registerTool(CreateAndRunTaskToolData, createAndRunTaskTool)); - this._register(toolsService.executeToolSet.addTool(RunTaskToolData)); - this._register(toolsService.executeToolSet.addTool(CreateAndRunTaskToolData)); - this._register(toolsService.readToolSet.addTool(GetTaskOutputToolData)); + const createAndRunTaskTool = _instantiationService.createInstance(CreateAndRunTaskTool); + this._register(_toolsService.registerTool(CreateAndRunTaskToolData, createAndRunTaskTool)); + this._register(_toolsService.executeToolSet.addTool(RunTaskToolData)); + this._register(_toolsService.executeToolSet.addTool(CreateAndRunTaskToolData)); + this._register(_toolsService.readToolSet.addTool(GetTaskOutputToolData)); // #endregion + + // Re-register run_in_terminal tool when sandbox-related settings change, + // so the tool description and input schema stay in sync with the current + // sandbox state. + this._register(this._configurationService.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled) || + e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains) || + e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains) || + e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains) + ) { + this._registerRunInTerminalTool(); + } + })); + this._register(this._trustedDomainService.onDidChangeTrustedDomains(() => { + this._registerRunInTerminalTool(); + })); + } + + private _runInTerminalTool: RunInTerminalTool | undefined; + + private _registerRunInTerminalTool(): void { + const version = ++this._runInTerminalToolRegistrationVersion; + this._instantiationService.invokeFunction(createRunInTerminalToolData).then(runInTerminalToolData => { + if (this._store.isDisposed || version !== this._runInTerminalToolRegistrationVersion) { + return; + } + if (!this._runInTerminalTool) { + this._runInTerminalTool = this._register(this._instantiationService.createInstance(RunInTerminalTool)); + } + // Dispose old registration first so registerToolData doesn't throw + // "already registered" for the same tool ID. + this._runInTerminalToolRegistration.value = undefined; + const store = new DisposableStore(); + store.add(this._toolsService.registerToolData(runInTerminalToolData)); + store.add(this._toolsService.registerToolImplementation(runInTerminalToolData.id, this._runInTerminalTool)); + store.add(this._toolsService.executeToolSet.addTool(runInTerminalToolData)); + this._runInTerminalToolRegistration.value = store; + }); } } registerWorkbenchContribution2(ChatAgentToolsContribution.ID, ChatAgentToolsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts index b18edd0b7b3..317ff148988 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts @@ -45,6 +45,7 @@ export interface ICommandLineAnalyzerOptions { treeSitterLanguage: TreeSitterCommandParserLanguage; terminalToolSessionId: string; chatSessionResource: URI | undefined; + requiresUnsandboxConfirmation?: boolean; } export interface ICommandLineAnalyzerResult { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts index dfd1d5d8d3c..cff1b32e22e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts @@ -51,7 +51,8 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma } async analyze(options: ICommandLineAnalyzerOptions): Promise { - if (options.chatSessionResource && this._terminalChatService.hasChatSessionAutoApproval(options.chatSessionResource)) { + const isAutoApproveEnabledInSettings = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnableAutoApprove) === true; + if (isAutoApproveEnabledInSettings && options.chatSessionResource && this._terminalChatService.hasChatSessionAutoApproval(options.chatSessionResource)) { this._log('Session has auto approval enabled, auto approving command'); const disableUri = createCommandUri(TerminalChatCommandId.DisableSessionAutoApproval, options.chatSessionResource); const mdTrustSettings = { @@ -82,7 +83,17 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma let autoApproveInfo: IMarkdownString | undefined; let customActions: ToolConfirmationAction[] | undefined; - if (!subCommands) { + if (!subCommands?.length) { + if (trimmedCommandLine.length === 0) { + this._log('Command line is empty, auto approving'); + return { + isAutoApproved: true, + isAutoApproveAllowed: true, + disclaimers: [], + }; + } + + this._log('No sub-commands were parsed, auto approval is not allowed'); return { isAutoApproveAllowed: false, disclaimers: [], diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineSandboxAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineSandboxAnalyzer.ts index 39ed6639c52..7d75a0d38cb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineSandboxAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineSandboxAnalyzer.ts @@ -22,7 +22,7 @@ export class CommandLineSandboxAnalyzer extends Disposable implements ICommandLi } return { isAutoApproveAllowed: true, - forceAutoApproval: true, + forceAutoApproval: _options.requiresUnsandboxConfirmation ? false : true, }; } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/sandboxedCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/sandboxedCommandLinePresenter.ts index d2b094e6aec..06c7776bf46 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/sandboxedCommandLinePresenter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/sandboxedCommandLinePresenter.ts @@ -8,8 +8,9 @@ import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLineP /** * Command line presenter for sandboxed commands. - * Extracts the original command from the sandbox wrapper for cleaner display, - * while the actual sandboxed command runs unchanged. + * Returns the display form of the command (provided via {@link ICommandLineRewriterResult.forDisplay} + * from the rewriter pipeline) for cleaner presentation, while the actual sandboxed command runs + * unchanged. */ export class SandboxedCommandLinePresenter implements ICommandLinePresenter { constructor( @@ -22,7 +23,7 @@ export class SandboxedCommandLinePresenter implements ICommandLinePresenter { return undefined; } return { - commandLine: options.commandLine.original ?? options.commandLine.forDisplay, + commandLine: options.commandLine.forDisplay, processOtherPresenters: true }; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts index 3a3c4a3c955..2c1dbc6acc0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineRewriter.ts @@ -17,6 +17,7 @@ export interface ICommandLineRewriterOptions { cwd: URI | undefined; shell: string; os: OperatingSystem; + requestUnsandboxedExecution?: boolean; } export interface ICommandLineRewriterResult { @@ -24,4 +25,5 @@ export interface ICommandLineRewriterResult { reasoning: string; //for scenarios where we want to show a different command in the chat UI than what is actually run in the terminal forDisplay?: string; + isSandboxWrapped?: boolean; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts index 030afe5a76a..db6e9610526 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineSandboxRewriter.ts @@ -15,6 +15,10 @@ export class CommandLineSandboxRewriter extends Disposable implements ICommandLi } async rewrite(options: ICommandLineRewriterOptions): Promise { + if (options.requestUnsandboxedExecution) { + return undefined; + } + if (!(await this._sandboxService.isEnabled())) { return undefined; } @@ -30,7 +34,8 @@ export class CommandLineSandboxRewriter extends Disposable implements ICommandLi return { rewritten: wrappedCommand, reasoning: 'Wrapped command for sandbox execution', - forDisplay: options.commandLine, // show the command that is passed as input. In this case, the output from CommandLinePreventHistoryRewriter + forDisplay: options.commandLine, // show the command that is passed as input (after prior rewrites like cd prefix stripping) + isSandboxWrapped: true, }; } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/getTerminalOutputTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/getTerminalOutputTool.ts index b7ac92861e7..1dbfc71c462 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/getTerminalOutputTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/getTerminalOutputTool.ts @@ -16,7 +16,7 @@ export const GetTerminalOutputToolData: IToolData = { toolReferenceName: 'getTerminalOutput', legacyToolReferenceFullNames: ['runCommands/getTerminalOutput'], displayName: localize('getTerminalOutputTool.displayName', 'Get Terminal Output'), - modelDescription: `Get the output of a terminal command previously started with ${TerminalToolId.RunInTerminal}`, + modelDescription: `Get the output of a background terminal command previously started with ${TerminalToolId.RunInTerminal}. The ID must be the exact opaque value returned by ${TerminalToolId.RunInTerminal} when isBackground=true; terminal names, labels, and integers are not valid IDs.`, icon: Codicon.terminal, source: ToolDataSource.Internal, inputSchema: { @@ -24,7 +24,8 @@ export const GetTerminalOutputToolData: IToolData = { properties: { id: { type: 'string', - description: 'The ID of the terminal to check.' + description: `The ID of the background terminal to check (returned by ${TerminalToolId.RunInTerminal} when isBackground=true). This must be the exact opaque ID returned by that tool; terminal names, labels, or integers are invalid.`, + pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$' }, }, required: [ @@ -47,10 +48,20 @@ export class GetTerminalOutputTool extends Disposable implements IToolImpl { async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { const args = invocation.parameters as IGetTerminalOutputInputParams; + const execution = RunInTerminalTool.getExecution(args.id); + if (!execution) { + return { + content: [{ + kind: 'text', + value: `Error: No active terminal execution found with ID ${args.id}. The ID must be the exact value returned by ${TerminalToolId.RunInTerminal} for a background command.` + }] + }; + } + return { content: [{ kind: 'text', - value: `Output of terminal ${args.id}:\n${RunInTerminalTool.getBackgroundOutput(args.id)}` + value: `Output of terminal ${args.id}:\n${execution.getOutput()}` }] }; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 43ef48eca9d..e3b8bc31ed3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -230,18 +230,18 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { } // Check for VS Code's task finish messages (like "press any key to close the terminal"). - // These should only be ignored if it's a task AND the task is finished. - // Otherwise, "press any key to continue" from scripts should prompt the user. + // If the execution is a task and the output contains a VS Code task finish message, + // always treat it as a stop signal regardless of task active state (which can be stale). const isTask = this._execution.task !== undefined; - const isTaskInactive = this._execution.isActive ? !(await this._execution.isActive()) : true; - if (isTask && isTaskInactive && detectsVSCodeTaskFinishMessage(output)) { - this._logService.trace('OutputMonitor: Idle -> VS Code task finish message detected for inactive task, stopping'); + if (isTask && detectsVSCodeTaskFinishMessage(output)) { + this._logService.trace('OutputMonitor: Idle -> VS Code task finish message detected, stopping'); // Task is finished, ignore the "press any key to close" message return { shouldContinuePollling: false, output }; } // Check for generic "press any key" prompts from scripts. - if ((!isTask || !isTaskInactive) && detectsGenericPressAnyKeyPattern(output)) { + // Only shown for non-task executions since task finish messages are handled above. + if (!isTask && detectsGenericPressAnyKeyPattern(output)) { this._logService.trace('OutputMonitor: Idle -> generic "press any key" detected'); const autoReply = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoReplyToPrompts) || this._isAutopilotMode(); if (autoReply) { @@ -474,6 +474,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { const model = await this._getLanguageModel(); if (!model) { return 'No models available'; + } const response = await this._languageModelsService.sendChatRequest( @@ -927,8 +928,26 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { } private async _getLanguageModel(): Promise { - const models = await this._safeSelectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); - return models.length ? models[0] : undefined; + const fastModels = await this._safeSelectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); + if (fastModels.length) { + return fastModels[0]; + } + + const widget = this._chatWidgetService.lastFocusedWidget ?? this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)[0]; + const currentModel = widget?.input.currentLanguageModel; + if (currentModel) { + const currentFamilyModels = await this._safeSelectLanguageModels({ vendor: 'copilot', family: currentModel.replaceAll('copilot/', '') }); + if (currentFamilyModels.length) { + return currentFamilyModels[0]; + } + } + + const copilotModels = await this._safeSelectLanguageModels({ vendor: 'copilot' }); + if (copilotModels.length) { + return copilotModels[0]; + } + + return undefined; } private async _safeSelectLanguageModels(selector: ILanguageModelChatSelector): Promise { @@ -1050,8 +1069,14 @@ const taskFinishMessages = [ // "Press any key to close the terminal." (with exit code placeholder removed for matching) localize('exitCode.closeTerminal', "Press any key to close the terminal."), localize('exitCode.reuseTerminal', "Press any key to close the terminal."), + // Punctuation variant: "The terminal will be reused by tasks. Press any key to close." + localize('reuseTerminal.pressClose', "The terminal will be reused by tasks. Press any key to close."), ]; +const normalizedTaskFinishMessages = taskFinishMessages.map(msg => + msg.replace(/[\s.,:;!?"'`()[\]{}<>\-_/\\]+/g, '').toLowerCase() +); + /** * Detects VS Code's specific task completion messages like: * - "Press any key to close the terminal." @@ -1061,9 +1086,9 @@ const taskFinishMessages = [ * that can split words across lines (e.g., "t\no" instead of "to"). */ export function detectsVSCodeTaskFinishMessage(cursorLine: string): boolean { - // Remove all whitespace to handle line wrapping that splits words mid-word - const normalized = cursorLine.replace(/\s/g, '').toLowerCase(); - return taskFinishMessages.some(msg => normalized.includes(msg.replace(/\s/g, '').toLowerCase())); + // Be tolerant to whitespace, punctuation, and line wrapping that can split words mid-word. + const compact = cursorLine.replace(/[\s.,:;!?"'`()[\]{}<>\-_/\\]+/g, '').toLowerCase(); + return normalizedTaskFinishMessages.some(msg => compact.includes(msg)); } /** diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/outputAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/outputAnalyzer.ts index 0490f5e332f..dcb1580e466 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/outputAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/outputAnalyzer.ts @@ -7,6 +7,7 @@ export interface IOutputAnalyzerOptions { readonly exitCode: number | undefined; readonly exitResult: string; readonly commandLine: string; + readonly isSandboxWrapped: boolean; } export interface IOutputAnalyzer { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalConfirmationTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalConfirmationTool.ts index 65a0a565068..1093ebfcabd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalConfirmationTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalConfirmationTool.ts @@ -58,6 +58,10 @@ export const ConfirmTerminalCommandToolData: IToolData = { }; export class ConfirmTerminalCommandTool extends RunInTerminalTool { + override get _enableCommandLineSandboxRewriting() { + return false; + } + override async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise { const preparedInvocation = await super.prepareToolInvocation(context, token); if (preparedInvocation) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 2947937e8e5..d6650025591 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -9,15 +9,17 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../../ba import { Codicon } from '../../../../../../base/common/codicons.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Event } from '../../../../../../base/common/event.js'; -import { MarkdownString, type IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { escapeMarkdownSyntaxTokens, MarkdownString, type IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; +import { getMediaMime } from '../../../../../../base/common/mime.js'; import { basename, posix, win32 } from '../../../../../../base/common/path.js'; import { OperatingSystem, OS } from '../../../../../../base/common/platform.js'; import { count } from '../../../../../../base/common/strings.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IInstantiationService, type ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; @@ -26,7 +28,7 @@ import { ITerminalLogService, ITerminalProfile } from '../../../../../../platfor import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService/chatService.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IStreamedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js'; import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js'; import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; @@ -36,7 +38,7 @@ import type { ITerminalExecuteStrategy, ITerminalExecuteStrategyResult } from '. import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh, normalizeTerminalCommandForDisplay } from '../runInTerminalHelpers.js'; import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js'; import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js'; import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js'; @@ -70,15 +72,16 @@ import { clamp } from '../../../../../../base/common/numbers.js'; import { IOutputAnalyzer } from './outputAnalyzer.js'; import { SandboxOutputAnalyzer } from './sandboxOutputAnalyzer.js'; import { IAgentSessionsService } from '../../../../chat/browser/agentSessions/agentSessionsService.js'; +import { ITerminalSandboxService, type ITerminalSandboxResolvedNetworkDomains } from '../../common/terminalSandboxService.js'; // #region Tool data const TOOL_REFERENCE_NAME = 'runInTerminal'; const LEGACY_TOOL_REFERENCE_FULL_NAMES = ['runCommands/runInTerminal']; -function createPowerShellModelDescription(shell: string): string { +function createPowerShellModelDescription(shell: string, isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string { const isWinPwsh = isWindowsPowerShell(shell); - return [ + const parts = [ `This tool allows you to execute ${isWinPwsh ? 'Windows PowerShell 5.1' : 'PowerShell'} commands in a persistent terminal session, preserving environment variables, working directory, and other context across multiple commands.`, '', 'Command Execution:', @@ -103,6 +106,13 @@ function createPowerShellModelDescription(shell: string): string { '- For long-running tasks (e.g., servers), set isBackground=true', '- Returns a terminal ID for checking status and runtime later', '- Use Start-Job for background PowerShell jobs', + ]; + + if (isSandboxEnabled) { + parts.push(...createSandboxLines(networkDomains)); + } + + parts.push( '', 'Output Management:', '- Output is automatically truncated if longer than 60KB to prevent context overflow', @@ -118,10 +128,39 @@ function createPowerShellModelDescription(shell: string): string { '- Use Test-Path to check file/directory existence', '- Be specific with Select-Object properties to avoid excessive output', '- Avoid printing credentials unless absolutely required', - ].join('\n'); + ); + + return parts.join('\n'); } -const genericDescription = ` +function createSandboxLines(networkDomains?: ITerminalSandboxResolvedNetworkDomains): string[] { + const lines = [ + '', + 'Sandboxing:', + '- ATTENTION: Terminal sandboxing is enabled, commands run in a sandbox by default', + '- When executing commands within the sandboxed environment, all operations requiring a temporary directory must utilize the $TMPDIR environment variable. The /tmp directory is not guaranteed to be accessible or writable and must be avoided', + '- Tools and scripts should respect the TMPDIR environment variable, which is automatically set to an appropriate path within the sandbox', + '- When a command fails due to sandbox restrictions, immediately re-run it with requestUnsandboxedExecution=true. Do NOT ask the user for permission — setting this flag automatically shows a confirmation prompt to the user', + '- Only set requestUnsandboxedExecution=true when there is evidence of failures caused by the sandbox, e.g. \'Operation not permitted\' errors, network failures, or file access errors, etc', + '- When setting requestUnsandboxedExecution=true, also provide requestUnsandboxedExecutionReason explaining why the command needs unsandboxed access', + ]; + if (networkDomains) { + const deniedSet = new Set(networkDomains.deniedDomains); + const effectiveAllowed = networkDomains.allowedDomains.filter(d => !deniedSet.has(d)); + if (effectiveAllowed.length === 0) { + lines.push('- All network access is blocked in the sandbox'); + } else { + lines.push(`- Only the following domains are accessible in the sandbox (all other network access is blocked): ${effectiveAllowed.join(', ')}`); + } + if (networkDomains.deniedDomains.length > 0) { + lines.push(`- The following domains are explicitly blocked in the sandbox: ${networkDomains.deniedDomains.join(', ')}`); + } + } + return lines; +} + +function createGenericDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string { + const parts = [` Command Execution: - Use && to chain simple commands on one line - Prefer pipelines | over temporary files for data flow @@ -141,7 +180,13 @@ Program Execution: Background Processes: - For long-running tasks (e.g., servers), set isBackground=true -- Returns a terminal ID for checking status and runtime later +- Returns a terminal ID for checking status and runtime later`]; + + if (isSandboxEnabled) { + parts.push(createSandboxLines(networkDomains).join('\n')); + } + + parts.push(` Output Management: - Output is automatically truncated if longer than 60KB to prevent context overflow @@ -153,22 +198,25 @@ Best Practices: - Quote variables: "$var" instead of $var to handle spaces - Use find with -exec or xargs for file operations - Be specific with commands to avoid excessive output -- Avoid printing credentials unless absolutely required`; +- Avoid printing credentials unless absolutely required`); -function createBashModelDescription(): string { + return parts.join(''); +} + +function createBashModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string { return [ 'This tool allows you to execute shell commands in a persistent bash terminal session, preserving environment variables, working directory, and other context across multiple commands.', - genericDescription, + createGenericDescription(isSandboxEnabled, networkDomains), '- Use [[ ]] for conditional tests instead of [ ]', '- Prefer $() over backticks for command substitution', '- Use set -e at start of complex commands to exit on errors' ].join('\n'); } -function createZshModelDescription(): string { +function createZshModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string { return [ 'This tool allows you to execute shell commands in a persistent zsh terminal session, preserving environment variables, working directory, and other context across multiple commands.', - genericDescription, + createGenericDescription(isSandboxEnabled, networkDomains), '- Use type to check command type (builtin, function, alias)', '- Use jobs, fg, bg for job control', '- Use [[ ]] for conditional tests instead of [ ]', @@ -178,10 +226,10 @@ function createZshModelDescription(): string { ].join('\n'); } -function createFishModelDescription(): string { +function createFishModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string { return [ 'This tool allows you to execute shell commands in a persistent fish terminal session, preserving environment variables, working directory, and other context across multiple commands.', - genericDescription, + createGenericDescription(isSandboxEnabled, networkDomains), '- Use type to check command type (builtin, function, alias)', '- Use jobs, fg, bg for job control', '- Use test expressions for conditionals (no [[ ]] syntax)', @@ -196,20 +244,26 @@ export async function createRunInTerminalToolData( accessor: ServicesAccessor ): Promise { const instantiationService = accessor.get(IInstantiationService); + const terminalSandboxService = accessor.get(ITerminalSandboxService); const profileFetcher = instantiationService.createInstance(TerminalProfileFetcher); - const shell = await profileFetcher.getCopilotShell(); - const os = await profileFetcher.osBackend; + const [shell, os, isSandboxEnabled] = await Promise.all([ + profileFetcher.getCopilotShell(), + profileFetcher.osBackend, + terminalSandboxService.isEnabled(), + ]); + + const networkDomains = isSandboxEnabled ? terminalSandboxService.getResolvedNetworkDomains() : undefined; let modelDescription: string; if (shell && os && isPowerShell(shell, os)) { - modelDescription = createPowerShellModelDescription(shell); + modelDescription = createPowerShellModelDescription(shell, isSandboxEnabled, networkDomains); } else if (shell && os && isZsh(shell, os)) { - modelDescription = createZshModelDescription(); + modelDescription = createZshModelDescription(isSandboxEnabled, networkDomains); } else if (shell && os && isFish(shell, os)) { - modelDescription = createFishModelDescription(); + modelDescription = createFishModelDescription(isSandboxEnabled, networkDomains); } else { - modelDescription = createBashModelDescription(); + modelDescription = createBashModelDescription(isSandboxEnabled, networkDomains); } return { @@ -242,8 +296,18 @@ export async function createRunInTerminalToolData( }, timeout: { type: 'number', - description: 'An optional timeout in milliseconds. When provided, the tool will stop tracking the command after this duration and return the output collected so far. Be conservative with the timeout duration, give enough time that the command would complete on a low-end machine. Use 0 for no timeout. If it\'s not clear how long the command will take then use 0 to avoid prematurely terminating it, never guess too low.', + description: 'An optional timeout in milliseconds. When provided, the tool will stop tracking the command after this duration and return the output collected so far with a timeout indicator. Be conservative with the timeout duration, give enough time that the command would complete on a low-end machine. Use 0 for no timeout. If it\'s not clear how long the command will take then use 0 to avoid prematurely terminating it, never guess too low.', }, + ...isSandboxEnabled ? { + requestUnsandboxedExecution: { + type: 'boolean', + description: 'Request that this command run outside the terminal sandbox. Only set this when the command clearly needs unsandboxed access. The user will be prompted before the command runs unsandboxed.' + }, + requestUnsandboxedExecutionReason: { + type: 'string', + description: 'A short explanation of why this command must run outside the terminal sandbox. Only provide this when requestUnsandboxedExecution is true.' + }, + } : {}, }, required: [ 'command', @@ -277,6 +341,8 @@ export interface IRunInTerminalInputParams { goal: string; isBackground: boolean; timeout?: number; + requestUnsandboxedExecution?: boolean; + requestUnsandboxedExecutionReason?: string; } /** @@ -362,10 +428,20 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { RunInTerminalTool._activeExecutions.delete(id); return true; } - + /** + * Controls whether this tool wires up sandbox-specific command-line + * behavior, including both the {@link CommandLineSandboxRewriter} and the + * {@link CommandLineSandboxAnalyzer}. This is separate from + * ITerminalSandboxService.isEnabled(), which reports whether terminal + * sandboxing is currently enabled for the running window. + */ + protected get _enableCommandLineSandboxRewriting() { + return true; + } constructor( @IChatService protected readonly _chatService: IChatService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService, @IHistoryService private readonly _historyService: IHistoryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, @@ -375,6 +451,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, @ITerminalLogService private readonly _logService: ITerminalLogService, @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalSandboxService private readonly _terminalSandboxService: ITerminalSandboxService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @@ -392,14 +469,20 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._commandLineRewriters = [ this._register(this._instantiationService.createInstance(CommandLineCdPrefixRewriter)), this._register(this._instantiationService.createInstance(CommandLinePwshChainOperatorRewriter, this._treeSitterCommandParser)), - this._register(this._instantiationService.createInstance(CommandLinePreventHistoryRewriter)), - this._register(this._instantiationService.createInstance(CommandLineSandboxRewriter)), ]; + if (this._enableCommandLineSandboxRewriting) { + this._commandLineRewriters.push(this._register(this._instantiationService.createInstance(CommandLineSandboxRewriter))); + } + // PreventHistoryRewriter must be last so the leading space is applied to the final + // command, including any sandbox wrapping. + this._commandLineRewriters.push(this._register(this._instantiationService.createInstance(CommandLinePreventHistoryRewriter))); this._commandLineAnalyzers = [ this._register(this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineFileWriteAnalyzer: ${message}`, args))), this._register(this._instantiationService.createInstance(CommandLineAutoApproveAnalyzer, this._treeSitterCommandParser, this._telemetry, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineAutoApproveAnalyzer: ${message}`, args))), - this._register(this._instantiationService.createInstance(CommandLineSandboxAnalyzer)), ]; + if (this._enableCommandLineSandboxRewriting) { + this._commandLineAnalyzers.push(this._register(this._instantiationService.createInstance(CommandLineSandboxAnalyzer))); + } this._commandLinePresenters = [ this._instantiationService.createInstance(SandboxedCommandLinePresenter), new NodeCommandLinePresenter(), @@ -434,6 +517,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } + async handleToolStream(context: IToolInvocationStreamContext, _token: CancellationToken): Promise { + const partialInput = context.rawInput as Partial | undefined; + if (partialInput && typeof partialInput === 'object' && partialInput.command) { + const normalizedCommand = normalizeTerminalCommandForDisplay(partialInput.command).replace(/\r\n|\r|\n/g, ' '); + const truncatedCommand = normalizedCommand.length > 80 + ? normalizedCommand.substring(0, 77) + '...' + : normalizedCommand; + const invocationMessage = partialInput.isBackground + ? new MarkdownString(localize('runInTerminal.streaming.background', "Running `{0}` in background", truncatedCommand)) + : new MarkdownString(localize('runInTerminal.streaming', "Running `{0}`", truncatedCommand)); + return { invocationMessage }; + } + return { invocationMessage: localize('runInTerminal.streaming.default', "Running command") }; + } + async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise { const args = context.parameters as IRunInTerminalInputParams; @@ -445,7 +543,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { instance = toolTerminal.instance; } } - const [os, shell, cwd] = await Promise.all([ + const [os, shell, cwd, isTerminalSandboxEnabled] = await Promise.all([ this._osBackend, this._profileFetcher.getCopilotShell(), (async () => { @@ -456,9 +554,11 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { cwd = workspaceFolder?.uri; } return cwd; - })() + })(), + this._terminalSandboxService.isEnabled() ]); const language = os === OperatingSystem.Windows ? 'pwsh' : 'sh'; + const requiresUnsandboxConfirmation = isTerminalSandboxEnabled && args.requestUnsandboxedExecution === true; const terminalToolSessionId = generateUuid(); // Generate a custom command ID to link the command between renderer and pty host @@ -466,16 +566,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let rewrittenCommand: string | undefined = args.command; let forDisplayCommand: string | undefined = undefined; + let isSandboxWrapped = false; for (const rewriter of this._commandLineRewriters) { const rewriteResult = await rewriter.rewrite({ commandLine: rewrittenCommand, cwd, shell, - os + os, + requestUnsandboxedExecution: requiresUnsandboxConfirmation, }); if (rewriteResult) { rewrittenCommand = rewriteResult.rewritten; - forDisplayCommand = rewriteResult.forDisplay; + forDisplayCommand = rewriteResult.forDisplay ?? forDisplayCommand; + if (rewriteResult.isSandboxWrapped) { + isSandboxWrapped = true; + } this._logService.info(`RunInTerminalTool: Command rewritten by ${rewriter.constructor.name}: ${rewriteResult.reasoning}`); } } @@ -487,11 +592,14 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { commandLine: { original: args.command, toolEdited: rewrittenCommand === args.command ? undefined : rewrittenCommand, - forDisplay: forDisplayCommand, + forDisplay: forDisplayCommand ?? normalizeTerminalCommandForDisplay(rewrittenCommand ?? args.command), + isSandboxWrapped, }, cwd, language, isBackground: args.isBackground, + requestUnsandboxedExecution: requiresUnsandboxConfirmation, + requestUnsandboxedExecutionReason: args.requestUnsandboxedExecutionReason, }; // HACK: Exit early if there's an alternative recommendation, this is a little hacky but @@ -537,8 +645,16 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { treeSitterLanguage: isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash, terminalToolSessionId, chatSessionResource, + requiresUnsandboxConfirmation, }; - const commandLineAnalyzerResults = await Promise.all(this._commandLineAnalyzers.map(e => e.analyze(commandLineAnalyzerOptions))); + + // In Autopilot/Bypass Approvals modes, do not interact with terminal auto-approve rules. + // Commands should flow through directly based on the chat permission level. + const isSessionAutoApproved = chatSessionResource && this._isSessionAutoApproveLevel(chatSessionResource); + const commandLineAnalyzers = isSessionAutoApproved + ? this._commandLineAnalyzers.filter(e => !(e instanceof CommandLineAutoApproveAnalyzer)) + : this._commandLineAnalyzers; + const commandLineAnalyzerResults = await Promise.all(commandLineAnalyzers.map(e => e.analyze(commandLineAnalyzerOptions))); const disclaimersRaw = commandLineAnalyzerResults.map(e => e.disclaimers).filter(e => !!e).flatMap(e => e); let disclaimer: IMarkdownString | undefined; @@ -587,7 +703,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } // Extract cd prefix for display - show directory in title, command suffix in editor - const commandToDisplay = (toolSpecificData.commandLine.userEdited ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original).trimStart(); + const commandToDisplay = (toolSpecificData.commandLine.forDisplay ?? toolSpecificData.commandLine.userEdited ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original).trimStart(); const extractedCd = extractCdPrefix(commandToDisplay, shell, os); let confirmationTitle: string; if (extractedCd && cwd) { @@ -646,20 +762,47 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } - // Check if the session's permission level (Autopilot/Bypass Approvals) auto-approves all tools. - // When active, skip terminal confirmation entirely since the user has opted into full auto-approval. - const isSessionAutoApproved = chatSessionResource && this._isSessionAutoApproveLevel(chatSessionResource); + if (requiresUnsandboxConfirmation) { + confirmationTitle = args.isBackground + ? localize('runInTerminal.unsandboxed.background', "Run `{0}` command outside the sandbox in background?", shellType) + : localize('runInTerminal.unsandboxed', "Run `{0}` command outside the sandbox?", shellType); + } // If forceConfirmationReason is set, always show confirmation regardless of auto-approval const shouldShowConfirmation = (!isFinalAutoApproved && !isSessionAutoApproved) || context.forceConfirmationReason !== undefined; + const confirmationMessage = requiresUnsandboxConfirmation + ? new MarkdownString(localize( + 'runInTerminal.unsandboxed.confirmationMessage', + "Explanation: {0}\n\nGoal: {1}\n\nReason for leaving the sandbox: {2}", + args.explanation, + args.goal, + args.requestUnsandboxedExecutionReason || localize('runInTerminal.unsandboxed.confirmationMessage.defaultReason', "The model indicated that this command needs unsandboxed access.") + )) + : new MarkdownString(localize('runInTerminal.confirmationMessage', "Explanation: {0}\n\nGoal: {1}", args.explanation, args.goal)); const confirmationMessages = shouldShowConfirmation ? { title: confirmationTitle, - message: new MarkdownString(localize('runInTerminal.confirmationMessage', "Explanation: {0}\n\nGoal: {1}", args.explanation, args.goal)), + message: confirmationMessage, disclaimer, + allowAutoConfirm: undefined, terminalCustomActions: customActions, } : undefined; + const rawDisplayCommand = toolSpecificData.commandLine.forDisplay ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original; + const displayCommand = rawDisplayCommand.length > 80 + ? rawDisplayCommand.substring(0, 77) + '...' + : rawDisplayCommand; + const escapedDisplayCommand = escapeMarkdownSyntaxTokens(displayCommand); + const invocationMessage = toolSpecificData.commandLine.isSandboxWrapped + ? args.isBackground + ? new MarkdownString(localize('runInTerminal.invocation.sandbox.background', "Running `{0}` in sandbox in background", escapedDisplayCommand)) + : new MarkdownString(localize('runInTerminal.invocation.sandbox', "Running `{0}` in sandbox", escapedDisplayCommand)) + : args.isBackground + ? new MarkdownString(localize('runInTerminal.invocation.background', "Running `{0}` in background", escapedDisplayCommand)) + : new MarkdownString(localize('runInTerminal.invocation', "Running `{0}`", escapedDisplayCommand)); + return { + invocationMessage, + icon: toolSpecificData.commandLine.isSandboxWrapped ? Codicon.terminalSecure : Codicon.terminal, confirmationMessages, toolSpecificData, }; @@ -721,9 +864,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const didToolEditCommand = ( !didUserEditCommand && toolSpecificData.commandLine.toolEdited !== undefined && - toolSpecificData.commandLine.toolEdited !== toolSpecificData.commandLine.original + toolSpecificData.commandLine.toolEdited !== toolSpecificData.commandLine.original && + // Only consider it a meaningful edit if the display form also differs from the + // original. Cosmetic rewrites like prepending a space to prevent shell history + // should not trigger the "tool simplified the command" note. + normalizeTerminalCommandForDisplay(toolSpecificData.commandLine.toolEdited).trim() !== normalizeTerminalCommandForDisplay(toolSpecificData.commandLine.original).trim() ); + const didSandboxWrapCommand = toolSpecificData.commandLine.isSandboxWrapped === true; + if (token.isCancellationRequested) { throw new CancellationError(); } @@ -769,6 +918,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let didTimeout = false; let didMoveToBackground = args.isBackground; let timeoutPromise: CancelablePromise | undefined; + let timeoutRacePromise: Promise<{ type: 'timeout' }> | undefined; let outputMonitor: OutputMonitor | undefined; let pollingResult: IPollingResult & { pollDurationMs: number } | undefined; const executeCancellation = store.add(new CancellationTokenSource(token)); @@ -779,12 +929,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const shouldEnforceTimeout = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnforceTimeoutFromModel) === true; if (shouldEnforceTimeout) { timeoutPromise = timeout(timeoutValue); - timeoutPromise.then(() => { - if (!executeCancellation.token.isCancellationRequested) { - didTimeout = true; - executeCancellation.cancel(); - } - }); + timeoutRacePromise = timeoutPromise.then( + () => ({ type: 'timeout' as const }) + ).catch(() => ({ type: 'timeout' as const })); } } @@ -823,7 +970,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { if (toolTerminal.shellIntegrationQuality === ShellIntegrationQuality.None) { toolResultMessage = '$(info) Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) to improve command detection'; } - this._logService.debug(`RunInTerminalTool: Using \`${execution.strategy.type}\` execute strategy for command \`${command}\``); + this._logService.info(`RunInTerminalTool: Using \`${execution.strategy.type}\` execute strategy for command \`${command}\``); store.add(execution); RunInTerminalTool._activeExecutions.set(termId, execution); @@ -867,22 +1014,38 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { state.timestamp = state.timestamp ?? timingStart; toolSpecificData.terminalCommandState = state; + // if the command is wrapped in a sandbox, we will not show the command. This is because the sandbox may add additional commands that are not relevant to the user, and the output will provide more context about what is running. let resultText = ( - didUserEditCommand - ? `Note: The user manually edited the command to \`${command}\`, and that command is now running in terminal with ID=${termId}` - : didToolEditCommand - ? `Note: The tool simplified the command to \`${command}\`, and that command is now running in terminal with ID=${termId}` - : `Command is running in terminal with ID=${termId}` + didSandboxWrapCommand ? `Command is now running in terminal with ID=${termId}` + : didUserEditCommand + ? `Note: The user manually edited the command to \`${command}\`, and that command is now running in terminal with ID=${termId}` + : didToolEditCommand + ? `Note: The tool simplified the command to \`${command}\`, and that command is now running in terminal with ID=${termId}` + : `Command is running in terminal with ID=${termId}` ); + const backgroundOutput = pollingResult?.modelOutputEvalResponse ?? pollingResult?.output; + const outputAnalyzerMessage = backgroundOutput + ? await this._getOutputAnalyzerMessage(undefined, backgroundOutput, command, didSandboxWrapCommand) + : undefined; if (pollingResult && pollingResult.modelOutputEvalResponse) { - resultText += `\n\ The command became idle with output:\n${pollingResult.modelOutputEvalResponse}`; + resultText += `\n\ The command became idle with output:\n`; + if (outputAnalyzerMessage) { + resultText += `${outputAnalyzerMessage}\n`; + } + resultText += pollingResult.modelOutputEvalResponse; } else if (pollingResult) { - resultText += `\n\ The command is still running, with output:\n${pollingResult.output}`; + resultText += `\n\ The command is still running, with output:\n`; + if (outputAnalyzerMessage) { + resultText += `${outputAnalyzerMessage}\n`; + } + resultText += pollingResult.output; } - + const endCwd = await toolTerminal.instance.getCwdResource(); return { toolMetadata: { - exitCode: undefined // Background processes don't have immediate exit codes + exitCode: undefined, // Background processes don't have immediate exit codes + id: termId, + cwd: endCwd?.toString(), }, content: [{ kind: 'text', @@ -891,10 +1054,14 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }; } else { // Foreground mode: race execution completion against continue in background - const raceResult = await Promise.race([ + const raceCandidates: Promise<{ type: 'completed'; result: ITerminalExecuteStrategyResult } | { type: 'background' } | { type: 'timeout' }>[] = [ executionPromise.then(result => ({ type: 'completed' as const, result })), continueInBackgroundPromise.then(() => ({ type: 'background' as const })) - ]); + ]; + if (timeoutRacePromise) { + raceCandidates.push(timeoutRacePromise); + } + const raceResult = await Promise.race(raceCandidates); if (raceResult.type === 'background') { // Moved to background - execution continues running, just return current output @@ -903,6 +1070,18 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const backgroundOutput = execution.getOutput(); outputLineCount = backgroundOutput ? count(backgroundOutput.trim(), '\n') + 1 : 0; terminalResult = backgroundOutput; + } else if (raceResult.type === 'timeout') { + // Timeout reached - return partial output and keep terminal alive as background. + this._logService.debug(`RunInTerminalTool: Timeout reached, returning output collected so far`); + error = 'timeout'; + didTimeout = true; + didMoveToBackground = true; + toolTerminal.isBackground = true; + this._sessionTerminalAssociations.delete(chatSessionResource); + await this._associateProcessIdWithSession(toolTerminal.instance, chatSessionResource, termId, toolTerminal.shellIntegrationQuality, true); + const timeoutOutput = execution.getOutput(); + outputLineCount = timeoutOutput ? count(timeoutOutput.trim(), '\n') + 1 : 0; + terminalResult = timeoutOutput ?? ''; } else { const executeResult = raceResult.result; // Reset user input state after command execution completes @@ -918,10 +1097,13 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { toolResultMessage = altBufferMessage; outputLineCount = 0; error = executeResult.error ?? 'alternateBuffer'; + const altBufferCwd = await toolTerminal.instance.getCwdResource(); altBufferResult = { toolResultMessage, toolMetadata: { - exitCode: undefined + exitCode: undefined, + id: termId, + cwd: altBufferCwd?.toString(), }, content: [{ kind: 'text', @@ -942,7 +1124,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { toolSpecificData.terminalCommandState = state; } - this._logService.debug(`RunInTerminalTool: Finished \`${execution.strategy.type}\` execute strategy with exitCode \`${executeResult.exitCode}\`, result.length \`${executeResult.output?.length}\`, error \`${executeResult.error}\``); + this._logService.info(`RunInTerminalTool: Finished \`${execution.strategy.type}\` execute strategy with exitCode \`${executeResult.exitCode}\`, result.length \`${executeResult.output?.length}\`, error \`${executeResult.error}\``); outputLineCount = executeResult.output === undefined ? 0 : count(executeResult.output.trim(), '\n') + 1; exitCode = executeResult.exitCode; error = executeResult.error; @@ -963,6 +1145,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { if (didTimeout && e instanceof CancellationError) { this._logService.debug(`RunInTerminalTool: Timeout reached, returning output collected so far`); error = 'timeout'; + didMoveToBackground = true; + toolTerminal.isBackground = true; + this._sessionTerminalAssociations.delete(chatSessionResource); const timeoutOutput = getOutput(toolTerminal.instance, undefined); outputLineCount = timeoutOutput ? count(timeoutOutput.trim(), '\n') + 1 : 0; terminalResult = timeoutOutput ?? ''; @@ -1009,6 +1194,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { didUserEditCommand, didToolEditCommand, isBackground: args.isBackground, + isSandboxWrapped: toolSpecificData.commandLine.isSandboxWrapped === true, shellIntegrationQuality: toolTerminal.shellIntegrationQuality, error, isNewSession, @@ -1036,45 +1222,126 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } const resultText: string[] = []; - if (didUserEditCommand) { - resultText.push(`Note: The user manually edited the command to \`${command}\`, and this is the output of running that command instead:\n`); - } else if (didToolEditCommand) { - resultText.push(`Note: The tool simplified the command to \`${command}\`, and this is the output of running that command instead:\n`); - } - if (didMoveToBackground && !args.isBackground) { - resultText.push(`Note: This terminal execution was moved to the background using the ID ${termId}\n`); - } - let outputAnalyzerMessage: string | undefined; - for (const analyzer of this._outputAnalyzers) { - const message = await analyzer.analyze({ exitCode, exitResult: terminalResult, commandLine: command }); - if (message) { - outputAnalyzerMessage = message; - break; + if (!didSandboxWrapCommand) { + if (didUserEditCommand) { + resultText.push(`Note: The user manually edited the command to \`${command}\`, and this is the output of running that command instead:\n`); + } else if (didToolEditCommand) { + resultText.push(`Note: The tool simplified the command to \`${command}\`, and this is the output of running that command instead:\n`); + } + if (didMoveToBackground && !args.isBackground) { + resultText.push(`Note: This terminal execution was moved to the background using the ID ${termId}\n`); } } + if (didTimeout && timeoutValue !== undefined && timeoutValue > 0) { + resultText.push(`Note: Command timed out after ${timeoutValue}ms. Output collected so far is shown below and the command may still be running in terminal ID ${termId}.\n\n`); + } + const outputAnalyzerMessage = await this._getOutputAnalyzerMessage(exitCode, terminalResult, command, didSandboxWrapCommand); if (outputAnalyzerMessage) { resultText.push(`${outputAnalyzerMessage}\n`); } resultText.push(terminalResult); const isError = exitCode !== undefined && exitCode !== 0; + const endCwd = await toolTerminal.instance.getCwdResource(); + + const imageContent = await this._extractImagesFromOutput(terminalResult, endCwd); + return { toolResultMessage, toolMetadata: { - exitCode: exitCode + exitCode: exitCode, + id: termId, + cwd: endCwd?.toString(), + timedOut: didTimeout || undefined, + timeoutMs: didTimeout ? timeoutValue : undefined, }, toolResultDetails: isError ? { input: command, output: [{ type: 'embed', isText: true, value: terminalResult }], isError: true } : undefined, - content: [{ - kind: 'text', - value: resultText.join(''), - }] + content: [ + { + kind: 'text', + value: resultText.join(''), + }, + ...imageContent, + ] }; } + private async _getOutputAnalyzerMessage(exitCode: number | undefined, exitResult: string, commandLine: string, isSandboxWrapped: boolean): Promise { + for (const analyzer of this._outputAnalyzers) { + const message = await analyzer.analyze({ exitCode, exitResult, commandLine, isSandboxWrapped }); + if (message) { + return message; + } + } + + return undefined; + } + + private static readonly _maxImageFileSize = 5 * 1024 * 1024; + + /** + * Scans terminal output for file paths that point to images and reads them. + * Returns data content parts for any found images that exist on disk. + */ + private async _extractImagesFromOutput(output: string, cwd: URI | undefined): Promise { + const normalizedOutput = output.replace(/\r?\n/g, ''); + + // Match paths ending with image extensions. A leading / or \ is sufficient + // to identify a path segment; the full path up to the extension is captured. + const pathPattern = /(?:[^\s]*[\/\\][^\s]*\.(?:png|jpe?g|gif|webp|bmp))/gi; + + const matches = new Set(); + for (const match of normalizedOutput.matchAll(pathPattern)) { + matches.add(match[0]); + } + + if (matches.size === 0) { + return []; + } + + const results: IToolResult['content'] = []; + for (const filePath of matches) { + try { + const mimeType = getMediaMime(filePath); + if (!mimeType || !mimeType.startsWith('image/')) { + continue; + } + + // Resolve the URI - check for absolute path (Unix / or Windows drive letter) + let fileUri: URI; + if (/^\/|^[A-Za-z]:[\\\/]/.test(filePath)) { + fileUri = URI.file(filePath); + } else if (cwd) { + fileUri = URI.joinPath(cwd, filePath); + } else { + continue; + } + + const stat = await this._fileService.stat(fileUri).catch(() => undefined); + if (!stat || stat.isDirectory || stat.size > RunInTerminalTool._maxImageFileSize) { + continue; + } + + const fileContent = await this._fileService.readFile(fileUri); + results.push({ + kind: 'data', + value: { + mimeType, + data: fileContent.value, + }, + }); + } catch { + // Ignore files that can't be read + } + } + + return results; + } + private _handleTerminalVisibility(toolTerminal: IToolTerminal, chatSessionResource: URI) { const chatSessionOpenInWidget = !!this._chatWidgetService.getWidgetBySessionResource(chatSessionResource); if (this._configurationService.getValue(TerminalChatAgentToolsSettingId.OutputLocation) === 'terminal' && chatSessionOpenInWidget) { @@ -1449,6 +1716,15 @@ export class TerminalProfileFetcher { }; } + // Force bash over sh as sh doesn't have shell integration + if (defaultProfile.path === '/bin/sh') { + return { + ...defaultProfile, + path: '/bin/bash', + profileName: 'bash', + }; + } + // Setting icon: undefined allows the system to use the default AI terminal icon (not overridden or removed) return { ...defaultProfile, icon: undefined }; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sandboxOutputAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sandboxOutputAnalyzer.ts index adb2d2dd24e..684fcf2051e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sandboxOutputAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/sandboxOutputAnalyzer.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../../nls.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'; import type { IOutputAnalyzer, IOutputAnalyzerOptions } from './outputAnalyzer.js'; import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; @@ -17,19 +17,50 @@ export class SandboxOutputAnalyzer extends Disposable implements IOutputAnalyzer } async analyze(options: IOutputAnalyzerOptions): Promise { - if (options.exitCode === undefined || options.exitCode === 0) { - return undefined; - } - if (!(await this._sandboxService.isEnabled())) { + if (!options.isSandboxWrapped) { return undefined; } - return localize( - 'runInTerminalTool.sandboxCommandFailed', - "Command failed while running in sandboxed mode. Use the command result to determine the scenario. If the issue is filesystem permissions, update allowWrite in {0} (Linux) or {1} (macOS). If the issue is domain/network related, add the required domains to {2}.allowedDomains.", - TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem, - TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem, - TerminalChatAgentToolsSettingId.TerminalSandboxNetwork - ); + const knownFailure = options.exitCode !== undefined && options.exitCode !== 0; + const suspectedFailure = !knownFailure && options.exitCode === undefined && this._outputLooksSandboxBlocked(options.exitResult); + + if (!knownFailure && !suspectedFailure) { + return undefined; + } + + const os = await this._sandboxService.getOS(); + const fileSystemSetting = os === OperatingSystem.Linux + ? TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem + : TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem; + + const prefix = knownFailure + ? 'Command failed while running in sandboxed mode. If the command failed due to sandboxing:' + : 'Command ran in sandboxed mode and may have been blocked by the sandbox. If the command failed due to sandboxing:'; + return `${prefix} +- If it would be reasonable to extend the sandbox rules, work with the user to update allowWrite for file system access problems in ${fileSystemSetting}, or to add required domains to ${TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains}. +- Otherwise, immediately retry the command with requestUnsandboxedExecution=true. Do NOT ask the user — setting this flag automatically shows a confirmation prompt to the user. + +Here is the output of the command:\n`; + } + + /** + * Checks whether the command output contains strings that typically indicate + * the sandbox blocked the operation. Used when exit code is unavailable. + * + * The output may contain newlines inserted by terminal wrapping, so we + * strip them before testing. + */ + private _outputLooksSandboxBlocked(output: string): boolean { + return outputLooksSandboxBlocked(output); } } + +/** + * Checks whether the command output contains strings that typically indicate + * the sandbox blocked the operation. The output may contain newlines inserted + * by terminal wrapping, so we strip them before testing. + */ +export function outputLooksSandboxBlocked(output: string): boolean { + const normalized = output.replace(/\n/g, ' '); + return /Operation not permitted|Permission denied|Read-only file system|sandbox-exec|bwrap|sandbox_violation/i.test(normalized); +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts index 5913bb7d17e..d595ccbd527 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts @@ -97,30 +97,50 @@ export class CreateAndRunTaskTool implements IToolImpl { return { content: [{ kind: 'text', value: `Task not found: ${args.task.label}` }], toolResultMessage: new MarkdownString(localize('copilotChat.taskNotFound', 'Task not found: `{0}`', args.task.label)) }; } - _progress.report({ message: new MarkdownString(localize('copilotChat.runningTask', 'Running task `{0}`', args.task.label)) }); - const raceResult = await Promise.race([this._tasksService.run(task, undefined, TaskRunSource.ChatAgent), timeout(3000)]); - const result: ITaskSummary | undefined = raceResult && typeof raceResult === 'object' ? raceResult as ITaskSummary : undefined; + const preRunMarkersStore = new DisposableStore(); + let result: ITaskSummary | undefined; + let terminalResults: Awaited> = []; + try { + const dependencyTasks = await resolveDependencyTasks(task, args.workspaceFolder, this._configurationService, this._tasksService); + const startMarkersByTerminalInstanceId = new Map>(); + for (const terminal of this._terminalService.instances) { + const marker = terminal.registerMarker(); + startMarkersByTerminalInstanceId.set(terminal.instanceId, marker); + if (marker) { + preRunMarkersStore.add(marker); + } + } - const dependencyTasks = await resolveDependencyTasks(task, args.workspaceFolder, this._configurationService, this._tasksService); - const resources = this._tasksService.getTerminalsForTasks(dependencyTasks ?? task); - const terminals = resources?.map(resource => this._terminalService.instances.find(t => t.resource.path === resource?.path && t.resource.scheme === resource.scheme)).filter(Boolean) as ITerminalInstance[]; - if (!terminals || terminals.length === 0) { - return { content: [{ kind: 'text', value: `Task started but no terminal was found for: ${args.task.label}` }], toolResultMessage: new MarkdownString(localize('copilotChat.noTerminal', 'Task started but no terminal was found for: `{0}`', args.task.label)) }; + _progress.report({ message: new MarkdownString(localize('copilotChat.runningTask', 'Running task `{0}`', args.task.label)) }); + const raceResult = await Promise.race([this._tasksService.run(task, undefined, TaskRunSource.ChatAgent), timeout(3000)]); + result = raceResult && typeof raceResult === 'object' ? raceResult as ITaskSummary : undefined; + + const resources = this._tasksService.getTerminalsForTasks(dependencyTasks ?? task); + const terminals = resources?.map(resource => this._terminalService.instances.find(t => t.resource.path === resource?.path && t.resource.scheme === resource.scheme)).filter(Boolean) as ITerminalInstance[]; + if (!terminals || terminals.length === 0) { + return { content: [{ kind: 'text', value: `Task started but no terminal was found for: ${args.task.label}` }], toolResultMessage: new MarkdownString(localize('copilotChat.noTerminal', 'Task started but no terminal was found for: `{0}`', args.task.label)) }; + } + const store = new DisposableStore(); + try { + terminalResults = await collectTerminalResults( + terminals, + task, + this._instantiationService, + invocation.context!, + _progress, + token, + store, + (terminalTask) => this._isTaskActive(terminalTask), + dependencyTasks, + this._tasksService, + startMarkersByTerminalInstanceId + ); + } finally { + store.dispose(); + } + } finally { + preRunMarkersStore.dispose(); } - const store = new DisposableStore(); - const terminalResults = await collectTerminalResults( - terminals, - task, - this._instantiationService, - invocation.context!, - _progress, - token, - store, - (terminalTask) => this._isTaskActive(terminalTask), - dependencyTasks, - this._tasksService - ); - store.dispose(); for (const r of terminalResults) { this._telemetryService.publicLog2?.('copilotChat.runTaskTool.createAndRunTask', { taskId: args.task.label, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/getTaskOutputTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/getTaskOutputTool.ts index 757aabd617b..c365803393b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/getTaskOutputTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/getTaskOutputTool.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { IMarker as IXtermMarker } from '@xterm/xterm'; import type { CancellationToken } from '../../../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../../../base/common/lifecycle.js'; @@ -95,43 +96,56 @@ export class GetTaskOutputTool extends Disposable implements IToolImpl { if (!terminals || terminals.length === 0) { return { content: [{ kind: 'text', value: `Terminal not found for task ${taskLabel}` }], toolResultMessage: new MarkdownString(localize('copilotChat.terminalNotFound', 'Terminal not found for task \`{0}\`', taskLabel)) }; } - const store = new DisposableStore(); - const terminalResults = await collectTerminalResults( - terminals, - task, - this._instantiationService, - invocation.context!, - _progress, - token, - store, - (terminalTask) => this._isTaskActive(terminalTask), - dependencyTasks, - this._tasksService - ); - store.dispose(); - for (const r of terminalResults) { - this._telemetryService.publicLog2?.('copilotChat.getTaskOutputTool.get', { - taskId: args.id, - bufferLength: r.output.length ?? 0, - pollDurationMs: r.pollDurationMs ?? 0, - inputToolManualAcceptCount: r.inputToolManualAcceptCount ?? 0, - inputToolManualRejectCount: r.inputToolManualRejectCount ?? 0, - inputToolManualChars: r.inputToolManualChars ?? 0, - inputToolManualShownCount: r.inputToolManualShownCount ?? 0, - inputToolFreeFormInputCount: r.inputToolFreeFormInputCount ?? 0, - inputToolFreeFormInputShownCount: r.inputToolFreeFormInputShownCount ?? 0 - }); + const startMarkersByTerminalInstanceId = task.configurationProperties.isBackground + ? new Map() + : undefined; + if (startMarkersByTerminalInstanceId) { + // Background/watch tasks should read their current buffer when queried after start. + for (const terminal of terminals) { + startMarkersByTerminalInstanceId.set(terminal.instanceId, undefined); + } } - const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`); - const uniqueDetails = Array.from(new Set(details)).join('\n\n'); - const toolResultDetails = toolResultDetailsFromResponse(terminalResults); - const toolResultMessage = toolResultMessageFromResponse(undefined, taskLabel, toolResultDetails, terminalResults, true, task.configurationProperties.isBackground); + const store = new DisposableStore(); + try { + const terminalResults = await collectTerminalResults( + terminals, + task, + this._instantiationService, + invocation.context!, + _progress, + token, + store, + (terminalTask) => this._isTaskActive(terminalTask), + dependencyTasks, + this._tasksService, + startMarkersByTerminalInstanceId + ); + for (const r of terminalResults) { + this._telemetryService.publicLog2?.('copilotChat.getTaskOutputTool.get', { + taskId: args.id, + bufferLength: r.output.length ?? 0, + pollDurationMs: r.pollDurationMs ?? 0, + inputToolManualAcceptCount: r.inputToolManualAcceptCount ?? 0, + inputToolManualRejectCount: r.inputToolManualRejectCount ?? 0, + inputToolManualChars: r.inputToolManualChars ?? 0, + inputToolManualShownCount: r.inputToolManualShownCount ?? 0, + inputToolFreeFormInputCount: r.inputToolFreeFormInputCount ?? 0, + inputToolFreeFormInputShownCount: r.inputToolFreeFormInputShownCount ?? 0 + }); + } + const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`); + const uniqueDetails = Array.from(new Set(details)).join('\n\n'); + const toolResultDetails = toolResultDetailsFromResponse(terminalResults); + const toolResultMessage = toolResultMessageFromResponse(undefined, taskLabel, toolResultDetails, terminalResults, true, task.configurationProperties.isBackground); - return { - content: [{ kind: 'text', value: uniqueDetails }], - toolResultMessage, - toolResultDetails - }; + return { + content: [{ kind: 'text', value: uniqueDetails }], + toolResultMessage, + toolResultDetails + }; + } finally { + store.dispose(); + } } private async _isTaskActive(task: Task): Promise { const busyTasks = await this._tasksService.getBusyTasks(); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/runTaskTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/runTaskTool.ts index f469f6eb502..67a6d43be45 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/runTaskTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/runTaskTool.ts @@ -10,7 +10,7 @@ import { ITelemetryService } from '../../../../../../../platform/telemetry/commo import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../../../../../chat/common/tools/languageModelToolsService.js'; import { ITaskService, ITaskSummary, Task, TasksAvailableContext } from '../../../../../tasks/common/taskService.js'; import { TaskRunSource } from '../../../../../tasks/common/tasks.js'; -import { ITerminalService } from '../../../../../terminal/browser/terminal.js'; +import { ITerminalInstance, ITerminalService } from '../../../../../terminal/browser/terminal.js'; import { collectTerminalResults, getTaskDefinition, getTaskForTool, resolveDependencyTasks, tasksMatch } from '../../taskHelpers.js'; import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; @@ -54,57 +54,75 @@ export class RunTaskTool implements IToolImpl { return { content: [{ kind: 'text', value: `The task ${taskLabel} is already running.` }], toolResultMessage: new MarkdownString(localize('chat.taskAlreadyRunning', 'The task \`{0}\` is already running.', taskLabel)) }; } - const raceResult = await Promise.race([this._tasksService.run(task, undefined, TaskRunSource.ChatAgent), timeout(3000)]); - const result: ITaskSummary | undefined = raceResult && typeof raceResult === 'object' ? raceResult as ITaskSummary : undefined; - const dependencyTasks = await resolveDependencyTasks(task, args.workspaceFolder, this._configurationService, this._tasksService); - const resources = this._tasksService.getTerminalsForTasks(dependencyTasks ?? task); - if (!resources || resources.length === 0) { - return { content: [{ kind: 'text', value: `Task started but no terminal was found for: ${taskLabel}` }], toolResultMessage: new MarkdownString(localize('chat.noTerminal', 'Task started but no terminal was found for: \`{0}\`', taskLabel)) }; + const startMarkersByTerminalInstanceId = new Map>(); + const startMarkersDisposableStore = new DisposableStore(); + for (const terminal of this._terminalService.instances) { + const marker = terminal.registerMarker(); + startMarkersByTerminalInstanceId.set(terminal.instanceId, marker); + if (marker) { + startMarkersDisposableStore.add(marker); + } } - const terminals = this._terminalService.instances.filter(t => resources.some(r => r.path === t.resource.path && r.scheme === t.resource.scheme)); - if (terminals.length === 0) { - return { content: [{ kind: 'text', value: `Task started but no terminal was found for: ${taskLabel}` }], toolResultMessage: new MarkdownString(localize('chat.noTerminal', 'Task started but no terminal was found for: \`{0}\`', taskLabel)) }; + try { + const raceResult = await Promise.race([this._tasksService.run(task, undefined, TaskRunSource.ChatAgent), timeout(3000)]); + const result: ITaskSummary | undefined = raceResult && typeof raceResult === 'object' ? raceResult as ITaskSummary : undefined; + + const resources = this._tasksService.getTerminalsForTasks(dependencyTasks ?? task); + if (!resources || resources.length === 0) { + return { content: [{ kind: 'text', value: `Task started but no terminal was found for: ${taskLabel}` }], toolResultMessage: new MarkdownString(localize('chat.noTerminal', 'Task started but no terminal was found for: \`{0}\`', taskLabel)) }; + } + const terminals = this._terminalService.instances.filter(t => resources.some(r => r.path === t.resource.path && r.scheme === t.resource.scheme)); + if (terminals.length === 0) { + return { content: [{ kind: 'text', value: `Task started but no terminal was found for: ${taskLabel}` }], toolResultMessage: new MarkdownString(localize('chat.noTerminal', 'Task started but no terminal was found for: \`{0}\`', taskLabel)) }; + } + + const store = new DisposableStore(); + let terminalResults: Awaited> = []; + try { + terminalResults = await collectTerminalResults( + terminals, + task, + this._instantiationService, + invocation.context!, + _progress, + token, + store, + (terminalTask) => this._isTaskActive(terminalTask), + dependencyTasks, + this._tasksService, + startMarkersByTerminalInstanceId + ); + } finally { + store.dispose(); + } + for (const r of terminalResults) { + this._telemetryService.publicLog2?.('copilotChat.runTaskTool.run', { + taskId: args.id, + bufferLength: r.output.length ?? 0, + pollDurationMs: r.pollDurationMs ?? 0, + inputToolManualAcceptCount: r.inputToolManualAcceptCount ?? 0, + inputToolManualRejectCount: r.inputToolManualRejectCount ?? 0, + inputToolManualChars: r.inputToolManualChars ?? 0, + inputToolManualShownCount: r.inputToolManualShownCount ?? 0, + inputToolFreeFormInputShownCount: r.inputToolFreeFormInputShownCount ?? 0, + inputToolFreeFormInputCount: r.inputToolFreeFormInputCount ?? 0 + }); + } + + const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`); + const uniqueDetails = Array.from(new Set(details)).join('\n\n'); + const toolResultDetails = toolResultDetailsFromResponse(terminalResults); + const toolResultMessage = toolResultMessageFromResponse(result, taskLabel, toolResultDetails, terminalResults, undefined, task.configurationProperties.isBackground); + + return { + content: [{ kind: 'text', value: uniqueDetails }], + toolResultMessage, + toolResultDetails + }; + } finally { + startMarkersDisposableStore.dispose(); } - - const store = new DisposableStore(); - const terminalResults = await collectTerminalResults( - terminals, - task, - this._instantiationService, - invocation.context!, - _progress, - token, - store, - (terminalTask) => this._isTaskActive(terminalTask), - dependencyTasks, - this._tasksService - ); - store.dispose(); - for (const r of terminalResults) { - this._telemetryService.publicLog2?.('copilotChat.runTaskTool.run', { - taskId: args.id, - bufferLength: r.output.length ?? 0, - pollDurationMs: r.pollDurationMs ?? 0, - inputToolManualAcceptCount: r.inputToolManualAcceptCount ?? 0, - inputToolManualRejectCount: r.inputToolManualRejectCount ?? 0, - inputToolManualChars: r.inputToolManualChars ?? 0, - inputToolManualShownCount: r.inputToolManualShownCount ?? 0, - inputToolFreeFormInputShownCount: r.inputToolFreeFormInputShownCount ?? 0, - inputToolFreeFormInputCount: r.inputToolFreeFormInputCount ?? 0 - }); - } - - const details = terminalResults.map(r => `Terminal: ${r.name}\nOutput:\n${r.output}`); - const uniqueDetails = Array.from(new Set(details)).join('\n\n'); - const toolResultDetails = toolResultDetailsFromResponse(terminalResults); - const toolResultMessage = toolResultMessageFromResponse(result, taskLabel, toolResultDetails, terminalResults, undefined, task.configurationProperties.isBackground); - - return { - content: [{ kind: 'text', value: uniqueDetails }], - toolResultMessage, - toolResultDetails - }; } private async _isTaskActive(task: Task): Promise { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index f50dfea219f..221f2e29d20 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -21,16 +21,20 @@ export const enum TerminalChatAgentToolsSettingId { AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts', OutputLocation = 'chat.tools.terminal.outputLocation', TerminalSandboxEnabled = 'chat.tools.terminal.sandbox.enabled', - TerminalSandboxNetwork = 'chat.tools.terminal.sandbox.network', + TerminalSandboxNetworkAllowedDomains = 'chat.tools.terminal.sandbox.network.allowedDomains', + TerminalSandboxNetworkDeniedDomains = 'chat.tools.terminal.sandbox.network.deniedDomains', + TerminalSandboxNetworkAllowTrustedDomains = 'chat.tools.terminal.sandbox.network.allowTrustedDomains', TerminalSandboxLinuxFileSystem = 'chat.tools.terminal.sandbox.linuxFileSystem', TerminalSandboxMacFileSystem = 'chat.tools.terminal.sandbox.macFileSystem', PreventShellHistory = 'chat.tools.terminal.preventShellHistory', EnforceTimeoutFromModel = 'chat.tools.terminal.enforceTimeoutFromModel', + IdlePollInterval = 'chat.tools.terminal.idlePollInterval', TerminalProfileLinux = 'chat.tools.terminal.terminalProfile.linux', TerminalProfileMacOs = 'chat.tools.terminal.terminalProfile.osx', TerminalProfileWindows = 'chat.tools.terminal.terminalProfile.windows', + DeprecatedTerminalSandboxNetwork = 'chat.tools.terminal.sandbox.network', DeprecatedAutoApproveCompatible = 'chat.agent.terminal.autoApprove', DeprecatedAutoApprove1 = 'chat.agent.terminal.allowList', DeprecatedAutoApprove2 = 'chat.agent.terminal.denyList', @@ -435,13 +439,20 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary('terminalSandboxService'); +export interface ITerminalSandboxResolvedNetworkDomains { + allowedDomains: string[]; + deniedDomains: string[]; +} + export interface ITerminalSandboxService { readonly _serviceBrand: undefined; isEnabled(): Promise; + getOS(): Promise; wrapCommand(command: string): string; getSandboxConfigPath(forceRefresh?: boolean): Promise; getTempDir(): URI | undefined; setNeedsForceUpdateConfigFile(): void; + getResolvedNetworkDomains(): ITerminalSandboxResolvedNetworkDomains; } export class TerminalSandboxService extends Disposable implements ITerminalSandboxService { @@ -48,6 +58,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb private _appRoot: string; private _os: OperatingSystem = OS; private _defaultWritePaths: string[] = ['~/.npm']; + private static readonly _sandboxTempDirName = 'tmp'; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -56,6 +67,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb @ILogService private readonly _logService: ILogService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IProductService private readonly _productService: IProductService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, ) { super(); this._appRoot = dirname(FileAccess.asFileUri('').path); @@ -69,7 +83,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb // If terminal sandbox settings changed, update sandbox config. if ( e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled) || - e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains) || e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) || e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ) { @@ -80,17 +96,37 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb this._register(this._trustedDomainService.onDidChangeTrustedDomains(() => { this.setNeedsForceUpdateConfigFile(); })); + + this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(() => { + this.setNeedsForceUpdateConfigFile(); + })); + + this._register(this._lifecycleService.onWillShutdown(e => { + if (!this._tempDir) { + return; + } + e.join(this._cleanupSandboxTempDir(), { + id: 'join.deleteFilesInSandboxTempDir', + label: localize('deleteFilesInSandboxTempDir', "Delete Files in Sandbox Temp Dir"), + order: WillShutdownJoinerOrder.Default + }); + })); } public async isEnabled(): Promise { - this._remoteEnvDetails = await this._remoteEnvDetailsPromise; - this._os = this._remoteEnvDetails ? this._remoteEnvDetails.os : OS; - if (this._os === OperatingSystem.Windows) { + const os = await this.getOS(); + if (os === OperatingSystem.Windows) { return false; } return this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled); } + public async getOS(): Promise { + this._remoteEnvDetails = await this._remoteEnvDetailsPromise; + this._os = this._remoteEnvDetails ? this._remoteEnvDetails.os : OS; + return this._os; + } + public wrapCommand(command: string): string { if (!this._sandboxConfigPath || !this._tempDir) { throw new Error('Sandbox config path or temp dir not initialized'); @@ -107,7 +143,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb // Use ELECTRON_RUN_AS_NODE=1 to make Electron executable behave as Node.js // TMPDIR must be set as environment variable before the command // Quote shell arguments so the wrapped command cannot break out of the outer shell. - const wrappedCommand = `PATH="$PATH:${dirname(this._rgPath)}" TMPDIR="${this._tempDir.path}" "${this._execPath}" "${this._srtPath}" --settings "${this._sandboxConfigPath}" -c ${this._quoteShellArgument(command)}`; + const wrappedCommand = `PATH="$PATH:${dirname(this._rgPath)}" TMPDIR="${this._tempDir.path}" CLAUDE_TMPDIR="${this._tempDir.path}" "${this._execPath}" "${this._srtPath}" --settings "${this._sandboxConfigPath}" -c ${this._quoteShellArgument(command)}`; if (this._remoteEnvDetails) { return `${wrappedCommand}`; } @@ -142,9 +178,8 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb this._srtPathResolved = true; const remoteEnv = this._remoteEnvDetails || await this._remoteEnvDetailsPromise; if (remoteEnv) { - this._appRoot = remoteEnv.appRoot.path; - this._execPath = this._pathJoin(this._appRoot, 'node'); + this._execPath = remoteEnv.execPath; } this._srtPath = this._pathJoin(this._appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js'); this._rgPath = this._pathJoin(this._appRoot, 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg'); @@ -156,7 +191,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb await this._initTempDir(); } if (this._tempDir) { - const networkSetting = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ?? {}; + const allowedDomainsSetting = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains) ?? []; + const deniedDomainsSetting = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains) ?? []; + const allowTrustedDomains = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains) ?? false; const linuxFileSystemSetting = this._os === OperatingSystem.Linux ? this._configurationService.getValue<{ denyRead?: string[]; allowWrite?: string[]; denyWrite?: string[] }>(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) ?? {} : {}; @@ -164,19 +201,18 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb ? this._configurationService.getValue<{ denyRead?: string[]; allowWrite?: string[]; denyWrite?: string[] }>(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ?? {} : {}; const configFileUri = URI.joinPath(this._tempDir, `vscode-sandbox-settings-${this._sandboxSettingsId}.json`); - const defaultAllowWrite = [...this._defaultWritePaths]; - const linuxAllowWrite = [...new Set([...(linuxFileSystemSetting.allowWrite ?? []), ...defaultAllowWrite])]; - const macAllowWrite = [...new Set([...(macFileSystemSetting.allowWrite ?? []), ...defaultAllowWrite])]; + const linuxAllowWrite = this._updateAllowWritePathsWithWorkspaceFolders(linuxFileSystemSetting.allowWrite); + const macAllowWrite = this._updateAllowWritePathsWithWorkspaceFolders(macFileSystemSetting.allowWrite); - let allowedDomains = networkSetting.allowedDomains ?? []; - if (networkSetting.allowTrustedDomains) { + let allowedDomains = allowedDomainsSetting; + if (allowTrustedDomains) { allowedDomains = this._addTrustedDomainsToAllowedDomains(allowedDomains); } const sandboxSettings = { network: { allowedDomains, - deniedDomains: networkSetting.deniedDomains ?? [] + deniedDomains: deniedDomainsSetting }, filesystem: { denyRead: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyRead : linuxFileSystemSetting.denyRead, @@ -201,13 +237,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb if (await this.isEnabled()) { this._needsForceUpdateConfigFile = true; const remoteEnv = this._remoteEnvDetails || await this._remoteEnvDetailsPromise; - if (remoteEnv) { - this._tempDir = remoteEnv.tmpDir; - } else { - const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI }; - this._tempDir = environmentService.tmpDir; - } + this._tempDir = this._getSandboxTempDirPath(remoteEnv); if (this._tempDir) { + await this._fileService.createFolder(this._tempDir); this._defaultWritePaths.push(this._tempDir.path); } if (!this._tempDir) { @@ -216,6 +248,52 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb } } + private async _cleanupSandboxTempDir(): Promise { + if (!this._tempDir) { + return; + } + try { + await this._fileService.del(this._tempDir, { recursive: true, useTrash: false }); + } catch (error) { + this._logService.warn('TerminalSandboxService: Failed to delete sandbox temp dir', error); + } + } + + private _getSandboxTempDirPath(remoteEnv: IRemoteAgentEnvironment | null): URI | undefined { + const sandboxTempDirName = this._getSandboxWindowTempDirName(); + if (remoteEnv?.userHome) { + const sandboxRoot = URI.joinPath(remoteEnv.userHome, this._productService.serverDataFolderName ?? this._productService.dataFolderName, TerminalSandboxService._sandboxTempDirName); + return sandboxTempDirName ? URI.joinPath(sandboxRoot, sandboxTempDirName) : sandboxRoot; + } + + const nativeEnv = this._environmentService as IEnvironmentService & { userHome?: URI }; + if (nativeEnv.userHome) { + const sandboxRoot = URI.joinPath(nativeEnv.userHome, this._productService.dataFolderName, TerminalSandboxService._sandboxTempDirName); + return sandboxTempDirName ? URI.joinPath(sandboxRoot, sandboxTempDirName) : sandboxRoot; + } + + return undefined; + } + + private _getSandboxWindowTempDirName(): string | undefined { + const workbenchEnv = this._environmentService as IEnvironmentService & { window?: { id?: number } }; + const windowId = workbenchEnv.window?.id; + return typeof windowId === 'number' ? `tmp_vscode_${windowId}` : undefined; + } + + public getResolvedNetworkDomains(): ITerminalSandboxResolvedNetworkDomains { + let allowedDomains = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains) ?? []; + const deniedDomains = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains) ?? []; + const allowTrustedDomains = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains) ?? false; + if (allowTrustedDomains) { + allowedDomains = this._addTrustedDomainsToAllowedDomains(allowedDomains); + } + return { + allowedDomains, + deniedDomains + }; + } + private _addTrustedDomainsToAllowedDomains(allowedDomains: string[]): string[] { const allowedDomainsSet = new Set(allowedDomains); for (const domain of this._trustedDomainService.trustedDomains) { @@ -230,4 +308,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb } return Array.from(allowedDomainsSet); } + + private _updateAllowWritePathsWithWorkspaceFolders(configuredAllowWrite: string[] | undefined): string[] { + const workspaceFolderPaths = this._workspaceContextService.getWorkspace().folders.map(folder => folder.uri.path); + return [...new Set([...workspaceFolderPaths, ...this._defaultWritePaths, ...(configuredAllowWrite ?? [])])]; + } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/commandLineAutoApproveAnalyzer.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/commandLineAutoApproveAnalyzer.test.ts new file mode 100644 index 00000000000..1959af50879 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/commandLineAutoApproveAnalyzer.test.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import type { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import type { ICommandLineAnalyzerOptions } from '../../browser/tools/commandLineAnalyzer/commandLineAnalyzer.js'; +import { CommandLineAutoApproveAnalyzer } from '../../browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.js'; +import { RunInTerminalToolTelemetry } from '../../browser/runInTerminalToolTelemetry.js'; +import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../../browser/treeSitterCommandParser.js'; + +suite('CommandLineAutoApproveAnalyzer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: IInstantiationService; + let analyzer: CommandLineAutoApproveAnalyzer; + + setup(() => { + const configurationService = new TestConfigurationService(); + instantiationService = workbenchInstantiationService({ + configurationService: () => configurationService + }, store); + + const parser = { + extractSubCommands: async () => [], + } as unknown as TreeSitterCommandParser; + const telemetry = { + logPrepare: () => { }, + } as unknown as RunInTerminalToolTelemetry; + + analyzer = store.add(instantiationService.createInstance( + CommandLineAutoApproveAnalyzer, + parser, + telemetry, + () => { } + )); + }); + + test('should not allow auto approve when sub-command parsing returns an empty list', async () => { + const options: ICommandLineAnalyzerOptions = { + commandLine: 'rm -- file.txt', + cwd: undefined, + shell: 'pwsh', + os: OperatingSystem.Windows, + treeSitterLanguage: TreeSitterCommandParserLanguage.PowerShell, + terminalToolSessionId: 'test', + chatSessionResource: undefined, + }; + + const result = await analyzer.analyze(options); + strictEqual(result.isAutoApproveAllowed, false); + strictEqual(result.isAutoApproved, undefined); + strictEqual(result.disclaimers?.length ?? 0, 0); + }); + + test('should auto approve empty command strings when sub-command parsing returns an empty list', async () => { + const options: ICommandLineAnalyzerOptions = { + commandLine: ' ', + cwd: undefined, + shell: 'pwsh', + os: OperatingSystem.Windows, + treeSitterLanguage: TreeSitterCommandParserLanguage.PowerShell, + terminalToolSessionId: 'test', + chatSessionResource: undefined, + }; + + const result = await analyzer.analyze(options); + strictEqual(result.isAutoApproveAllowed, true); + strictEqual(result.isAutoApproved, true); + strictEqual(result.disclaimers?.length ?? 0, 0); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/getTerminalOutputTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/getTerminalOutputTool.test.ts new file mode 100644 index 00000000000..ad53a0598c3 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/getTerminalOutputTool.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { GetTerminalOutputTool, GetTerminalOutputToolData } from '../../browser/tools/getTerminalOutputTool.js'; +import { RunInTerminalTool, type IActiveTerminalExecution } from '../../browser/tools/runInTerminalTool.js'; +import type { IToolInvocation } from '../../../../chat/common/tools/languageModelToolsService.js'; +import type { ITerminalExecuteStrategyResult } from '../../browser/executeStrategy/executeStrategy.js'; +import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; + +suite('GetTerminalOutputTool', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + const UNKNOWN_TERMINAL_ID = '123e4567-e89b-12d3-a456-426614174000'; + const KNOWN_TERMINAL_ID = '123e4567-e89b-12d3-a456-426614174001'; + let tool: GetTerminalOutputTool; + let originalGetExecution: typeof RunInTerminalTool.getExecution; + + setup(() => { + tool = store.add(new GetTerminalOutputTool()); + originalGetExecution = RunInTerminalTool.getExecution; + }); + + teardown(() => { + RunInTerminalTool.getExecution = originalGetExecution; + }); + + function createInvocation(id: string): IToolInvocation { + return { + parameters: { id }, + callId: 'test-call', + context: { sessionId: 'test-session' }, + toolId: 'get_terminal_output', + tokenBudget: 1000, + isComplete: () => false, + isCancellationRequested: false, + } as unknown as IToolInvocation; + } + + function createMockExecution(output: string): IActiveTerminalExecution { + return { + completionPromise: Promise.resolve({ output } as ITerminalExecuteStrategyResult), + instance: {} as ITerminalInstance, + getOutput: () => output, + }; + } + + test('tool description documents opaque terminal ids', () => { + const idProperty = GetTerminalOutputToolData.inputSchema?.properties?.id as { description?: string; pattern?: string } | undefined; + assert.ok(GetTerminalOutputToolData.modelDescription.includes('exact opaque value')); + assert.ok(/exact opaque id returned by that tool/i.test(idProperty?.description ?? '')); + assert.ok(idProperty?.pattern?.includes('[0-9a-fA-F]{8}')); + }); + + test('returns explicit error for unknown terminal id', async () => { + RunInTerminalTool.getExecution = () => undefined; + + const result = await tool.invoke( + createInvocation(UNKNOWN_TERMINAL_ID), + async () => 0, + { report: () => { } }, + CancellationToken.None, + ); + + assert.strictEqual(result.content.length, 1); + assert.strictEqual(result.content[0].kind, 'text'); + const value = (result.content[0] as { value: string }).value; + assert.ok(value.includes('No active terminal execution found')); + assert.ok(value.includes('exact value returned by run_in_terminal')); + }); + + test('returns output for active terminal id', async () => { + RunInTerminalTool.getExecution = () => createMockExecution('line1\nline2'); + + const result = await tool.invoke( + createInvocation(KNOWN_TERMINAL_ID), + async () => 0, + { report: () => { } }, + CancellationToken.None, + ); + + assert.strictEqual(result.content.length, 1); + assert.strictEqual(result.content[0].kind, 'text'); + const value = (result.content[0] as { value: string }).value; + assert.ok(value.includes(`Output of terminal ${KNOWN_TERMINAL_ID}:`)); + assert.ok(value.includes('line1\nline2')); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/noneExecuteStrategy.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/noneExecuteStrategy.test.ts new file mode 100644 index 00000000000..5618680c591 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/noneExecuteStrategy.test.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import type { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { NoneExecuteStrategy } from '../../browser/executeStrategy/noneExecuteStrategy.js'; +import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; + +suite('NoneExecuteStrategy', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + function createLogService(): ITerminalLogService { + return new class extends NullLogService { readonly _logBrand = undefined; }; + } + + /** + * Creates a mock terminal instance and xterm for testing NoneExecuteStrategy. + * + * @param contentsAsText The text that `xterm.getContentsAsText()` will return (simulates + * the terminal buffer content between the start and end markers) + * @param cursorLineText The text at the cursor line, used by prompt detection heuristics + */ + function createMockTerminalAndXterm(contentsAsText: string, cursorLineText: string): { + instance: ITerminalInstance; + onDataEmitter: Emitter; + } { + const onDataEmitter = store.add(new Emitter()); + const activeBuffer = {}; + const alternateBuffer = {}; // different object → not alt buffer + + const mockXterm = { + raw: { + registerMarker: () => ({ + line: 0, + isDisposed: false, + onDispose: Event.None, + dispose: () => { }, + }), + buffer: { + active: { + ...activeBuffer, + baseY: 0, + cursorY: 1, + getLine: () => ({ + translateToString: () => cursorLineText, + }), + }, + alternate: alternateBuffer, + onBufferChange: () => ({ dispose: () => { } }), + }, + onWriteParsed: Event.None, + }, + getContentsAsText: () => contentsAsText, + }; + + const mockInstance = { + xtermReadyPromise: Promise.resolve(mockXterm), + onData: onDataEmitter.event, + sendText: () => { }, + } as unknown as ITerminalInstance; + + return { instance: mockInstance, onDataEmitter }; + } + + test('should report "Command produced no output" when output is empty', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + // Simulate a command that produces no output. Between the start and end markers, + // getContentsAsText returns only whitespace (no actual command output). + const { instance } = createMockTerminalAndXterm( + ' \n \n ', // only whitespace between markers + 'user@host:~$ ' // prompt at cursor line → triggers prompt detection + ); + + const logService = createLogService(); + const configService = new TestConfigurationService(); + const strategy = store.add(new NoneExecuteStrategy(instance, () => false, configService, logService)); + const cts = store.add(new CancellationTokenSource()); + + const result = await strategy.execute('echo test', cts.token); + + assert.strictEqual(result.additionalInformation, 'Command produced no output'); + })); + + test('should not leak sandbox command echo as output when command produces no output', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + // This simulates the exact scenario from issue #303531: + // A sandboxed command produces no output, but getContentsAsText returns the + // prompt + sandbox-wrapped command echo + next prompt line. + const promptLine = '[ user@host:~/src (main) ] $ '; + const sandboxCommandEcho = 'ELECTRON_RUN_AS_NODE=1 PATH="$PATH:/app/node_modules/@vscode/ripgrep/bin" ' + + 'TMPDIR="/var/folders/bb/_8jjjyy971x2frm3nr3g7m4r0000gn/T" ' + + '"/app/Contents/MacOS/Code - Insiders" "/app/Contents/Resources/app/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js" ' + + '--settings "/var/folders/bb/_8jjjyy971x2frm3nr3g7m4r0000gn/T/vscode-sandbox-settings.json" ' + + '-c \' git diff 0e5d5949d13f..2c357a926df6 -- \'\\\'\'src/foo.ts\'\\\'\' | grep -A3 -B3 \'\\\'\'someFunc\'\\\'\'\''; + const terminalContent = `${promptLine}${sandboxCommandEcho}\n${' '.repeat(80)}\n${promptLine}`; + + const { instance } = createMockTerminalAndXterm( + terminalContent, + promptLine // prompt at cursor line → triggers prompt detection + ); + + const logService = createLogService(); + const configService = new TestConfigurationService(); + const strategy = store.add(new NoneExecuteStrategy(instance, () => false, configService, logService)); + const cts = store.add(new CancellationTokenSource()); + + const result = await strategy.execute( + 'git diff 0e5d5949d13f..2c357a926df6 -- \'src/foo.ts\' | grep -A3 -B3 \'someFunc\'', + cts.token + ); + + // The output should NOT contain sandbox wrapper artifacts + assert.strictEqual(result.output?.includes('sandbox-runtime') ?? false, false, 'Output should not leak sandbox-runtime path'); + assert.strictEqual(result.output?.includes('ELECTRON_RUN_AS_NODE') ?? false, false, 'Output should not leak ELECTRON_RUN_AS_NODE'); + + // Should report that the command produced no output + assert.strictEqual(result.additionalInformation, 'Command produced no output'); + })); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputHelpers.test.ts new file mode 100644 index 00000000000..4dc62b623c6 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputHelpers.test.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 { strictEqual } from 'assert'; +import type { IMarker as IXtermMarker } from '@xterm/xterm'; +import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; +import { getOutput } from '../../browser/outputHelpers.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('outputHelpers', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + function createMockInstance(lines: { text: string; isWrapped?: boolean }[]): ITerminalInstance { + const buffer = { + length: lines.length, + getLine: (index: number) => { + const line = lines[index]; + if (!line) { + return undefined; + } + return { + isWrapped: !!line.isWrapped, + translateToString: (trimRight?: boolean) => trimRight ? line.text.replace(/\s+$/g, '') : line.text + }; + } + }; + return { + xterm: { + raw: { + buffer: { + active: buffer + } + } + } + } as unknown as ITerminalInstance; + } + + test('preserves explicit newline after an 80-column soft wrap', () => { + const line80 = 'A'.repeat(80); + const instance = createMockInstance([ + { text: line80 }, + { text: 'X', isWrapped: true }, + { text: 'after' } + ]); + + const output = getOutput(instance); + strictEqual(output, `${line80}X\nafter`); + }); + + test('rewinds marker when it starts on a wrapped continuation line', () => { + const line80 = 'A'.repeat(80); + const instance = createMockInstance([ + { text: line80 }, + { text: 'X', isWrapped: true }, + { text: 'after' } + ]); + + const marker = { line: 1 } as IXtermMarker; + const output = getOutput(instance, marker); + strictEqual(output, `${line80}X\nafter`); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts index f4f34ecd4e9..be398e10c0b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts @@ -452,6 +452,7 @@ suite('OutputMonitor', () => { test('detects VS Code task completion messages', () => { assert.strictEqual(detectsVSCodeTaskFinishMessage('Press any key to close the terminal.'), true); assert.strictEqual(detectsVSCodeTaskFinishMessage('Terminal will be reused by tasks, press any key to close it.'), true); + assert.strictEqual(detectsVSCodeTaskFinishMessage('The terminal will be reused by tasks. Press any key to close. Please provide the required input to the terminal.'), true); // Case insensitive assert.strictEqual(detectsVSCodeTaskFinishMessage('press any key to close the terminal.'), true); assert.strictEqual(detectsVSCodeTaskFinishMessage('PRESS ANY KEY TO CLOSE THE TERMINAL.'), true); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts index 0e6ede9ea6f..49735c06797 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ok, strictEqual } from 'assert'; -import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix } from '../../browser/runInTerminalHelpers.js'; +import { generateAutoApproveActions, TRUNCATION_MESSAGE, dedupeRules, isPowerShell, sanitizeTerminalOutput, truncateOutputKeepingTail, extractCdPrefix, normalizeTerminalCommandForDisplay } from '../../browser/runInTerminalHelpers.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; @@ -293,6 +293,25 @@ suite('sanitizeTerminalOutput', () => { }); }); +suite('normalizeTerminalCommandForDisplay', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('removes escaped single and double quotes', () => { + const input = 'git rev-parse \\\'stash@{0}\\\' && echo \\\"done\\\"'; + strictEqual(normalizeTerminalCommandForDisplay(input), 'git rev-parse \'stash@{0}\' && echo "done"'); + }); + + test('normalizes escaped forward slashes', () => { + const input = 'echo \\/Users\\/me\\/project'; + strictEqual(normalizeTerminalCommandForDisplay(input), 'echo /Users/me/project'); + }); + + test('preserves non-quote escapes', () => { + const input = 'echo path\\ with\\ spaces'; + strictEqual(normalizeTerminalCommandForDisplay(input), input); + }); +}); + suite('generateAutoApproveActions', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxOutputAnalyzer.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxOutputAnalyzer.test.ts new file mode 100644 index 00000000000..2349129f7f5 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxOutputAnalyzer.test.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { outputLooksSandboxBlocked } from '../../browser/tools/sandboxOutputAnalyzer.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('outputLooksSandboxBlocked', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const positives: [string, string][] = [ + ['macOS sandbox file write', '/bin/bash: /tmp/test.txt: Operation not permitted'], + ['Linux sandbox file write', '/usr/bin/bash: /tmp/test.txt: Read-only file system'], + ['Permission denied', 'bash: ./script.sh: Permission denied'], + ['sandbox-exec reference', 'sandbox-exec: some error occurred'], + ['bwrap reference', 'bwrap: error setting up namespace'], + ['sandbox_violation', 'sandbox_violation: deny(1) file-write-create /tmp/foo'], + ['case insensitive', '/bin/bash: OPERATION NOT PERMITTED'], + ['wrapped across lines', '/bin/bash: Operation not\npermitted'], + ]; + + for (const [label, output] of positives) { + test(`detects: ${label}`, () => { + strictEqual(outputLooksSandboxBlocked(output), true); + }); + } + + const negatives: [string, string][] = [ + ['normal output', 'hello world'], + ['empty output', ''], + ['unrelated error', 'Error: ENOENT: no such file or directory'], + ]; + + for (const [label, output] of negatives) { + test(`ignores: ${label}`, () => { + strictEqual(outputLooksSandboxBlocked(output), false); + }); + } +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxedCommandLinePresenter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxedCommandLinePresenter.test.ts index b1a8fed729a..e3e2cca256f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxedCommandLinePresenter.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/sandboxedCommandLinePresenter.test.ts @@ -56,6 +56,17 @@ suite('SandboxedCommandLinePresenter', () => { strictEqual(result.languageDisplayName, undefined); }); + test('should use forDisplay over original when both are provided', async () => { + const presenter = createPresenter(); + const result = await presenter.present({ + commandLine: { original: 'cd /some/path && ls -lh', forDisplay: 'ls -lh' }, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, 'ls -lh'); + }); + test('should return undefined when sandboxing is disabled', async () => { const presenter = createPresenter(false); const result = await presenter.present({ diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/strategyHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/strategyHelpers.test.ts new file mode 100644 index 00000000000..8e22ff4a36e --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/strategyHelpers.test.ts @@ -0,0 +1,564 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { stripCommandEchoAndPrompt } from '../../browser/executeStrategy/strategyHelpers.js'; + +suite('stripCommandEchoAndPrompt', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('strips single-line command echo and trailing prompt', () => { + const output = [ + 'user@host:~/src $ echo hello', + 'hello', + 'user@host:~/src $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hello'), + 'hello' + ); + }); + + test('strips command echo with zsh-style prompt (] $ )', () => { + const output = [ + 's/testWorkspace (main**) ] $ true', + '[ alex@Alexandrus-MacBook-Pro:/Users/alex/src/vscode4/extensions/vscode-api-test', + 's/testWorkspace (main**) ] $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('preserves actual command output between echo and prompt', () => { + const output = [ + 's/testWorkspace (main**) ] $ echo MARKER_123', + 'MARKER_123', + '[ alex@host:/some/path', + 's/testWorkspace (main**) ] $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo MARKER_123'), + 'MARKER_123' + ); + }); + + test('preserves multi-line command output', () => { + const output = [ + 'user@host:~ $ echo line1 && echo line2 && echo line3', + 'line1', + 'line2', + 'line3', + 'user@host:~ $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo line1 && echo line2 && echo line3'), + 'line1\nline2\nline3' + ); + }); + + test('handles empty output (no-output command)', () => { + const output = [ + 's/testWorkspace (main**) ] $ true', + '[ alex@host:/Users/alex/src/vscode4/extensions/vscode-api-test', + 's/testWorkspace (main**) ] $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('strips sandbox-wrapped command echo (long wrapped lines)', () => { + const sandboxCommand = 'ELECTRON_RUN_AS_NODE=1 PATH="$PATH:/app/rg/bin" TMPDIR="/tmp/sandbox" "/app/sandbox-runtime/dist/cli.js" --settings "/tmp/sandbox-settings.json" -c \'curl -s https://example.com\''; + const output = [ + 's/testWorkspace (main**) ] $ ELECTRON_RUN_AS_NODE=1 PATH="$PATH:/app/rg/bin" T', + 'MPDIR="/tmp/sandbox" "/app/sandbox-runtime/dist/cli.js" --settings "/tmp/sand', + 'box-settings.json" -c \'curl -s https://example.com\'', + '[ alex@host:/Users/alex/src/vscode4/extensions/vscode-api-test', + 's/testWorkspace (main**) ] $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, sandboxCommand), + '' + ); + }); + + test('strips trailing prompt with various prompt styles', () => { + // bash user@host:path $ + assert.strictEqual( + stripCommandEchoAndPrompt( + ['user@host:~ $ echo hello', 'hello', 'user@host:~ $ '].join('\n'), + 'echo hello' + ), + 'hello', + 'Failed for bash $ prompt' + ); + // root user@host:path # + assert.strictEqual( + stripCommandEchoAndPrompt( + ['root@server:/var/log# echo hello', 'hello', 'root@server:/var/log# '].join('\n'), + 'echo hello' + ), + 'hello', + 'Failed for root # prompt' + ); + // bracketed prompt ending with ] $ + assert.strictEqual( + stripCommandEchoAndPrompt( + ['s/workspace ] $ echo hello', 'hello', 's/workspace ] $ '].join('\n'), + 'echo hello' + ), + 'hello', + 'Failed for bracketed ] $ prompt' + ); + // PowerShell PS C:\> + assert.strictEqual( + stripCommandEchoAndPrompt( + ['PS C:\\Users\\test> echo hello', 'hello', 'PS C:\\Users\\test>'].join('\n'), + 'echo hello' + ), + 'hello', + 'Failed for PowerShell prompt' + ); + }); + + test('does not strip output lines ending with prompt-like characters', () => { + // Output ending with % (e.g. percentage) + assert.strictEqual( + stripCommandEchoAndPrompt( + ['user@host:~ $ echo "100%"', '100%', 'user@host:~ $ '].join('\n'), + 'echo "100%"' + ), + '100%', + 'Should not strip line ending with %' + ); + // Output ending with > (e.g. HTML or comparison) + assert.strictEqual( + stripCommandEchoAndPrompt( + ['user@host:~ $ echo "
"', '
', 'user@host:~ $ '].join('\n'), + 'echo "
"' + ), + '
', + 'Should not strip line ending with >' + ); + // Output ending with # (e.g. comment marker) + assert.strictEqual( + stripCommandEchoAndPrompt( + ['user@host:~ $ echo "item #"', 'item #', 'user@host:~ $ '].join('\n'), + 'echo "item #"' + ), + 'item #', + 'Should not strip line ending with #' + ); + }); + + test('handles command with leading space (history prevention)', () => { + const output = [ + 'user@host:~ $ echo hello', + 'hello', + 'user@host:~ $ ', + ].join('\n'); + + // The command has a leading space (from CommandLinePreventHistoryRewriter) + assert.strictEqual( + stripCommandEchoAndPrompt(output, ' echo hello'), + 'hello' + ); + }); + + test('does not strip actual output lines that happen to contain prompt chars', () => { + const output = [ + 'user@host:~ $ echo "price is $5"', + 'price is $5', + 'user@host:~ $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo "price is $5"'), + 'price is $5' + ); + }); + + test('handles output with no trailing prompt (e.g. command still running)', () => { + const output = [ + 'user@host:~ $ echo hello', + 'hello', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hello'), + 'hello' + ); + }); + + test('handles output with only the command echo and no prompt', () => { + const output = 'user@host:~ $ true'; + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('handles empty string input', () => { + assert.strictEqual( + stripCommandEchoAndPrompt('', 'echo hello'), + '' + ); + }); + + test('handles bash -c subshell command echo', () => { + const output = [ + 's/testWorkspace (main**) ] $ bash -c "exit 42"', + '[ alex@host:/Users/alex/src/vscode4/extensions/vscode-api-test', + 's/testWorkspace (main**) ] $ ', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'bash -c "exit 42"'), + '' + ); + }); + + test('strips wrapped prompt lines with user@hostname pattern', () => { + const output = [ + 'user@host:~ $ echo hi', + 'hi', + '[ alex@Alexandrus-MacBook-Pro:/very/long/path/that/wraps/across/terminal/col', + 'umns/in/the/test/workspace ] $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hi'), + 'hi' + ); + }); + + test('handles PowerShell-style prompt (PS C:\\>)', () => { + const output = [ + 'PS C:\\Users\\test> echo hello', + 'hello', + 'PS C:\\Users\\test>', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hello'), + 'hello' + ); + }); + + test('strips stale prompt fragments and ^C residue before command echo', () => { + // Simulates CI environment where previous ^C produces stale prompt + // fragments before the actual command echo line + const output = [ + 'ts/testWorkspace$ ^C', + 'cloudtest@5ac6b023c000000:/mnt/vss/_work/vscode/vscode/extensions/vscode-api-tes', + 'ts/testWorkspace$ echo MARKER_123', + 'MARKER_123', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo MARKER_123'), + 'MARKER_123' + ); + }); + + test('strips stale prompt fragments for no-output command', () => { + const output = [ + 'ts/testWorkspace$ ^C', + 'cloudtest@5ac6b023c000000:/mnt/vss/_work/vscode/vscode/extensions/vscode-api-tes', + 'ts/testWorkspace$ true', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('strips stale prompt fragments for multi-line output', () => { + const output = [ + 'ts/testWorkspace$ ^C', + 'cloudtest@5ac6b023c000000:/mnt/vss/_work/vscode/vscode/extensions/vscode-api-tes', + 'ts/testWorkspace$ echo M1 && echo M2 && echo M3', + 'M1', + 'M2', + 'M3', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo M1 && echo M2 && echo M3'), + 'M1\nM2\nM3' + ); + }); + + test('strips trailing prompt without @ (hostname:path user$)', () => { + const output = [ + 'dsm12-be220-abc:testWorkspace runner$ echo hello', + 'hello', + 'dsm12-be220-abc:testWorkspace runner$', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hello'), + 'hello' + ); + }); + + test('strips wrapped trailing prompt without @ (hostname:path + fragment$)', () => { + const output = [ + 'dsm12-be220-abc:testWorkspace runner$ echo hello', + 'hello', + 'dsm12-be220-8627ea7f-2c5a-40cd-8ba1-bf324bb4f59a-DA35C080942E:testWorkspace runn', + 'er$', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hello'), + 'hello' + ); + }); + + test('strips wrapped trailing prompt with path-like fragment (ts/testWorkspace$)', () => { + const output = [ + 'user@host:~ $ echo hello', + 'hello', + 'cloudtest@d4b0d881c000000:/mnt/vss/_work/vscode/vscode/extensions/vscode-api-tes', + 'ts/testWorkspace$', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo hello'), + 'hello' + ); + }); + + test('strips trailing prompt fragment for no-output command', () => { + const output = [ + 'dsm12-be220-abc:testWorkspace runner$ true', + 'dsm12-be220-8627ea7f-2c5a-40cd-8ba1-bf324bb4f59a-DA35C080942E:testWorkspace runn', + 'er$', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('strips mid-word wrapped command continuation (PowerShell/Windows)', () => { + // PowerShell wraps "echo MARKER_123_ECHO" across lines at column boundary + const output = [ + 'PS D:\\a\\_work\\vscode\\testWorkspace> echo MARK', + 'ER_123_ECHO', + 'MARKER_123_ECHO', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo MARKER_123_ECHO'), + 'MARKER_123_ECHO' + ); + }); + + test('strips PowerShell prompt from getOutput() result', () => { + // When shell integration markers misfire, getOutput() includes the prompt + command + const output = 'PS D:\\a\\_work\\vscode\\testWorkspace> cmd /c exit 42'; + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'cmd /c exit 42'), + '' + ); + }); + + test('strips partial command echo (suffix from wrapped getOutput)', () => { + // When getOutput() doesn't include the prompt line, only the wrapped + // continuation of the command echo appears at the start of the output. + const output = [ + '90741 ; echo M2_1774133190741 ; echo M3_1774133190741', + 'M1_1774133190741', + 'M2_1774133190741', + 'M3_1774133190741', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo M1_1774133190741 ; echo M2_1774133190741 ; echo M3_1774133190741'), + 'M1_1774133190741\nM2_1774133190741\nM3_1774133190741' + ); + }); + + test('strips bracketed prompt without @ (hostname:path format)', () => { + // macOS CI prompt: [hostname:path] username$ (wrapped so username is truncated) + const output = [ + '[W007DV9PF9-1:~/vss/_work/1/s/extensions/vscode-api-tests/testWorkspace] cloudte', + 'st$', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('strips bracketed prompt without @ (single line, no trailing $)', () => { + // When the terminal captures just the prompt (no-output command) + const output = '[W007DV9PF9-1:~/vss/_work/1/s/extensions/vscode-api-tests/testWorkspace] cloudte'; + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'true'), + '' + ); + }); + + test('strips bracketed prompt without @ with command echo', () => { + const output = [ + '[W007DV9PF9-1:~/vss/_work] cloudtest$ echo MARKER_123', + 'MARKER_123', + '[W007DV9PF9-1:~/vss/_work] cloudtest$', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo MARKER_123'), + 'MARKER_123' + ); + }); + + test('strips sandbox-wrapped command echo with error output and trailing prompt', () => { + const commandLine = 'ELECTRON_RUN_AS_NODE=1 PATH="$PATH:/Users/alex/src/vscode4/node_modules/@vscode/ripgrep/bin" TMPDIR="/Users/alex/.vscode-oss-dev/tmp" CLAUDE_TMPDIR="/Users/alex/.vscode-oss-dev/tmp" "/Users/alex/src/vscode4/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js" --settings "/Users/alex/.vscode-oss-dev/tmp/vscode-sandbox-settings-cf5b6232-825b-4f4c-8902-32a8591007fd.json" -c \' echo "SANDBOX_TMP_1774127409076" > /tmp/SANDBOX_TMP_1774127409076.txt\''; + const output = [ + 'ELECTRON_RUN_AS_NODE=1 PATH="$PATH:/Users/alex/src/vscode4/node_modules/@vscode/', + 'ripgrep/bin" TMPDIR="/Users/alex/.vscode-oss-dev/tmp" CLAUDE_TMPDIR="/Users/alex', + '/.vscode-oss-dev/tmp" "/Users/alex/src/vscode4/node_modules/@anthropic-ai/sandbo', + 'x-runtime/dist/cli.js" --settings "/Users/alex/.vscode-oss-dev/tmp/vscode-sandbo', + 'x-settings-cf5b6232-825b-4f4c-8902-32a8591007fd.json" -c \' echo "SANDBOX_TMP_177', + '4127409076" > /tmp/SANDBOX_TMP_1774127409076.txt\'', + '[ alex@Alexandrus-MacBook-Pro:/Users/alex/src/vscode4/extensions/vscode-api-test', + 's/testWorkspace (alexdima/fix-303531-sandbox-no-output-leak**) ] $ ELECTRON_RUN_', + 'AS_NODE=1 PATH="$PATH:/Users/alex/src/vscode4/node_modules/@vscode/ripgrep/bin" ', + 'TMPDIR="/Users/alex/.vscode-oss-dev/tmp" CLAUDE_TMPDIR="/Users/alex/.vscode-oss-', + 'dev/tmp" "/Users/alex/src/vscode4/node_modules/@anthropic-ai/sandbox-runtime/dis', + 't/cli.js" --settings "/Users/alex/.vscode-oss-dev/tmp/vscode-sandbox-settings-cf', + '5b6232-825b-4f4c-8902-32a8591007fd.json" -c \' echo "SANDBOX_TMP_1774127409076" >', + ' /tmp/SANDBOX_TMP_1774127409076.txt\'', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, commandLine), + '' + ); + }); + + // --- Adversarial tests: output that looks like prompts --- + // These verify that realistic output is NOT falsely stripped. + + suite('adversarial: output resembling prompts', () => { + + test('output ending with $ is preserved (not confused with wrapped prompt)', () => { + const output = [ + 'user@host:~ $ echo \'test$\'', + 'test$', + 'user@host:~ $', + ].join('\n'); + + // 'user@host:~ $' is a complete prompt → stripped and loop stops. + // 'test$' is preserved because nothing above a complete prompt is stripped. + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo \'test$\''), + 'test$' + ); + }); + + test('output ending with # is preserved (not confused with wrapped prompt)', () => { + const output = [ + 'user@host:~ $ echo \'div#\'', + 'div#', + 'user@host:~ $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo \'div#\''), + 'div#' + ); + }); + + test('bracketed log output [tag:~/path] is preserved', () => { + const output = [ + 'user@host:~ $ node build.js', + '[build:~/dist] compiled successfully', + 'user@host:~ $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'node build.js'), + '[build:~/dist] compiled successfully' + ); + }); + + test('output containing user@host:path ending with # is preserved', () => { + const output = [ + 'user@host:~ $ cat /etc/motd', + 'admin@server:~/docs #', + 'user@host:~ $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'cat /etc/motd'), + 'admin@server:~/docs #' + ); + }); + + test('output ending with ] $ is preserved', () => { + const output = [ + 'user@host:~ $ echo \'values: [a, b] $\'', + 'values: [a, b] $', + 'user@host:~ $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'echo \'values: [a, b] $\''), + 'values: [a, b] $' + ); + }); + + test('multiple prompt-like output lines are all preserved', () => { + // Complete prompt at the bottom stops stripping immediately, + // so all prompt-like output lines above are preserved. + const output = [ + 'user@host:~ $ cat prompts.txt', + 'admin@server:~/docs $', + 'root@box:/var/log #', + 'test@dev:~ $', + 'user@host:~ $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, 'cat prompts.txt'), + 'admin@server:~/docs $\nroot@box:/var/log #\ntest@dev:~ $' + ); + }); + + test('multi-line output where last line has $ after non-word chars is preserved', () => { + const output = [ + 'user@host:~ $ ./report.sh', + 'Revenue: 1000', + 'Currency: USD$', + 'user@host:~ $', + ].join('\n'); + + assert.strictEqual( + stripCommandEchoAndPrompt(output, './report.sh'), + 'Revenue: 1000\nCurrency: USD$' + ); + }); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/taskHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/taskHelpers.test.ts new file mode 100644 index 00000000000..68de3205b14 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/taskHelpers.test.ts @@ -0,0 +1,265 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IToolInvocationContext } from '../../../../chat/common/tools/languageModelToolsService.js'; +import { Task } from '../../../../tasks/common/taskService.js'; +import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; +import { collectTerminalResults } from '../../browser/taskHelpers.js'; +import { IExecution, OutputMonitorState } from '../../browser/tools/monitoring/types.js'; + +suite('Task Helpers', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('collectTerminalResults reads output from invocation start marker', async () => { + const lines = ['old output', 'more old output', 'new output line 1', 'new output line 2']; + let markerDisposed = false; + const marker = { + line: 2, + dispose: () => { markerDisposed = true; } + }; + const terminal = { + instanceId: 1, + title: 'task-terminal', + shellLaunchConfig: { name: 'task-terminal' }, + registerMarker: () => marker, + xterm: { + raw: { + buffer: { + active: { + length: lines.length, + getLine: (y: number) => ({ translateToString: () => lines[y] }) + } + } + } + } + } as unknown as ITerminalInstance; + const task = { + _label: 'my-task', + configurationProperties: {} + } as Task; + const invocationContext: IToolInvocationContext = { + sessionResource: URI.parse('vscode-chat-session://test') + }; + const instantiationService = { + createInstance: (_ctor: unknown, execution: IExecution) => { + const didFinishEmitter = new Emitter(); + const monitor = { + onDidFinishCommand: didFinishEmitter.event, + pollingResult: { + output: execution.getOutput(), + pollDurationMs: 1, + state: OutputMonitorState.Idle + }, + outputMonitorTelemetryCounters: { + inputToolManualAcceptCount: 0, + inputToolManualRejectCount: 0, + inputToolManualChars: 0, + inputToolAutoAcceptCount: 0, + inputToolAutoChars: 0, + inputToolManualShownCount: 0, + inputToolFreeFormInputShownCount: 0, + inputToolFreeFormInputCount: 0, + }, + dispose: () => didFinishEmitter.dispose() + }; + setTimeout(() => didFinishEmitter.fire(), 0); + return monitor; + } + } as unknown as IInstantiationService; + + const disposableStore = new DisposableStore(); + const results = await collectTerminalResults( + [terminal], + task, + instantiationService, + invocationContext, + { report: () => { } }, + CancellationToken.None, + disposableStore + ); + disposableStore.dispose(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].output, 'new output line 1\nnew output line 2'); + assert.strictEqual(markerDisposed, true); + }); + + test('collectTerminalResults uses provided pre-run marker when present', async () => { + const lines = ['old output', 'new output line 1', 'new output line 2', '* Terminal will be reused by tasks, press any key to close it.']; + let defaultMarkerDisposed = false; + let preRunMarkerDisposed = false; + const defaultMarker = { + line: 3, + dispose: () => { defaultMarkerDisposed = true; } + }; + const preRunMarker = { + id: 1, + line: 1, + isDisposed: false, + onDispose: new Emitter().event, + dispose: () => { preRunMarkerDisposed = true; } + }; + const terminal = { + instanceId: 1, + title: 'task-terminal', + shellLaunchConfig: { name: 'task-terminal' }, + registerMarker: () => defaultMarker, + xterm: { + raw: { + buffer: { + active: { + length: lines.length, + getLine: (y: number) => ({ translateToString: () => lines[y] }) + } + } + } + } + } as unknown as ITerminalInstance; + const task = { + _label: 'my-task', + configurationProperties: {} + } as Task; + const invocationContext: IToolInvocationContext = { + sessionResource: URI.parse('vscode-chat-session://test') + }; + const instantiationService = { + createInstance: (_ctor: unknown, execution: IExecution) => { + const didFinishEmitter = new Emitter(); + const monitor = { + onDidFinishCommand: didFinishEmitter.event, + pollingResult: { + output: execution.getOutput(), + pollDurationMs: 1, + state: OutputMonitorState.Idle + }, + outputMonitorTelemetryCounters: { + inputToolManualAcceptCount: 0, + inputToolManualRejectCount: 0, + inputToolManualChars: 0, + inputToolAutoAcceptCount: 0, + inputToolAutoChars: 0, + inputToolManualShownCount: 0, + inputToolFreeFormInputShownCount: 0, + inputToolFreeFormInputCount: 0, + }, + dispose: () => didFinishEmitter.dispose() + }; + setTimeout(() => didFinishEmitter.fire(), 0); + return monitor; + } + } as unknown as IInstantiationService; + + const startMarkersByTerminalInstanceId = new Map>(); + startMarkersByTerminalInstanceId.set(terminal.instanceId, preRunMarker as ReturnType); + + const disposableStore = new DisposableStore(); + const results = await collectTerminalResults( + [terminal], + task, + instantiationService, + invocationContext, + { report: () => { } }, + CancellationToken.None, + disposableStore, + undefined, + undefined, + undefined, + startMarkersByTerminalInstanceId + ); + disposableStore.dispose(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].output, 'new output line 1\nnew output line 2\n* Terminal will be reused by tasks, press any key to close it.'); + assert.strictEqual(preRunMarkerDisposed, true); + assert.strictEqual(defaultMarkerDisposed, false); + }); + + test('collectTerminalResults reads full output when pre-run marker map has no marker for terminal', async () => { + const lines = ['new output line 1', 'new output line 2', '* Terminal will be reused by tasks, press any key to close it.']; + let defaultMarkerDisposed = false; + const defaultMarker = { + line: 1, + dispose: () => { defaultMarkerDisposed = true; } + }; + const terminal = { + instanceId: 1, + title: 'task-terminal', + shellLaunchConfig: { name: 'task-terminal' }, + registerMarker: () => defaultMarker, + xterm: { + raw: { + buffer: { + active: { + length: lines.length, + getLine: (y: number) => ({ translateToString: () => lines[y] }) + } + } + } + } + } as unknown as ITerminalInstance; + const task = { + _label: 'my-task', + configurationProperties: {} + } as Task; + const invocationContext: IToolInvocationContext = { + sessionResource: URI.parse('vscode-chat-session://test') + }; + const instantiationService = { + createInstance: (_ctor: unknown, execution: IExecution) => { + const didFinishEmitter = new Emitter(); + const monitor = { + onDidFinishCommand: didFinishEmitter.event, + pollingResult: { + output: execution.getOutput(), + pollDurationMs: 1, + state: OutputMonitorState.Idle + }, + outputMonitorTelemetryCounters: { + inputToolManualAcceptCount: 0, + inputToolManualRejectCount: 0, + inputToolManualChars: 0, + inputToolAutoAcceptCount: 0, + inputToolAutoChars: 0, + inputToolManualShownCount: 0, + inputToolFreeFormInputShownCount: 0, + inputToolFreeFormInputCount: 0, + }, + dispose: () => didFinishEmitter.dispose() + }; + setTimeout(() => didFinishEmitter.fire(), 0); + return monitor; + } + } as unknown as IInstantiationService; + + const startMarkersByTerminalInstanceId = new Map>(); + + const disposableStore = new DisposableStore(); + const results = await collectTerminalResults( + [terminal], + task, + instantiationService, + invocationContext, + { report: () => { } }, + CancellationToken.None, + disposableStore, + undefined, + undefined, + undefined, + startMarkersByTerminalInstanceId + ); + disposableStore.dispose(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].output, 'new output line 1\nnew output line 2\n* Terminal will be reused by tasks, press any key to close it.'); + assert.strictEqual(defaultMarkerDisposed, false); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts index fdc4ab166ab..a85db3988f2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts @@ -6,12 +6,14 @@ import { strictEqual, ok } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { TestLifecycleService, workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { TestProductService } from '../../../../../test/common/workbenchTestServices.js'; import { TerminalSandboxService } from '../../common/terminalSandboxService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../../platform/product/common/productService.js'; import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; import { ITrustedDomainService } from '../../../../url/common/trustedDomainService.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -21,6 +23,9 @@ import { TestConfigurationService } from '../../../../../../platform/configurati import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { IRemoteAgentEnvironment } from '../../../../../../platform/remote/common/remoteAgentEnvironment.js'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, WorkbenchState } from '../../../../../../platform/workspace/common/workspace.js'; +import { testWorkspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js'; +import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; suite('TerminalSandboxService - allowTrustedDomains', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -29,7 +34,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { let configurationService: TestConfigurationService; let trustedDomainService: MockTrustedDomainService; let fileService: MockFileService; + let lifecycleService: TestLifecycleService; + let workspaceContextService: MockWorkspaceContextService; + let productService: IProductService; let createdFiles: Map; + let createdFolders: string[]; + let deletedFolders: string[]; + const windowId = 7; class MockTrustedDomainService implements ITrustedDomainService { _serviceBrand: undefined; @@ -47,6 +58,15 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { createdFiles.set(uri.path, contentString); return {}; } + + async createFolder(uri: URI): Promise { + createdFolders.push(uri.path); + return {}; + } + + async del(uri: URI): Promise { + deletedFolders.push(uri.path); + } } class MockRemoteAgentService { @@ -57,6 +77,7 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { os: OperatingSystem.Linux, tmpDir: URI.file('/tmp'), appRoot: URI.file('/app'), + execPath: '/app/node', pid: 1234, connectionToken: 'test-token', settingsPath: URI.file('/settings'), @@ -79,40 +100,98 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { } } + class MockWorkspaceContextService implements IWorkspaceContextService { + _serviceBrand: undefined; + readonly onDidChangeWorkbenchState = Event.None; + readonly onDidChangeWorkspaceName = Event.None; + readonly onWillChangeWorkspaceFolders = Event.None; + private readonly _onDidChangeWorkspaceFolders = new Emitter(); + readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event; + private _workspace: IWorkspace = testWorkspace(); + + getCompleteWorkspace(): Promise { + return Promise.resolve(this._workspace); + } + + getWorkspace(): IWorkspace { + return this._workspace; + } + + getWorkbenchState(): WorkbenchState { + return this._workspace.folders.length > 0 ? WorkbenchState.FOLDER : WorkbenchState.EMPTY; + } + + getWorkspaceFolder(_resource: URI): IWorkspaceFolder | null { + return null; + } + + isCurrentWorkspace(_workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean { + return false; + } + + isInsideWorkspace(_resource: URI): boolean { + return false; + } + + hasWorkspaceData(): boolean { + return this._workspace.folders.length > 0; + } + + setWorkspaceFolders(folders: URI[]): void { + const previousFolders = this._workspace.folders; + this._workspace = testWorkspace(...folders); + this._onDidChangeWorkspaceFolders.fire({ + added: this._workspace.folders.filter(folder => !previousFolders.some(previousFolder => previousFolder.uri.toString() === folder.uri.toString())), + removed: previousFolders.filter(folder => !this._workspace.folders.some(nextFolder => nextFolder.uri.toString() === folder.uri.toString())), + changed: [] + }); + } + } + setup(() => { createdFiles = new Map(); + createdFolders = []; + deletedFolders = []; instantiationService = workbenchInstantiationService({}, store); configurationService = new TestConfigurationService(); trustedDomainService = new MockTrustedDomainService(); fileService = new MockFileService(); + lifecycleService = store.add(new TestLifecycleService()); + workspaceContextService = new MockWorkspaceContextService(); + productService = { + ...TestProductService, + dataFolderName: '.test-data', + serverDataFolderName: '.test-server-data' + }; + workspaceContextService.setWorkspaceFolders([URI.file('/workspace-one')]); // Setup default configuration configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled, true); - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: [], - deniedDomains: [], - allowTrustedDomains: false - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, false); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IFileService, fileService); - instantiationService.stub(IEnvironmentService, { + instantiationService.stub(IEnvironmentService, { _serviceBrand: undefined, tmpDir: URI.file('/tmp'), - execPath: '/usr/bin/node' + execPath: '/usr/bin/node', + window: { id: windowId } }); instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IProductService, productService); instantiationService.stub(IRemoteAgentService, new MockRemoteAgentService()); instantiationService.stub(ITrustedDomainService, trustedDomainService); + instantiationService.stub(IWorkspaceContextService, workspaceContextService); + instantiationService.stub(ILifecycleService, lifecycleService); }); test('should filter out sole wildcard (*) from trusted domains', async () => { // Setup: Enable allowTrustedDomains and add * to trusted domains - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: [], - deniedDomains: [], - allowTrustedDomains: true - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, true); trustedDomainService.trustedDomains = ['*']; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -128,11 +207,9 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { test('should allow wildcards with domains like *.github.com', async () => { // Setup: Enable allowTrustedDomains and add *.github.com - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: [], - deniedDomains: [], - allowTrustedDomains: true - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, true); trustedDomainService.trustedDomains = ['*.github.com']; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -149,11 +226,9 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { test('should combine trusted domains with configured allowedDomains, filtering out *', async () => { // Setup: Enable allowTrustedDomains with multiple domains including * - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: ['example.com'], - deniedDomains: [], - allowTrustedDomains: true - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, ['example.com']); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, true); trustedDomainService.trustedDomains = ['*', '*.github.com', 'microsoft.com']; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -173,11 +248,9 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { test('should not include trusted domains when allowTrustedDomains is false', async () => { // Setup: Disable allowTrustedDomains - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: ['example.com'], - deniedDomains: [], - allowTrustedDomains: false - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, ['example.com']); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, false); trustedDomainService.trustedDomains = ['*', '*.github.com']; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -194,11 +267,9 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { test('should deduplicate domains when combining sources', async () => { // Setup: Same domain in both sources - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: ['github.com', '*.github.com'], - deniedDomains: [], - allowTrustedDomains: true - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, ['github.com', '*.github.com']); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, true); trustedDomainService.trustedDomains = ['*.github.com', 'github.com']; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -216,11 +287,9 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { test('should handle empty trusted domains list', async () => { // Setup: Empty trusted domains - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: ['example.com'], - deniedDomains: [], - allowTrustedDomains: true - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, ['example.com']); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, true); trustedDomainService.trustedDomains = []; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -237,11 +306,9 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { test('should handle only * in trusted domains', async () => { // Setup: Only * in trusted domains (edge case) - configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, { - allowedDomains: [], - deniedDomains: [], - allowTrustedDomains: true - }); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkDeniedDomains, []); + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowTrustedDomains, true); trustedDomainService.trustedDomains = ['*']; const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -255,6 +322,60 @@ suite('TerminalSandboxService - allowTrustedDomains', () => { strictEqual(config.network.allowedDomains.length, 0, 'Should have no domains (* filtered out)'); }); + test('should refresh allowWrite paths when workspace folders change', async () => { + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem, { + allowWrite: ['/configured/path'], + denyRead: [], + denyWrite: [] + }); + + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + + ok(configPath, 'Config path should be defined'); + const initialConfigContent = createdFiles.get(configPath); + ok(initialConfigContent, 'Config file should be created for the initial workspace folders'); + + const initialConfig = JSON.parse(initialConfigContent); + ok(initialConfig.filesystem.allowWrite.includes('/workspace-one'), 'Initial config should include the original workspace folder'); + ok(initialConfig.filesystem.allowWrite.includes('/configured/path'), 'Initial config should include configured allowWrite paths'); + + workspaceContextService.setWorkspaceFolders([URI.file('/workspace-two')]); + + const refreshedConfigPath = await sandboxService.getSandboxConfigPath(); + strictEqual(refreshedConfigPath, configPath, 'Config path should stay stable when the config is refreshed'); + + const refreshedConfigContent = createdFiles.get(configPath); + ok(refreshedConfigContent, 'Config file should be rewritten after workspace folders change'); + + const refreshedConfig = JSON.parse(refreshedConfigContent); + ok(refreshedConfig.filesystem.allowWrite.includes('/workspace-two'), 'Refreshed config should include the updated workspace folder'); + ok(!refreshedConfig.filesystem.allowWrite.includes('/workspace-one'), 'Refreshed config should remove the old workspace folder'); + ok(refreshedConfig.filesystem.allowWrite.includes('/configured/path'), 'Refreshed config should preserve configured allowWrite paths'); + }); + + test('should create sandbox temp dir under the server data folder', async () => { + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + const configPath = await sandboxService.getSandboxConfigPath(); + const expectedTempDir = URI.joinPath(URI.file('/home/user'), productService.serverDataFolderName ?? productService.dataFolderName, 'tmp', `tmp_vscode_${windowId}`); + + strictEqual(sandboxService.getTempDir()?.path, expectedTempDir.path, 'Sandbox temp dir should live under the server data folder'); + strictEqual(createdFolders[0], expectedTempDir.path, 'Sandbox temp dir should be created before writing the config'); + ok(configPath?.startsWith(expectedTempDir.path), 'Sandbox config file should be written inside the sandbox temp dir'); + }); + + test('should delete sandbox temp dir on shutdown', async () => { + const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); + await sandboxService.getSandboxConfigPath(); + const expectedTempDir = URI.joinPath(URI.file('/home/user'), productService.serverDataFolderName ?? productService.dataFolderName, 'tmp', `tmp_vscode_${windowId}`); + + lifecycleService.fireShutdown(); + await Promise.all(lifecycleService.shutdownJoiners); + + strictEqual(lifecycleService.shutdownJoiners.length, 1, 'Shutdown should register a temp-dir cleanup joiner'); + strictEqual(deletedFolders[0], expectedTempDir.path, 'Shutdown should delete the sandbox temp dir'); + }); + test('should add ripgrep bin directory to PATH when wrapping command', async () => { const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); await sandboxService.getSandboxConfigPath(); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineSandboxRewriter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineSandboxRewriter.test.ts index 46fe1a19203..9a679676dd3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineSandboxRewriter.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineSandboxRewriter.test.ts @@ -78,4 +78,28 @@ suite('CommandLineSandboxRewriter', () => { strictEqual(result?.reasoning, 'Wrapped command for sandbox execution'); deepStrictEqual(calls, ['getSandboxConfigPath', 'wrapCommand']); }); + + test('does not wrap command when sandbox bypass was explicitly requested', async () => { + const calls: string[] = []; + stubSandboxService({ + isEnabled: async () => true, + wrapCommand: command => { + calls.push(`wrap:${command}`); + return `wrapped:${command}`; + }, + getSandboxConfigPath: async () => { + calls.push('config'); + return '/tmp/sandbox.json'; + }, + }); + + const rewriter = store.add(instantiationService.createInstance(CommandLineSandboxRewriter)); + const result = await rewriter.rewrite({ + ...createRewriteOptions('echo hello'), + requestUnsandboxedExecution: true, + }); + + strictEqual(result, undefined); + deepStrictEqual(calls, []); + }); }); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 43c58ddd915..607bf6a7925 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -6,7 +6,7 @@ import { ok, strictEqual } from 'assert'; import { Separator } from '../../../../../../base/common/actions.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { Emitter } from '../../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { isLinux, isWindows, OperatingSystem } from '../../../../../../base/common/platform.js'; import { count } from '../../../../../../base/common/strings.js'; @@ -31,18 +31,26 @@ import { TestContextService } from '../../../../../test/common/workbenchTestServ import { TestIPCFileSystemProvider } from '../../../../../test/electron-browser/workbenchTestServices.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService/chatService.js'; +import { IChatWidgetService } from '../../../../chat/browser/chat.js'; +import { ChatPermissionLevel } from '../../../../chat/common/constants.js'; import { LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'; -import { ILanguageModelToolsService, IPreparedToolInvocation, IToolInvocationPreparationContext, type ToolConfirmationAction } from '../../../../chat/common/tools/languageModelToolsService.js'; +import { ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocationPreparationContext, ToolDataSource, ToolSet, type ToolConfirmationAction } from '../../../../chat/common/tools/languageModelToolsService.js'; import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js'; -import { RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser/tools/runInTerminalTool.js'; +import { createRunInTerminalToolData, RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser/tools/runInTerminalTool.js'; import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js'; import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; import { TerminalChatService } from '../../../chat/browser/terminalChatService.js'; import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IAgentSessionsService } from '../../../../chat/browser/agentSessions/agentSessionsService.js'; import { IAgentSession } from '../../../../chat/browser/agentSessions/agentSessionsModel.js'; +import { isDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ITrustedDomainService } from '../../../../url/common/trustedDomainService.js'; +import { ChatAgentToolsContribution } from '../../browser/terminal.chatAgentTools.contribution.js'; +import { TerminalToolId } from '../../browser/tools/toolIds.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; class TestRunInTerminalTool extends RunInTerminalTool { protected override _osBackend: Promise = Promise.resolve(OperatingSystem.Windows); @@ -67,6 +75,8 @@ suite('RunInTerminalTool', () => { let terminalServiceDisposeEmitter: Emitter; let chatServiceDisposeEmitter: Emitter<{ sessionResource: URI[]; reason: 'cleared' }>; let chatSessionArchivedEmitter: Emitter; + let sandboxEnabled: boolean; + let terminalSandboxService: ITerminalSandboxService; let runInTerminalTool: TestRunInTerminalTool; @@ -81,6 +91,7 @@ suite('RunInTerminalTool', () => { setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, true); setConfig(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, 'outsideWorkspace'); + sandboxEnabled = false; terminalServiceDisposeEmitter = new Emitter(); chatServiceDisposeEmitter = new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>(); chatSessionArchivedEmitter = new Emitter(); @@ -105,14 +116,17 @@ suite('RunInTerminalTool', () => { instantiationService.stub(IHistoryService, { getLastActiveWorkspaceRoot: () => undefined }); - instantiationService.stub(ITerminalSandboxService, { + terminalSandboxService = { _serviceBrand: undefined, - isEnabled: async () => false, - wrapCommand: command => command, - getSandboxConfigPath: async () => undefined, + isEnabled: async () => sandboxEnabled, + wrapCommand: (command: string) => `sandbox:${command}`, + getSandboxConfigPath: async () => sandboxEnabled ? '/tmp/sandbox.json' : undefined, getTempDir: () => undefined, - setNeedsForceUpdateConfigFile: () => { } - }); + setNeedsForceUpdateConfigFile: () => { }, + getOS: async () => OperatingSystem.Linux, + getResolvedNetworkDomains: () => ({ allowedDomains: [], deniedDomains: [] }), + }; + instantiationService.stub(ITerminalSandboxService, terminalSandboxService); const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService)); treeSitterLibraryService.isTest = true; @@ -198,6 +212,92 @@ suite('RunInTerminalTool', () => { } } + suite('sandbox invocation messaging', () => { + test('should instruct models to use $TMPDIR instead of /tmp when sandboxed', async () => { + sandboxEnabled = true; + + const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData); + + ok(toolData.modelDescription?.includes('must utilize the $TMPDIR environment variable'), 'Expected sandboxed tool description to require $TMPDIR usage'); + ok(toolData.modelDescription?.includes('The /tmp directory is not guaranteed to be accessible or writable and must be avoided'), 'Expected sandboxed tool description to discourage /tmp usage'); + }); + + test('should include requestUnsandboxedExecution in schema when sandbox is enabled', async () => { + sandboxEnabled = true; + + const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData); + const properties = toolData.inputSchema?.properties as Record | undefined; + + ok(properties?.['requestUnsandboxedExecution'], 'Expected requestUnsandboxedExecution in schema when sandbox is enabled'); + ok(properties?.['requestUnsandboxedExecutionReason'], 'Expected requestUnsandboxedExecutionReason in schema when sandbox is enabled'); + }); + + test('should not include requestUnsandboxedExecution in schema when sandbox is disabled', async () => { + sandboxEnabled = false; + + const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData); + const properties = toolData.inputSchema?.properties as Record | undefined; + + ok(!properties?.['requestUnsandboxedExecution'], 'Expected no requestUnsandboxedExecution in schema when sandbox is disabled'); + ok(!properties?.['requestUnsandboxedExecutionReason'], 'Expected no requestUnsandboxedExecutionReason in schema when sandbox is disabled'); + }); + + test('should reflect sandbox setting changes in tool data', async () => { + sandboxEnabled = false; + + const toolDataBefore = await instantiationService.invokeFunction(createRunInTerminalToolData); + const propertiesBefore = toolDataBefore.inputSchema?.properties as Record | undefined; + ok(!propertiesBefore?.['requestUnsandboxedExecution'], 'Expected no requestUnsandboxedExecution before enabling sandbox'); + + sandboxEnabled = true; + + const toolDataAfter = await instantiationService.invokeFunction(createRunInTerminalToolData); + const propertiesAfter = toolDataAfter.inputSchema?.properties as Record | undefined; + ok(propertiesAfter?.['requestUnsandboxedExecution'], 'Expected requestUnsandboxedExecution after enabling sandbox'); + ok(toolDataAfter.modelDescription?.includes('Sandboxing:'), 'Expected sandbox instructions in description after enabling sandbox'); + }); + + test('should include allowed and denied network domains in model description', async () => { + sandboxEnabled = true; + terminalSandboxService.getResolvedNetworkDomains = () => ({ + allowedDomains: ['github.com', 'npmjs.org'], + deniedDomains: ['evil.com'], + }); + + const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData); + + ok(toolData.modelDescription?.includes('github.com, npmjs.org'), 'Expected allowed domains in description'); + ok(toolData.modelDescription?.includes('evil.com'), 'Expected denied domains in description'); + }); + + test('should exclude denied domains from effective allowed list', async () => { + sandboxEnabled = true; + terminalSandboxService.getResolvedNetworkDomains = () => ({ + allowedDomains: ['github.com', 'evil.com', 'npmjs.org'], + deniedDomains: ['evil.com'], + }); + + const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData); + + ok(toolData.modelDescription?.includes('github.com, npmjs.org'), 'Expected effective allowed list without denied domain'); + ok(!toolData.modelDescription?.includes('accessible in the sandbox (all other network access is blocked): github.com, evil.com'), 'Expected denied domain removed from allowed list'); + }); + + test('should use sandbox labels when command is sandbox wrapped', async () => { + terminalSandboxService.isEnabled = async () => true; + terminalSandboxService.getSandboxConfigPath = async () => '/tmp/vscode-sandbox-settings.json'; + terminalSandboxService.wrapCommand = (command: string) => `sandbox-runtime ${command}`; + + const preparedInvocation = await executeToolTest({ command: 'echo hello' }); + + ok(preparedInvocation, 'Expected prepared invocation to be defined'); + strictEqual((preparedInvocation.invocationMessage as IMarkdownString).value, 'Running `echo hello` in sandbox'); + + const terminalData = preparedInvocation.toolSpecificData as IChatTerminalToolInvocationData; + strictEqual(terminalData.commandLine.isSandboxWrapped, true); + }); + }); + suite('default auto-approve rules', () => { const defaults = terminalChatAgentToolsConfiguration[TerminalChatAgentToolsSettingId.AutoApprove].default as Record; @@ -435,6 +535,43 @@ suite('RunInTerminalTool', () => { }); }); + suite('sandbox bypass requests', () => { + test('should force confirmation for explicit unsandboxed execution requests', async () => { + sandboxEnabled = true; + runInTerminalTool.setBackendOs(OperatingSystem.Linux); + + const result = await executeToolTest({ + requestUnsandboxedExecution: true, + requestUnsandboxedExecutionReason: 'Needs network access outside the sandbox', + }); + + assertConfirmationRequired(result, 'Run `bash` command outside the sandbox?'); + strictEqual(result?.confirmationMessages?.allowAutoConfirm, undefined); + const terminalData = result?.toolSpecificData as IChatTerminalToolInvocationData; + strictEqual(terminalData.requestUnsandboxedExecution, true); + strictEqual(terminalData.requestUnsandboxedExecutionReason, 'Needs network access outside the sandbox'); + strictEqual(terminalData.commandLine.toolEdited, undefined); + + const confirmationMessage = result?.confirmationMessages?.message; + ok(confirmationMessage && typeof confirmationMessage !== 'string'); + if (!confirmationMessage || typeof confirmationMessage === 'string') { + throw new Error('Expected markdown confirmation message'); + } + ok(confirmationMessage.value.includes('Reason for leaving the sandbox: Needs network access outside the sandbox')); + + strictEqual(result?.confirmationMessages?.disclaimer, undefined); + const actions = result?.confirmationMessages?.terminalCustomActions; + ok(actions, 'Expected custom actions to be defined'); + strictEqual(actions.length, 11); + ok(!isSeparator(actions[0])); + strictEqual(actions[0].label, 'Allow `echo …` in this Session'); + ok(!isSeparator(actions[4])); + strictEqual(actions[4].label, 'Allow Exact Command Line in this Session'); + ok(!isSeparator(actions[10])); + strictEqual(actions[10].label, 'Configure Auto Approve...'); + }); + }); + suite('prepareToolInvocation - auto approval behavior', () => { test('should auto-approve commands in allow list', async () => { @@ -1443,6 +1580,60 @@ suite('RunInTerminalTool', () => { ok(terminalData.autoApproveInfo, 'Expected autoApproveInfo to be defined'); ok(terminalData.autoApproveInfo.value.includes('Auto approved for this session'), 'Expected session approval message'); }); + + test('should bypass terminal auto-approve feature in Autopilot mode', async () => { + setAutoApprove({ + curl: false + }); + + const sessionResource = LocalChatSessionUri.forSession('autopilot-session'); + instantiationService.stub(IChatWidgetService, { + getWidgetBySessionResource: (() => ({ input: { currentModeInfo: { permissionLevel: ChatPermissionLevel.Autopilot } } })) as unknown as IChatWidgetService['getWidgetBySessionResource'], + lastFocusedWidget: undefined, + }); + + const autopilotRunInTerminalTool = store.add(instantiationService.createInstance(TestRunInTerminalTool)); + const result = await autopilotRunInTerminalTool.prepareToolInvocation({ + parameters: { + command: 'curl https://example.com', + explanation: 'Fetch a URL', + goal: 'Download content', + isBackground: false, + } as IRunInTerminalInputParams, + chatSessionResource: sessionResource, + } as IToolInvocationPreparationContext, CancellationToken.None); + + assertAutoApproved(result); + const terminalData = result!.toolSpecificData as IChatTerminalToolInvocationData; + strictEqual(terminalData.autoApproveInfo, undefined, 'Expected no terminal auto-approve info in Autopilot mode'); + }); + + test('should bypass terminal auto-approve feature in Bypass Approvals mode', async () => { + setAutoApprove({ + curl: false + }); + + const sessionResource = LocalChatSessionUri.forSession('bypass-session'); + instantiationService.stub(IChatWidgetService, { + getWidgetBySessionResource: (() => ({ input: { currentModeInfo: { permissionLevel: ChatPermissionLevel.AutoApprove } } })) as unknown as IChatWidgetService['getWidgetBySessionResource'], + lastFocusedWidget: undefined, + }); + + const bypassRunInTerminalTool = store.add(instantiationService.createInstance(TestRunInTerminalTool)); + const result = await bypassRunInTerminalTool.prepareToolInvocation({ + parameters: { + command: 'curl https://example.com', + explanation: 'Fetch a URL', + goal: 'Download content', + isBackground: false, + } as IRunInTerminalInputParams, + chatSessionResource: sessionResource, + } as IToolInvocationPreparationContext, CancellationToken.None); + + assertAutoApproved(result); + const terminalData = result!.toolSpecificData as IChatTerminalToolInvocationData; + strictEqual(terminalData.autoApproveInfo, undefined, 'Expected no terminal auto-approve info in Bypass Approvals mode'); + }); }); suite('TerminalProfileFetcher', () => { @@ -1564,4 +1755,238 @@ suite('RunInTerminalTool', () => { } }); }); + + suite('ConfirmTerminalCommandTool', () => { + test('should require confirmation when sandbox is enabled but sandbox rewriting is disabled', async () => { + sandboxEnabled = true; + + const { ConfirmTerminalCommandTool } = await import('../../browser/tools/runInTerminalConfirmationTool.js'); + const confirmTool = store.add(instantiationService.createInstance(ConfirmTerminalCommandTool)); + + const context: IToolInvocationPreparationContext = { + parameters: { + command: 'ping google.com', + explanation: 'Ping google.com', + goal: 'Ping google.com', + isBackground: false, + } as IRunInTerminalInputParams + } as IToolInvocationPreparationContext; + + const result = await confirmTool.prepareToolInvocation(context, CancellationToken.None); + assertConfirmationRequired(result); + }); + + test('should require confirmation when sandbox is disabled', async () => { + sandboxEnabled = false; + setAutoApprove({}); + + const { ConfirmTerminalCommandTool } = await import('../../browser/tools/runInTerminalConfirmationTool.js'); + const confirmTool = store.add(instantiationService.createInstance(ConfirmTerminalCommandTool)); + + const context: IToolInvocationPreparationContext = { + parameters: { + command: 'echo hello', + explanation: 'Print hello', + goal: 'Print hello', + isBackground: false, + } as IRunInTerminalInputParams + } as IToolInvocationPreparationContext; + + const result = await confirmTool.prepareToolInvocation(context, CancellationToken.None); + assertConfirmationRequired(result); + }); + }); +}); + +suite('ChatAgentToolsContribution - tool registration refresh', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let registeredToolData: Map; + let trustedDomainsEmitter: Emitter; + let sandboxEnabled: boolean; + + setup(() => { + configurationService = new TestConfigurationService(); + registeredToolData = new Map(); + trustedDomainsEmitter = store.add(new Emitter()); + sandboxEnabled = false; + + const logService = new NullLogService(); + const fileService = store.add(new FileService(logService)); + const fileSystemProvider = new TestIPCFileSystemProvider(); + store.add(fileService.registerProvider(Schemas.file, fileSystemProvider)); + + const terminalServiceDisposeEmitter = store.add(new Emitter()); + const chatServiceDisposeEmitter = store.add(new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>()); + const chatSessionArchivedEmitter = store.add(new Emitter()); + + instantiationService = workbenchInstantiationService({ + configurationService: () => configurationService, + fileService: () => fileService, + }, store); + + instantiationService.stub(IChatService, { + onDidDisposeSession: chatServiceDisposeEmitter.event, + getSession: () => undefined, + }); + instantiationService.stub(IAgentSessionsService, { + onDidChangeSessionArchivedState: chatSessionArchivedEmitter.event, + model: { + onDidChangeSessionArchivedState: chatSessionArchivedEmitter.event, + } as IAgentSessionsService['model'] + }); + instantiationService.stub(ITerminalChatService, store.add(instantiationService.createInstance(TerminalChatService))); + instantiationService.stub(IHistoryService, { + getLastActiveWorkspaceRoot: () => undefined + }); + + const terminalSandboxService: ITerminalSandboxService = { + _serviceBrand: undefined, + isEnabled: async () => sandboxEnabled, + wrapCommand: (command: string) => `sandbox:${command}`, + getSandboxConfigPath: async () => sandboxEnabled ? '/tmp/sandbox.json' : undefined, + getTempDir: () => undefined, + setNeedsForceUpdateConfigFile: () => { }, + getOS: async () => OperatingSystem.Linux, + getResolvedNetworkDomains: () => ({ allowedDomains: [], deniedDomains: [] }), + }; + instantiationService.stub(ITerminalSandboxService, terminalSandboxService); + + const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService)); + treeSitterLibraryService.isTest = true; + instantiationService.stub(ITreeSitterLibraryService, treeSitterLibraryService); + + instantiationService.stub(ITerminalService, { + onDidDisposeInstance: terminalServiceDisposeEmitter.event, + setNextCommandId: async () => { } + }); + instantiationService.stub(ITerminalProfileResolverService, { + getDefaultProfile: async () => ({ path: 'bash' } as ITerminalProfile) + }); + + instantiationService.stub(ITrustedDomainService, { + _serviceBrand: undefined, + onDidChangeTrustedDomains: trustedDomainsEmitter.event, + isValid: () => true, + trustedDomains: [], + }); + + const contextKeyService = instantiationService.get(IContextKeyService); + const registeredToolImpls = new Map(); + const mockToolsService: Partial = { + _serviceBrand: undefined, + onDidChangeTools: Event.None, + registerToolData(toolData: IToolData) { + registeredToolData.set(toolData.id, toolData); + return toDisposable(() => registeredToolData.delete(toolData.id)); + }, + registerToolImplementation(id: string, tool: IToolImpl) { + registeredToolImpls.set(id, tool); + return toDisposable(() => registeredToolImpls.delete(id)); + }, + registerTool(toolData: IToolData, tool: IToolImpl) { + registeredToolData.set(toolData.id, toolData); + registeredToolImpls.set(toolData.id, tool); + return toDisposable(() => { + registeredToolData.delete(toolData.id); + registeredToolImpls.delete(toolData.id); + if (isDisposable(tool)) { + tool.dispose(); + } + }); + }, + getTools() { + return registeredToolData.values(); + }, + executeToolSet: new ToolSet('execute', 'execute', Codicon.play, ToolDataSource.Internal, undefined, undefined, contextKeyService), + readToolSet: new ToolSet('read', 'read', Codicon.book, ToolDataSource.Internal, undefined, undefined, contextKeyService), + }; + instantiationService.stub(ILanguageModelToolsService, mockToolsService as ILanguageModelToolsService); + }); + + async function flushAsync(): Promise { + // Multiple microtask cycles to let async _registerRunInTerminalTool complete + for (let i = 0; i < 10; i++) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + + async function createContribution(): Promise { + const contribution = store.add(instantiationService.createInstance(ChatAgentToolsContribution)); + await flushAsync(); + return contribution; + } + + test('should register run_in_terminal tool on construction', async () => { + await createContribution(); + ok(registeredToolData.has(TerminalToolId.RunInTerminal), 'Expected run_in_terminal tool to be registered'); + }); + + test('should refresh run_in_terminal tool data when sandbox setting changes', async () => { + await createContribution(); + + const toolDataBefore = registeredToolData.get(TerminalToolId.RunInTerminal); + ok(toolDataBefore, 'Expected run_in_terminal tool to be registered'); + const propertiesBefore = toolDataBefore.inputSchema?.properties as Record | undefined; + ok(!propertiesBefore?.['requestUnsandboxedExecution'], 'Expected no requestUnsandboxedExecution before enabling sandbox'); + + // Enable sandbox and fire config change + sandboxEnabled = true; + configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled, true); + configurationService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: (key: string) => key === TerminalChatAgentToolsSettingId.TerminalSandboxEnabled, + affectedKeys: new Set([TerminalChatAgentToolsSettingId.TerminalSandboxEnabled]), + source: ConfigurationTarget.USER, + change: null!, + }); + + // Wait for async registration + await flushAsync(); + + const toolDataAfter = registeredToolData.get(TerminalToolId.RunInTerminal); + ok(toolDataAfter, 'Expected run_in_terminal tool to still be registered'); + const propertiesAfter = toolDataAfter.inputSchema?.properties as Record | undefined; + ok(propertiesAfter?.['requestUnsandboxedExecution'], 'Expected requestUnsandboxedExecution after enabling sandbox'); + }); + + test('should refresh run_in_terminal tool data when trusted domains change', async () => { + await createContribution(); + + const toolDataBefore = registeredToolData.get(TerminalToolId.RunInTerminal); + ok(toolDataBefore, 'Expected run_in_terminal tool to be registered'); + + // Fire trusted domains change + trustedDomainsEmitter.fire(); + + // Wait for async registration + await flushAsync(); + + // Tool should still be registered (re-registered with fresh data) + const toolDataAfter = registeredToolData.get(TerminalToolId.RunInTerminal); + ok(toolDataAfter, 'Expected run_in_terminal tool to still be registered after trusted domains change'); + }); + + test('should refresh run_in_terminal tool data when sandbox network setting changes', async () => { + sandboxEnabled = true; + await createContribution(); + + const toolDataBefore = registeredToolData.get(TerminalToolId.RunInTerminal); + ok(toolDataBefore, 'Expected run_in_terminal tool to be registered'); + + // Fire network config change + configurationService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: (key: string) => key === TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains, + affectedKeys: new Set([TerminalChatAgentToolsSettingId.TerminalSandboxNetworkAllowedDomains]), + source: ConfigurationTarget.USER, + change: null!, + }); + + // Wait for async registration + await flushAsync(); + + const toolDataAfter = registeredToolData.get(TerminalToolId.RunInTerminal); + ok(toolDataAfter, 'Expected run_in_terminal tool to still be registered after network setting change'); + }); }); diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts index 285c2c02660..fd3df87da93 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts @@ -114,6 +114,7 @@ export class TerminalFindWidget extends SimpleFindWidget { // Disable copy-on-selection during search to prevent search result from overriding clipboard this._register(xterm.onBeforeSearch(() => { + this._overrideCopyOnSelectionDisposable.clear(); this._overrideCopyOnSelectionDisposable.value = TerminalClipboardContribution.get(this._instance)?.overrideCopyOnSelection(false); })); @@ -182,9 +183,8 @@ export class TerminalFindWidget extends SimpleFindWidget { } protected _onFocusTrackerFocus() { - if (TerminalClipboardContribution.get(this._instance)?.overrideCopyOnSelection) { - this._overrideCopyOnSelectionDisposable.value = TerminalClipboardContribution.get(this._instance)?.overrideCopyOnSelection(false); - } + this._overrideCopyOnSelectionDisposable.clear(); + this._overrideCopyOnSelectionDisposable.value = TerminalClipboardContribution.get(this._instance)?.overrideCopyOnSelection(false); this._findWidgetFocused.set(true); } diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 05446ea9f07..fb1496acdff 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -209,7 +209,7 @@ export class TerminalLinkManager extends DisposableStore { this._telemetryService.publicLog2<{ linkType: TerminalBuiltinLinkType | string; }, { - owner: 'tyriar'; + owner: 'anthonykim1'; comment: 'When the user opens a link in the terminal'; linkType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of link being opened' }; }>('terminal/openLink', { linkType: isString(link.type) ? link.type : `extension:${link.type.id}` }); diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts index b7d264ee0c8..b5f0421b343 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts @@ -1428,7 +1428,7 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { private _sendLatencyStats(stats: PredictionStats) { /* __GDPR__ "terminalLatencyStats" : { - "owner": "Tyriar", + "owner": "anthonykim1", "min" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "max" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "median" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index e19fc273fd5..3dbd89389eb 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -39,6 +39,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { ILabelService } from '../../../../platform/label/common/label.js'; import { WorkbenchCompressibleObjectTree } from '../../../../platform/list/browser/listService.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IResourceLabel, ResourceLabels } from '../../../browser/labels.js'; @@ -78,13 +79,23 @@ export class TestCoverageView extends ViewPane { @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, @ITestCoverageService private readonly coverageService: ITestCoverageService, + @IStorageService private readonly storageService: IStorageService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); + const storedOrder = this.storageService.getNumber('testing.coverageSortOrder', StorageScope.WORKSPACE); + if (storedOrder !== undefined && storedOrder >= CoverageSortOrder.Coverage && storedOrder <= CoverageSortOrder.Name) { + this.sortOrder.set(storedOrder, undefined); + } } protected override renderBody(container: HTMLElement): void { super.renderBody(container); + this._register(autorun(reader => { + const order = this.sortOrder.read(reader); + this.storageService.store('testing.coverageSortOrder', order, StorageScope.WORKSPACE, StorageTarget.MACHINE); + })); + const labels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility })); this._register(autorun(reader => { diff --git a/src/vs/workbench/contrib/testing/common/testingChatAgentTool.ts b/src/vs/workbench/contrib/testing/common/testingChatAgentTool.ts index 94cb8a0f182..cf9ae07107f 100644 --- a/src/vs/workbench/contrib/testing/common/testingChatAgentTool.ts +++ b/src/vs/workbench/contrib/testing/common/testingChatAgentTool.ts @@ -31,14 +31,14 @@ import { ToolProgress, } from '../../chat/common/tools/languageModelToolsService.js'; import { TestId } from './testId.js'; -import { FileCoverage, getTotalCoveragePercent } from './testCoverage.js'; +import { FileCoverage, TestCoverage, getTotalCoveragePercent } from './testCoverage.js'; import { TestingContextKeys } from './testingContextKeys.js'; import { collectTestStateCounts, getTestProgressText } from './testingProgressMessages.js'; import { isFailedState } from './testingStates.js'; import { LiveTestResult } from './testResult.js'; import { ITestResultService } from './testResultService.js'; import { ITestService, testsInFile, waitForTestToBeIdle } from './testService.js'; -import { IncrementalTestCollectionItem, TestItemExpandState, TestMessageType, TestResultState, TestRunProfileBitset } from './testTypes.js'; +import { DetailType, IncrementalTestCollectionItem, TestItemExpandState, TestMessageType, TestResultState, TestRunProfileBitset } from './testTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITestProfileService } from './testProfileService.js'; @@ -70,7 +70,7 @@ interface IRunTestToolParams { mode?: Mode; } -class RunTestTool implements IToolImpl { +export class RunTestTool implements IToolImpl { public static readonly ID = 'runTests'; public static readonly DEFINITION: IToolData = { id: this.ID, @@ -101,7 +101,7 @@ class RunTestTool implements IToolImpl { coverageFiles: { type: 'array', items: { type: 'string' }, - description: 'When mode="coverage": absolute file paths to include detailed coverage info for. Only the first matching file will be summarized.' + description: 'When mode="coverage": absolute file paths to include detailed coverage info for. If not provided, a file-level summary of all files with incomplete coverage is shown.' } }, }, @@ -168,7 +168,7 @@ class RunTestTool implements IToolImpl { }; } - const summary = await this._buildSummary(result, mode, coverageFiles); + const summary = await buildTestRunSummary(result, mode, coverageFiles); const content = [{ kind: 'text', value: summary } as const]; return { @@ -177,132 +177,6 @@ class RunTestTool implements IToolImpl { }; } - private async _buildSummary(result: LiveTestResult, mode: Mode, coverageFiles: string[] | undefined): Promise { - const failures = result.counts[TestResultState.Errored] + result.counts[TestResultState.Failed]; - let str = `\n`; - if (failures !== 0) { - str += await this._getFailureDetails(result); - } - if (mode === 'coverage') { - str += await this._getCoverageSummary(result, coverageFiles); - } - return str; - } - - private async _getCoverageSummary(result: LiveTestResult, coverageFiles: string[] | undefined): Promise { - if (!coverageFiles || !coverageFiles.length) { - return ''; - } - for (const task of result.tasks) { - const coverage = task.coverage.get(); - if (!coverage) { - continue; - } - const normalized = coverageFiles.map(file => URI.file(file).fsPath); - const coveredFilesMap = new Map(); - for (const file of coverage.getAllFiles().values()) { - coveredFilesMap.set(file.uri.fsPath, file); - } - for (const path of normalized) { - const file = coveredFilesMap.get(path); - if (!file) { - continue; - } - let summary = `\n`; - const pct = getTotalCoveragePercent(file.statement, file.branch, file.declaration) * 100; - summary += `\n`; - summary += `\n`; - return summary; - } - } - return ''; - } - - private async _getFailureDetails(result: LiveTestResult): Promise { - let str = ''; - let hadMessages = false; - for (const failure of result.tests) { - if (!isFailedState(failure.ownComputedState)) { - continue; - } - - const [, ...testPath] = TestId.split(failure.item.extId); - const testName = testPath.pop(); - str += ` '))}>\n`; - // Extract detailed failure information from error messages - for (const task of failure.tasks) { - for (const message of task.messages.filter(m => m.type === TestMessageType.Error)) { - hadMessages = true; - - // Add expected/actual outputs if available - if (message.expected !== undefined && message.actual !== undefined) { - str += `\n${message.expected}\n\n`; - str += `\n${message.actual}\n\n`; - } else { - // Fallback to the message content - const messageText = typeof message.message === 'string' ? message.message : message.message.value; - str += `\n${messageText}\n\n`; - } - - // Add stack trace information if available (limit to first 10 frames) - if (message.stackTrace && message.stackTrace.length > 0) { - for (const frame of message.stackTrace.slice(0, 10)) { - if (frame.uri && frame.position) { - str += `\n`; - } else if (frame.uri) { - str += `${frame.label}\n`; - } else { - str += `${frame.label}\n`; - } - } - } - - // Add location information if available - if (message.location) { - str += `\n`; - } - } - } - - str += `\n`; - } - - if (!hadMessages) { // some adapters don't have any per-test messages and just output - const output = result.tasks.map(t => t.output.getRange(0, t.output.length).toString().trim()).join('\n'); - if (output) { - str += `\n${output}\n\n`; - } - } - - return str; - } - /** Updates the UI progress as the test runs, resolving when the run is finished. */ private async _monitorRunProgress(result: LiveTestResult, progress: ToolProgress, token: CancellationToken): Promise { const store = new DisposableStore(); @@ -451,3 +325,202 @@ class RunTestTool implements IToolImpl { }); } } + +/** Builds the full summary string for a completed test run. */ +export async function buildTestRunSummary(result: LiveTestResult, mode: Mode, coverageFiles: string[] | undefined): Promise { + const failures = result.counts[TestResultState.Errored] + result.counts[TestResultState.Failed]; + let str = `\n`; + if (failures !== 0) { + str += await getFailureDetails(result); + } + if (mode === 'coverage') { + str += await getCoverageSummary(result, coverageFiles); + } + return str; +} + +/** Gets a coverage summary from a test result, either overall or per-file. */ +export async function getCoverageSummary(result: LiveTestResult, coverageFiles: string[] | undefined): Promise { + let str = ''; + for (const task of result.tasks) { + const coverage = task.coverage.get(); + if (!coverage) { + continue; + } + + if (!coverageFiles || !coverageFiles.length) { + str += getOverallCoverageSummary(coverage); + continue; + } + + const normalized = coverageFiles.map(file => URI.file(file).fsPath); + const coveredFilesMap = new Map(); + for (const file of coverage.getAllFiles().values()) { + coveredFilesMap.set(file.uri.fsPath, file); + } + + for (const path of normalized) { + const file = coveredFilesMap.get(path); + if (!file) { + continue; + } + str += await getFileCoverageDetails(file, path); + } + } + return str; +} + +/** Gets a file-level coverage overview sorted by lowest coverage first. */ +export function getOverallCoverageSummary(coverage: TestCoverage): string { + const files = [...coverage.getAllFiles().values()] + .map(f => ({ path: f.uri.fsPath, pct: getTotalCoveragePercent(f.statement, f.branch, f.declaration) * 100 })) + .filter(f => f.pct < 100) + .sort((a, b) => a.pct - b.pct); + + if (!files.length) { + return 'All files have 100% coverage.\n'; + } + + let str = '\n'; + for (const f of files) { + str += `\n`; + } + str += '\n'; + return str; +} + +/** Gets detailed coverage information for a single file including uncovered items. */ +export async function getFileCoverageDetails(file: FileCoverage, path: string): Promise { + const pct = getTotalCoveragePercent(file.statement, file.branch, file.declaration) * 100; + let str = ` `${d.name}(L${d.line})`).join(', ') + '\n'; + } + if (uncoveredBranches.length) { + str += 'uncovered branches: ' + uncoveredBranches.map(b => b.label ? `L${b.line}(${b.label})` : `L${b.line}`).join(', ') + '\n'; + } + if (uncoveredLines.length) { + str += 'uncovered lines: ' + mergeLineRanges(uncoveredLines) + '\n'; + } + } catch { /* ignore - details not available */ } + + str += '\n'; + return str; +} + +/** Merges overlapping/contiguous line ranges and formats them compactly. */ +export function mergeLineRanges(ranges: [number, number][]): string { + if (!ranges.length) { + return ''; + } + ranges.sort((a, b) => a[0] - b[0]); + const merged: [number, number][] = [ranges[0]]; + for (let i = 1; i < ranges.length; i++) { + const last = merged[merged.length - 1]; + const [start, end] = ranges[i]; + if (start <= last[1] + 1) { + last[1] = Math.max(last[1], end); + } else { + merged.push([start, end]); + } + } + return merged.map(([s, e]) => s === e ? `${s}` : `${s}-${e}`).join(', '); +} + +/** Formats failure details from a test result into an XML-like string. */ +export async function getFailureDetails(result: LiveTestResult): Promise { + let str = ''; + let hadMessages = false; + for (const failure of result.tests) { + if (!isFailedState(failure.ownComputedState)) { + continue; + } + + const [, ...testPath] = TestId.split(failure.item.extId); + const testName = testPath.pop(); + str += ` '))}>\n`; + for (const task of failure.tasks) { + for (const message of task.messages.filter(m => m.type === TestMessageType.Error)) { + hadMessages = true; + + if (message.expected !== undefined && message.actual !== undefined) { + str += `\n${message.expected}\n\n`; + str += `\n${message.actual}\n\n`; + } else { + const messageText = typeof message.message === 'string' ? message.message : message.message.value; + str += `\n${messageText}\n\n`; + } + + if (message.stackTrace && message.stackTrace.length > 0) { + for (const frame of message.stackTrace.slice(0, 10)) { + if (frame.uri && frame.position) { + str += `\n`; + } else if (frame.uri) { + str += `${frame.label}\n`; + } else { + str += `${frame.label}\n`; + } + } + } + + if (message.location) { + str += `\n`; + } + } + } + + str += `\n`; + } + + if (!hadMessages) { + const output = result.tasks.map(t => t.output.getRange(0, t.output.length).toString().trim()).join('\n'); + if (output) { + str += `\n${output}\n\n`; + } + } + + return str; +} diff --git a/src/vs/workbench/contrib/testing/test/common/testingChatAgentTool.test.ts b/src/vs/workbench/contrib/testing/test/common/testingChatAgentTool.test.ts new file mode 100644 index 00000000000..a6ede42a998 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/testingChatAgentTool.test.ts @@ -0,0 +1,721 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../base/common/event.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { upcastPartial } from '../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { Position } from '../../../../../editor/common/core/position.js'; +import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IExtUri } from '../../../../../base/common/resources.js'; +import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; +import { IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep } from '../../../chat/common/tools/languageModelToolsService.js'; +import { FileCoverage, ICoverageAccessor, TestCoverage } from '../../common/testCoverage.js'; +import { LiveTestResult } from '../../common/testResult.js'; +import { ITestResultService } from '../../common/testResultService.js'; +import { IMainThreadTestCollection, ITestService } from '../../common/testService.js'; +import { CoverageDetails, DetailType, IBranchCoverage, IDeclarationCoverage, IFileCoverage, IStatementCoverage, ResolvedTestRunRequest, TestMessageType, TestResultState, TestRunProfileBitset } from '../../common/testTypes.js'; +import { ITestProfileService } from '../../common/testProfileService.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { TestId } from '../../common/testId.js'; +import { RunTestTool, buildTestRunSummary, getCoverageSummary, getOverallCoverageSummary, getFileCoverageDetails, mergeLineRanges, getFailureDetails } from '../../common/testingChatAgentTool.js'; + +suite('Workbench - RunTestTool', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + + let insertCounter = 0; + let tool: RunTestTool; + + const noopProgress = { + report: (_update: IToolProgressStep) => { }, + }; + const noopCountTokens = () => Promise.resolve(0); + + function createLiveTestResult(request?: ResolvedTestRunRequest): LiveTestResult { + const req = request ?? { + group: TestRunProfileBitset.Run, + targets: [{ profileId: 0, controllerId: 'ctrlId', testIds: ['id-a'] }], + }; + return ds.add(new LiveTestResult( + `result-${insertCounter++}`, + false, + req, + insertCounter, + NullTelemetryService, + )); + } + + function createTestCoverage(files: { uri: URI; statement: { covered: number; total: number }; branch?: { covered: number; total: number }; declaration?: { covered: number; total: number }; details?: CoverageDetails[] }[]): TestCoverage { + const result = createLiveTestResult(); + const accessor: ICoverageAccessor = { + getCoverageDetails: (id, _testId, _token) => { + const entry = files.find(f => f.uri.toString() === id); + return Promise.resolve(entry?.details ?? []); + }, + }; + const uriIdentity = upcastPartial({ + asCanonicalUri: (uri: URI) => uri, + extUri: upcastPartial({ + isEqual: (a: URI, b: URI) => a.toString() === b.toString(), + ignorePathCasing: () => false, + }), + }); + const coverage = new TestCoverage(result, 'task-1', uriIdentity, accessor); + for (const f of files) { + const fileCoverage: IFileCoverage = { + id: f.uri.toString(), + uri: f.uri, + statement: f.statement, + branch: f.branch, + declaration: f.declaration, + }; + coverage.append(fileCoverage, undefined); + } + return coverage; + } + + function makeStatement(line: number, count: number, endLine?: number, branches?: IBranchCoverage[]): IStatementCoverage { + return { + type: DetailType.Statement, + count, + location: new Range(line, 1, endLine ?? line, 1), + branches, + }; + } + + function makeDeclaration(name: string, line: number, count: number): IDeclarationCoverage { + return { + type: DetailType.Declaration, + name, + count, + location: new Range(line, 1, line, 1), + }; + } + + function makeBranch(line: number, count: number, label?: string): IBranchCoverage { + return { + count, + label, + location: new Range(line, 1, line, 1), + }; + } + + function createResultWithCoverage(coverageData: TestCoverage): LiveTestResult { + const result = createLiveTestResult(); + result.addTask({ id: 'task-1', name: 'Test Task', running: true, ctrlId: 'ctrlId' }); + const taskCov = result.tasks[0].coverage as ReturnType>; + taskCov.set(coverageData, undefined); + return result; + } + + function createResultWithTests(tests: { extId: string; label: string; state: TestResultState; messages?: { type: TestMessageType; message: string; expected?: string; actual?: string; location?: { uri: URI; range: Range }; stackTrace?: { uri?: URI; position?: { lineNumber: number; column: number }; label: string }[] }[] }[]): LiveTestResult { + const result = createLiveTestResult(); + result.addTask({ id: 't', name: 'Test Task', running: true, ctrlId: 'ctrlId' }); + + for (const t of tests) { + const chain = TestId.split(t.extId); + const items = chain.map((segment, i) => ({ + extId: new TestId(chain.slice(0, i + 1)).toString(), + label: i === chain.length - 1 ? t.label : segment, + busy: false, + description: null, + error: null, + range: null, + sortText: null, + tags: [], + uri: undefined, + })); + result.addTestChainToRun('ctrlId', items); + } + + for (const t of tests) { + result.updateState(t.extId, 't', t.state); + if (t.messages) { + for (const msg of t.messages) { + result.appendMessage(t.extId, 't', { + type: msg.type as TestMessageType.Error, + message: msg.message, + expected: msg.expected, + actual: msg.actual, + contextValue: undefined, + location: msg.location ? { uri: msg.location.uri, range: msg.location.range } : undefined, + stackTrace: msg.stackTrace?.map(f => ({ + uri: f.uri, + position: f.position ? new Position(f.position.lineNumber, f.position.column) : undefined, + label: f.label, + })), + }); + } + } + } + + return result; + } + + function createFileCov(uri: URI, statement: { covered: number; total: number }, details: CoverageDetails[], opts?: { branch?: { covered: number; total: number }; declaration?: { covered: number; total: number } }): FileCoverage { + const result = createLiveTestResult(); + const accessor: ICoverageAccessor = { + getCoverageDetails: () => Promise.resolve(details), + }; + return new FileCoverage({ id: 'file-1', uri, statement, branch: opts?.branch, declaration: opts?.declaration }, result, accessor); + } + + setup(() => { + insertCounter = 0; + + const mockTestService = upcastPartial({ + collection: upcastPartial({ + rootItems: [], + rootIds: [], + expand: () => Promise.resolve(), + getNodeById: () => undefined, + getNodeByUrl: () => [], + }), + runTests: () => Promise.resolve(upcastPartial({})), + cancelTestRun: () => { }, + }); + + const mockResultService = upcastPartial({ + onResultsChanged: Event.None, + }); + + const mockProfileService = upcastPartial({ + capabilitiesForTest: () => TestRunProfileBitset.Run | TestRunProfileBitset.Coverage, + }); + + const mockUriIdentity = upcastPartial({ + asCanonicalUri: (uri: URI) => uri, + extUri: upcastPartial({ isEqual: (a: URI, b: URI) => a.toString() === b.toString() }), + }); + + const mockWorkspaceContext = upcastPartial({ + getWorkspace: () => upcastPartial({ id: 'test', folders: [upcastPartial({ uri: URI.file('/workspace') })] }), + }); + + tool = new RunTestTool( + mockTestService, + mockUriIdentity, + mockWorkspaceContext, + mockResultService, + mockProfileService, + ); + }); + + suite('invoke', () => { + test('returns error when no tests found', async () => { + const result = await tool.invoke( + upcastPartial({ parameters: { files: ['/nonexistent/test.ts'] } }), + noopCountTokens, noopProgress, CancellationToken.None, + ); + assert.ok(result.toolResultError); + assert.ok(result.content[0].kind === 'text' && result.content[0].value.includes('No tests found')); + }); + }); + + suite('_buildSummary', () => { + test('includes pass/fail counts', async () => { + const result = createResultWithTests([ + { extId: new TestId(['ctrlId', 'a']).toString(), label: 'a', state: TestResultState.Passed }, + { extId: new TestId(['ctrlId', 'b']).toString(), label: 'b', state: TestResultState.Failed, messages: [{ type: TestMessageType.Error, message: 'boom' }] }, + ]); + result.markComplete(); + + const summary = await buildTestRunSummary(result, 'run', undefined); + assert.ok(summary.includes('')); + }); + + test('combines errored and failed in failure count', async () => { + const result = createResultWithTests([ + { extId: new TestId(['ctrlId', 'a']).toString(), label: 'a', state: TestResultState.Failed, messages: [{ type: TestMessageType.Error, message: 'fail' }] }, + { extId: new TestId(['ctrlId', 'b']).toString(), label: 'b', state: TestResultState.Errored, messages: [{ type: TestMessageType.Error, message: 'error' }] }, + { extId: new TestId(['ctrlId', 'c']).toString(), label: 'c', state: TestResultState.Passed }, + ]); + result.markComplete(); + + const summary = await buildTestRunSummary(result, 'run', undefined); + assert.ok(summary.includes('failed=2')); + }); + + test('includes coverage when mode is coverage', async () => { + const coverageData = createTestCoverage([ + { uri: URI.file('/src/a.ts'), statement: { covered: 8, total: 10 } }, + ]); + const result = createResultWithCoverage(coverageData); + result.markComplete(); + + const summary = await buildTestRunSummary(result, 'coverage', undefined); + assert.ok(summary.includes('')); + }); + + test('omits coverage when mode is run', async () => { + const result = createLiveTestResult(); + result.addTask({ id: 't', name: 'n', running: true, ctrlId: 'ctrl' }); + result.markComplete(); + + const summary = await buildTestRunSummary(result, 'run', undefined); + assert.ok(!summary.includes(' { + test('returns overall summary when no coverageFiles specified', async () => { + const fileA = URI.file('/src/a.ts'); + const fileB = URI.file('/src/b.ts'); + const coverageData = createTestCoverage([ + { uri: fileA, statement: { covered: 5, total: 10 } }, + { uri: fileB, statement: { covered: 10, total: 10 } }, + ]); + const result = createResultWithCoverage(coverageData); + + const summary = await getCoverageSummary(result, undefined); + assert.ok(summary.includes('')); + assert.ok(summary.includes(fileA.fsPath)); + assert.ok(!summary.includes(fileB.fsPath)); // 100% covered, excluded + }); + + test('returns detailed summary for specified coverageFiles', async () => { + const fileA = URI.file('/src/a.ts'); + const details: CoverageDetails[] = [ + makeDeclaration('uncoveredFn', 10, 0), + makeStatement(20, 0, 25), + ]; + const coverageData = createTestCoverage([ + { uri: fileA, statement: { covered: 8, total: 10 }, declaration: { covered: 0, total: 1 }, details }, + ]); + const result = createResultWithCoverage(coverageData); + + const summary = await getCoverageSummary(result, [fileA.fsPath]); + assert.ok(summary.includes(` { + const fileA = URI.file('/src/a.ts'); + const result = createLiveTestResult(); + result.addTask({ id: 't', name: 'n', running: true, ctrlId: 'ctrl' }); + + const summary = await getCoverageSummary(result, [fileA.fsPath]); + assert.strictEqual(summary, ''); + }); + + test('handles multiple coverageFiles', async () => { + const fileA = URI.file('/src/a.ts'); + const fileB = URI.file('/src/b.ts'); + const coverageData = createTestCoverage([ + { uri: fileA, statement: { covered: 8, total: 10 }, details: [makeStatement(5, 0)] }, + { uri: fileB, statement: { covered: 3, total: 10 }, details: [makeDeclaration('fn', 1, 0)] }, + ]); + const result = createResultWithCoverage(coverageData); + + const summary = await getCoverageSummary(result, [fileA.fsPath, fileB.fsPath]); + assert.ok(summary.includes(fileA.fsPath)); + assert.ok(summary.includes(fileB.fsPath)); + }); + + test('skips non-matching coverageFiles gracefully', async () => { + const fileA = URI.file('/src/a.ts'); + const nonExistent = URI.file('/src/nonexistent.ts'); + const coverageData = createTestCoverage([ + { uri: fileA, statement: { covered: 8, total: 10 } }, + ]); + const result = createResultWithCoverage(coverageData); + + const summary = await getCoverageSummary(result, [nonExistent.fsPath]); + assert.strictEqual(summary, ''); + }); + }); + + suite('getOverallCoverageSummary', () => { + test('returns all-covered message when everything is 100%', () => { + const coverage = createTestCoverage([ + { uri: URI.file('/src/a.ts'), statement: { covered: 10, total: 10 } }, + { uri: URI.file('/src/b.ts'), statement: { covered: 5, total: 5 } }, + ]); + assert.strictEqual( + getOverallCoverageSummary(coverage), + 'All files have 100% coverage.\n', + ); + }); + + test('sorts files by coverage ascending', () => { + const high = URI.file('/src/high.ts'); + const low = URI.file('/src/low.ts'); + const mid = URI.file('/src/mid.ts'); + const coverage = createTestCoverage([ + { uri: high, statement: { covered: 9, total: 10 } }, + { uri: low, statement: { covered: 3, total: 10 } }, + { uri: mid, statement: { covered: 7, total: 10 } }, + ]); + const summary = getOverallCoverageSummary(coverage); + const lowIdx = summary.indexOf(low.fsPath); + const midIdx = summary.indexOf(mid.fsPath); + const highIdx = summary.indexOf(high.fsPath); + assert.ok(lowIdx < midIdx && midIdx < highIdx); + }); + + test('excludes 100% files from listing', () => { + const partial = URI.file('/src/partial.ts'); + const full = URI.file('/src/full.ts'); + const coverage = createTestCoverage([ + { uri: partial, statement: { covered: 5, total: 10 } }, + { uri: full, statement: { covered: 10, total: 10 } }, + ]); + const summary = getOverallCoverageSummary(coverage); + assert.ok(summary.includes(partial.fsPath)); + assert.ok(!summary.includes(full.fsPath)); + }); + + test('includes percentage in output', () => { + const coverage = createTestCoverage([ + { uri: URI.file('/src/a.ts'), statement: { covered: 7, total: 10 } }, + ]); + const summary = getOverallCoverageSummary(coverage); + assert.ok(summary.includes('percent=70.0')); + }); + }); + + suite('getFileCoverageDetails', () => { + test('shows header with statement counts', async () => { + const uri = URI.file('/src/foo.ts'); + const file = createFileCov(uri, { covered: 8, total: 10 }, []); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('statements=8/10')); + assert.ok(output.includes('percent=80.0')); + assert.ok(output.startsWith(`\n')); + }); + + test('includes branch counts when available', async () => { + const uri = URI.file('/src/foo.ts'); + const file = createFileCov(uri, { covered: 8, total: 10 }, [], { branch: { covered: 3, total: 5 } }); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('branches=3/5')); + }); + + test('includes declaration counts when available', async () => { + const uri = URI.file('/src/foo.ts'); + const file = createFileCov(uri, { covered: 8, total: 10 }, [], { declaration: { covered: 2, total: 4 } }); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('declarations=2/4')); + }); + + test('omits branch/declaration when not available', async () => { + const uri = URI.file('/src/foo.ts'); + const file = createFileCov(uri, { covered: 8, total: 10 }, []); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(!output.includes('branches=')); + assert.ok(!output.includes('declarations=')); + }); + + test('lists uncovered declarations', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeDeclaration('handleError', 89, 0), + makeDeclaration('processQueue', 120, 0), + makeDeclaration('coveredFn', 50, 3), + ]; + const file = createFileCov(uri, { covered: 8, total: 10 }, details, { declaration: { covered: 1, total: 3 } }); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('uncovered functions: handleError(L89), processQueue(L120)')); + assert.ok(!output.includes('coveredFn')); + }); + + test('lists uncovered branches with labels', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeStatement(34, 5, undefined, [ + makeBranch(34, 5, 'then'), + makeBranch(36, 0, 'else'), + ]), + makeStatement(56, 2, undefined, [ + makeBranch(56, 0, 'case "foo"'), + makeBranch(58, 2, 'case "bar"'), + ]), + ]; + const file = createFileCov(uri, { covered: 8, total: 10 }, details, { branch: { covered: 2, total: 4 } }); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('uncovered branches: L36(else), L56(case "foo")')); + }); + + test('lists uncovered branches without labels', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeStatement(10, 1, undefined, [makeBranch(10, 0)]), + ]; + const file = createFileCov(uri, { covered: 8, total: 10 }, details); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('uncovered branches: L10\n')); + }); + + test('uses parent statement location when branch has no location', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeStatement(42, 1, undefined, [{ count: 0, label: 'else' }]), + ]; + const file = createFileCov(uri, { covered: 8, total: 10 }, details); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('L42(else)')); + }); + + test('lists merged uncovered line ranges', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeStatement(23, 0, 27), + makeStatement(28, 0, 30), + makeStatement(45, 0), + makeStatement(67, 0, 72), + makeStatement(100, 0, 105), + makeStatement(50, 5), // covered + ]; + const file = createFileCov(uri, { covered: 5, total: 11 }, details); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes('uncovered lines: 23-30, 45, 67-72, 100-105')); + }); + + test('omits uncovered sections when all covered', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeDeclaration('fn', 10, 3), + makeStatement(20, 5), + makeStatement(30, 1, undefined, [makeBranch(30, 1, 'then'), makeBranch(32, 2, 'else')]), + ]; + const file = createFileCov(uri, { covered: 10, total: 10 }, details); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(!output.includes('uncovered')); + }); + + test('handles details() throwing gracefully', async () => { + const uri = URI.file('/src/err.ts'); + const result = createLiveTestResult(); + const accessor: ICoverageAccessor = { + getCoverageDetails: () => Promise.reject(new Error('not available')), + }; + const file = new FileCoverage({ id: 'err', uri, statement: { covered: 5, total: 10 } }, result, accessor); + const output = await getFileCoverageDetails(file, uri.fsPath); + assert.ok(output.includes(`')); + assert.ok(!output.includes('uncovered')); + }); + + test('full output snapshot', async () => { + const uri = URI.file('/src/foo.ts'); + const details: CoverageDetails[] = [ + makeDeclaration('uncoveredFn', 10, 0), + makeDeclaration('coveredFn', 20, 3), + makeStatement(30, 0, 32), + makeStatement(40, 5, undefined, [ + makeBranch(40, 5, 'then'), + makeBranch(42, 0, 'else'), + ]), + makeStatement(50, 3), + ]; + const file = createFileCov( + uri, + { covered: 8, total: 10 }, + details, + { branch: { covered: 1, total: 2 }, declaration: { covered: 1, total: 2 } }, + ); + assert.deepStrictEqual( + await getFileCoverageDetails(file, uri.fsPath), + `\n` + + 'uncovered functions: uncoveredFn(L10)\n' + + 'uncovered branches: L42(else)\n' + + 'uncovered lines: 30-32\n' + + '\n', + ); + }); + }); + + suite('mergeLineRanges', () => { + test('returns empty for empty input', () => { + assert.strictEqual(mergeLineRanges([]), ''); + }); + + test('single range', () => { + assert.strictEqual(mergeLineRanges([[5, 10]]), '5-10'); + }); + + test('single line', () => { + assert.strictEqual(mergeLineRanges([[5, 5]]), '5'); + }); + + test('merges contiguous ranges', () => { + assert.strictEqual(mergeLineRanges([[1, 3], [4, 6]]), '1-6'); + }); + + test('keeps non-contiguous ranges separate', () => { + assert.strictEqual(mergeLineRanges([[1, 3], [10, 12]]), '1-3, 10-12'); + }); + + test('merges overlapping ranges', () => { + assert.strictEqual(mergeLineRanges([[1, 5], [3, 8]]), '1-8'); + }); + + test('merges adjacent single-line ranges', () => { + assert.strictEqual(mergeLineRanges([[5, 5], [6, 6], [10, 10]]), '5-6, 10'); + }); + + test('handles unsorted input', () => { + assert.strictEqual(mergeLineRanges([[10, 12], [1, 3], [4, 6]]), '1-6, 10-12'); + }); + + test('handles complex mixed ranges', () => { + assert.strictEqual(mergeLineRanges([[1, 1], [3, 5], [2, 2], [7, 9], [10, 10]]), '1-5, 7-10'); + }); + }); + + suite('getFailureDetails', () => { + test('formats expected/actual outputs', async () => { + const result = createResultWithTests([{ + extId: new TestId(['ctrlId', 'suite', 'myTest']).toString(), + label: 'myTest', + state: TestResultState.Failed, + messages: [{ + type: TestMessageType.Error, + message: 'Assertion failed', + expected: 'hello', + actual: 'world', + }], + }]); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(output.includes('\nhello\n')); + assert.ok(output.includes('\nworld\n')); + }); + + test('formats plain message when no expected/actual', async () => { + const result = createResultWithTests([{ + extId: new TestId(['ctrlId', 'myTest']).toString(), + label: 'myTest', + state: TestResultState.Failed, + messages: [{ + type: TestMessageType.Error, + message: 'Something went wrong', + }], + }]); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(output.includes('\nSomething went wrong\n')); + }); + + test('includes test name and path', async () => { + const result = createResultWithTests([{ + extId: new TestId(['ctrlId', 'suite1', 'suite2', 'myTest']).toString(), + label: 'myTest', + state: TestResultState.Failed, + messages: [{ type: TestMessageType.Error, message: 'fail' }], + }]); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(output.includes('name="myTest"')); + assert.ok(output.includes('path="suite1 > suite2"')); + }); + + test('includes stack trace frames', async () => { + const testUri = URI.file('/src/test.ts'); + const helperUri = URI.file('/src/helper.ts'); + const result = createResultWithTests([{ + extId: new TestId(['ctrlId', 'myTest']).toString(), + label: 'myTest', + state: TestResultState.Failed, + messages: [{ + type: TestMessageType.Error, + message: 'fail', + stackTrace: [ + { uri: testUri, position: { lineNumber: 10, column: 5 }, label: 'testFn' }, + { uri: helperUri, position: undefined, label: 'helperFn' }, + { uri: undefined, position: undefined, label: 'anonymous' }, + ], + }], + }]); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(output.includes(`path="${testUri.fsPath}" line="10" col="5"`)); + assert.ok(output.includes(`path="${helperUri.fsPath}">helperFn`)); + assert.ok(output.includes('>anonymous')); + }); + + test('includes location information', async () => { + const testUri = URI.file('/src/test.ts'); + const result = createResultWithTests([{ + extId: new TestId(['ctrlId', 'myTest']).toString(), + label: 'myTest', + state: TestResultState.Failed, + messages: [{ + type: TestMessageType.Error, + message: 'fail', + location: { uri: testUri, range: new Range(42, 8, 42, 20) }, + }], + }]); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(output.includes(`path="${testUri.fsPath}" line="42" col="8"`)); + }); + + test('skips passing tests', async () => { + const result = createResultWithTests([ + { extId: new TestId(['ctrlId', 'pass']).toString(), label: 'pass', state: TestResultState.Passed }, + { extId: new TestId(['ctrlId', 'fail']).toString(), label: 'fail', state: TestResultState.Failed, messages: [{ type: TestMessageType.Error, message: 'boom' }] }, + ]); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(!output.includes('name="pass"')); + assert.ok(output.includes('name="fail"')); + }); + + test('shows task output when no per-test messages', async () => { + const result = createResultWithTests([{ + extId: new TestId(['ctrlId', 'myTest']).toString(), + label: 'myTest', + state: TestResultState.Failed, + }]); + result.appendOutput(VSBuffer.fromString('raw test output'), 't'); + result.markComplete(); + + const output = await getFailureDetails(result); + assert.ok(output.includes('\nraw test output\n')); + }); + }); + + suite('prepareToolInvocation', () => { + test('shows file names in confirmation', async () => { + const prepared = await tool.prepareToolInvocation( + upcastPartial({ parameters: { files: ['/path/to/test1.ts', '/path/to/test2.ts'] }, toolCallId: 'call-1', chatSessionResource: undefined }), + CancellationToken.None, + ); + assert.ok(prepared); + const msg = prepared.confirmationMessages?.message; + assert.ok(msg); + const msgStr = typeof msg === 'string' ? msg : msg.value; + assert.ok(msgStr.includes('test1.ts')); + assert.ok(msgStr.includes('test2.ts')); + }); + + test('shows all-tests message when no files', async () => { + const prepared = await tool.prepareToolInvocation( + upcastPartial({ parameters: {}, toolCallId: 'call-2', chatSessionResource: undefined }), + CancellationToken.None, + ); + assert.ok(prepared); + const msg = prepared.confirmationMessages?.message; + assert.ok(msg); + const msgStr = typeof msg === 'string' ? msg : msg.value; + assert.ok(msgStr.toLowerCase().includes('all tests')); + }); + }); +}); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 5614822fcb0..382c529d0bd 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -9,7 +9,7 @@ import { MenuRegistry, MenuId, Action2, registerAction2, ISubmenuItem } from '.. import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings } from '../../../services/themes/common/workbenchThemeService.js'; +import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings, ThemeSettingDefaults } from '../../../services/themes/common/workbenchThemeService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IColorRegistry, Extensions as ColorRegistryExtensions } from '../../../../platform/theme/common/colorRegistry.js'; @@ -557,6 +557,90 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.tryNewDefaultThemes', + title: localize2('tryNewDefaultThemes', "Try New Default Themes"), + category: Categories.Preferences, + f1: true, + }); + } + override async run(accessor: ServicesAccessor) { + const themeService = accessor.get(IWorkbenchThemeService); + const quickInputService = accessor.get(IQuickInputService); + const configurationService = accessor.get(IConfigurationService); + + const previousTheme = themeService.getColorTheme(); + const allThemes = await themeService.getColorThemes(); + const newThemeSettingsIds = new Set([ThemeSettingDefaults.COLOR_THEME_LIGHT, ThemeSettingDefaults.COLOR_THEME_DARK]); + const themes = allThemes.filter(t => newThemeSettingsIds.has(t.settingsId)); + + const items: IQuickPickItem[] = themes.map(t => ({ + id: t.id, + label: t.label, + description: t.description, + })); + + const disposables = new DisposableStore(); + const picker = disposables.add(quickInputService.createQuickPick()); + picker.items = items; + picker.placeholder = localize('pickNewTheme', "Pick a new default theme"); + picker.canSelectMany = false; + + const preferredId = (previousTheme.type === ColorScheme.LIGHT || previousTheme.type === ColorScheme.HIGH_CONTRAST_LIGHT) ? ThemeSettingDefaults.COLOR_THEME_LIGHT : ThemeSettingDefaults.COLOR_THEME_DARK; + const activeItem = items.find(i => themes.find(t => t.id === i.id)?.settingsId === preferredId); + if (activeItem) { + picker.activeItems = [activeItem]; + } + + disposables.add(picker.onDidChangeActive(selected => { + if (selected[0]) { + const theme = themes.find(t => t.id === selected[0].id); + if (theme) { + themeService.setColorTheme(theme, 'preview'); + } + } + })); + + disposables.add(picker.onDidAccept(() => { + const selected = picker.activeItems[0]; + const theme = selected ? themes.find(t => t.id === selected.id) : undefined; + + picker.hide(); + + if (!theme) { + return; + } + + (async () => { + try { + await themeService.setColorTheme(theme, 'auto'); + await configurationService.updateValue(ThemeSettings.PREFERRED_LIGHT_THEME, ThemeSettingDefaults.COLOR_THEME_LIGHT); + await configurationService.updateValue(ThemeSettings.PREFERRED_DARK_THEME, ThemeSettingDefaults.COLOR_THEME_DARK); + } catch (error) { + if (!isCancellationError(error)) { + onUnexpectedError(error); + } + } + })(); + })); + + const result = new Promise(resolve => { + disposables.add(picker.onDidHide(() => { + if (!picker.selectedItems.length) { + themeService.setColorTheme(previousTheme, undefined); + } + resolve(); + })); + }).finally(() => disposables.dispose()); + + picker.show(); + + return result; + } +}); + CommandsRegistry.registerCommand('workbench.action.previewColorTheme', async function (accessor: ServicesAccessor, extension: { publisher: string; name: string; version: string }, themeSettingsId?: string) { const themeService = accessor.get(IWorkbenchThemeService); @@ -602,13 +686,18 @@ function isItem(i: QuickPickInput): i is ThemeItem { return (i)['type'] !== 'separator'; } +const defaultThemeDescriptions: Record = { + [ThemeSettingDefaults.COLOR_THEME_LIGHT]: localize('defaultLight', "Default Light"), + [ThemeSettingDefaults.COLOR_THEME_DARK]: localize('defaultDark', "Default Dark"), +}; + function toEntry(theme: IWorkbenchTheme): ThemeItem { const settingId = theme.settingsId ?? undefined; const item: ThemeItem = { id: theme.id, theme: theme, label: theme.label, - description: theme.description || (theme.label === settingId ? undefined : settingId), + description: defaultThemeDescriptions[settingId ?? ''] ?? theme.description ?? (theme.label === settingId ? undefined : settingId), }; if (theme.extensionData) { item.buttons = [configureButton]; @@ -617,7 +706,15 @@ function toEntry(theme: IWorkbenchTheme): ThemeItem { } function toEntries(themes: Array, label?: string): QuickPickInput[] { - const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label); + const pinnedIds = new Set([ThemeSettingDefaults.COLOR_THEME_DARK, ThemeSettingDefaults.COLOR_THEME_LIGHT]); + const sorter = (t1: ThemeItem, t2: ThemeItem) => { + const pin1 = pinnedIds.has(t1.theme?.settingsId ?? ''); + const pin2 = pinnedIds.has(t2.theme?.settingsId ?? ''); + if (pin1 !== pin2) { + return pin1 ? -1 : 1; + } + return t1.label.localeCompare(t2.label); + }; const entries: QuickPickInput[] = themes.map(toEntry).sort(sorter); if (entries.length > 0 && label) { entries.unshift({ type: 'separator', label }); diff --git a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index 4c89442974b..33f120ddfeb 100644 --- a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -121,7 +121,7 @@ suite('Color Registry', function () { const docUrl = 'https://raw.githubusercontent.com/microsoft/vscode-docs/vnext/api/references/theme-color.md'; - const reqContext = await new RequestService('local', new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); + const reqContext = await new RequestService('local', new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl, callSite: 'colorRegistry.releaseTest' }, CancellationToken.None); const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css b/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css index eb3ac37b111..91d6c9ad61e 100644 --- a/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css +++ b/src/vs/workbench/contrib/update/browser/media/updateTitleBarEntry.css @@ -9,7 +9,7 @@ border-radius: var(--vscode-cornerRadius-medium); white-space: nowrap; padding: 0px 12px; - height: 24px; + height: 22px; background-color: transparent; border: 1px solid transparent; } @@ -46,8 +46,8 @@ content: ''; position: absolute; left: 0; - bottom: 0; - height: 2px; + bottom: 1px; + height: 1px; border-radius: 1px; } @@ -80,6 +80,21 @@ transition: background 0.3s ease; } +/* Bounce animation — macOS dock-style bounce when prominent state appears */ +.monaco-action-bar .update-indicator.prominent { + animation: update-indicator-bounce 0.6s ease; +} + +@keyframes update-indicator-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 85% { transform: translateY(-1px); } + 100% { transform: translateY(0); } +} + /* Reduced motion */ .monaco-workbench.monaco-reduce-motion .update-indicator.progress-indefinite .indicator-label::after { animation: none; @@ -88,3 +103,7 @@ .monaco-workbench.monaco-reduce-motion .update-indicator.progress-percent .indicator-label::after { transition: none; } + +.monaco-workbench.monaco-reduce-motion .update-indicator.prominent { + animation: none; +} diff --git a/src/vs/workbench/contrib/update/browser/media/updateTooltip.css b/src/vs/workbench/contrib/update/browser/media/updateTooltip.css index ab714ea2e0f..48b78f05bd4 100644 --- a/src/vs/workbench/contrib/update/browser/media/updateTooltip.css +++ b/src/vs/workbench/contrib/update/browser/media/updateTooltip.css @@ -8,8 +8,8 @@ flex-direction: column; gap: 12px; padding: 6px 6px; - min-width: 310px; - max-width: 410px; + min-width: 300px; + max-width: 350px; color: var(--vscode-descriptionForeground); font-size: var(--vscode-bodyFontSize-small); } @@ -54,6 +54,30 @@ margin-bottom: 4px; } +.update-tooltip .product-version { + display: flex; + align-items: center; + gap: 4px; +} + +.update-tooltip .copy-version-button { + cursor: pointer; + opacity: 0; + color: var(--vscode-descriptionForeground); + transition: opacity 0.1s; + margin-top: -2px; +} + +.update-tooltip .product-version:hover .copy-version-button, +.update-tooltip .product-version:focus-within .copy-version-button { + opacity: 1; +} + +.update-tooltip .copy-version-button:hover, +.update-tooltip .copy-version-button:focus-visible { + color: var(--vscode-foreground); +} + .update-tooltip .release-notes-link { color: var(--vscode-textLink-foreground); text-decoration: none; @@ -113,3 +137,23 @@ .update-tooltip .state-message-icon.codicon.codicon-error { color: var(--vscode-editorError-foreground); } + +/* Markdown */ +.update-tooltip .update-markdown { + background: var(--vscode-editor-background); + border-radius: var(--vscode-cornerRadius-large); + padding: 12px; +} + +.update-tooltip .update-markdown p { + margin-bottom: 16px; +} + +.update-tooltip .update-markdown p:last-child { + margin-bottom: 0; +} + +.update-tooltip .update-markdown .codicon[class*='codicon-'] { + font-size: 16px; + vertical-align: text-top; +} diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 92c942f9d5e..2cd16781ec5 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -222,7 +222,7 @@ export class ReleaseNotesManager extends Disposable { const file = this._codeEditorService.getActiveCodeEditor()?.getModel()?.getValue(); text = file ? file.substring(file.indexOf('#')) : undefined; } else { - text = await asTextOrError(await this._requestService.request({ url }, CancellationToken.None)); + text = await asTextOrError(await this._requestService.request({ url, callSite: 'releaseNotesEditor.fetchReleaseNotes' }, CancellationToken.None)); } } catch { throw new Error('Failed to fetch release notes'); @@ -275,7 +275,7 @@ export class ReleaseNotesManager extends Disposable { private async renderBody(fileContent: { text: string; base: URI }) { const nonce = generateUuid(); - const processedContent = await renderReleaseNotesMarkdown(fileContent.text, this._extensionService, this._languageService, this._simpleSettingRenderer); + const processedContent = await renderReleaseNotesMarkdown(fileContent.text, this._extensionService, this._languageService, this._simpleSettingRenderer, this._productService.quality); const colorMap = TokenizationRegistry.getColorMap(); const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; @@ -774,11 +774,46 @@ export class ReleaseNotesManager extends Disposable { } } +/** + * Processes conditional blocks in the release notes markdown. + * + * Conditional blocks use a single HTML comment with the format: + * ``` + * + * ``` + * + * Supported conditions: + * - `IN_PRODUCT` - Content shown in VS Code (both Stable and Insiders) + * - `WEB` - Content shown on the website only + * - `STABLE` - Content shown in VS Code Stable only + * - `INSIDERS` - Content shown in VS Code Insiders only + * + * On the website, the entire block is a single HTML comment, so the + * content is hidden by default. The website renderer would activate + * `WEB` blocks by stripping the comment markers. + */ +export function processConditionalBlocks(text: string, activeConditions: ReadonlySet): string { + return text.replace( + //gi, + (_match, condition: string, content: string) => { + if (activeConditions.has(condition.toUpperCase())) { + // Strip comment markers, reveal content + return content; + } + // Remove the entire block + return ''; + } + ); +} + export async function renderReleaseNotesMarkdown( text: string, extensionService: IExtensionService, languageService: ILanguageService, simpleSettingRenderer: SimpleSettingRenderer, + quality?: string, ): Promise { // Remove HTML comment markers around table of contents navigation text = text @@ -786,6 +821,15 @@ export async function renderReleaseNotesMarkdown( .replace(//gi, ''); + // Process conditional blocks based on active conditions + const activeConditions = new Set(['IN_PRODUCT']); + if (quality === 'stable') { + activeConditions.add('STABLE'); + } else if (quality === 'insider') { + activeConditions.add('INSIDERS'); + } + text = processConditionalBlocks(text, activeConditions); + return renderMarkdownDocument(text, extensionService, languageService, { sanitizerConfig: { allowRelativeMediaPaths: true, diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index aca3bb3ce27..1f0e150c205 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -33,6 +33,8 @@ import { toAction } from '../../../../base/common/actions.js'; import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js'; import { getInternalOrg } from '../../../../platform/assignment/common/assignment.js'; import { IVersion, preprocessError, tryParseVersion } from '../common/updateUtils.js'; +import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; +import { mainWindow } from '../../../../base/browser/window.js'; export const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Uninitialized); export const MAJOR_MINOR_UPDATE_AVAILABLE = new RawContextKey('majorMinorUpdateAvailable', false); @@ -227,12 +229,14 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IHostService private readonly hostService: IHostService + @IHostService private readonly hostService: IHostService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(); this.state = updateService.state; this.updateStateContextKey = CONTEXT_UPDATE_STATE.bindTo(this.contextKeyService); this.majorMinorUpdateAvailableContextKey = MAJOR_MINOR_UPDATE_AVAILABLE.bindTo(this.contextKeyService); + this.titleBarEnabled = this.isTitleBarEnabled(); this._register(updateService.onStateChange(this.onUpdateStateChange, this)); this.onUpdateStateChange(this.updateService.state); @@ -254,10 +258,16 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu this.storageService.remove('update/updateNotificationTime', StorageScope.APPLICATION); } - this.titleBarEnabled = this.configurationService.getValue('update.titleBar') !== 'none'; this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('update.titleBar')) { - this.titleBarEnabled = this.configurationService.getValue('update.titleBar') !== 'none'; + this.titleBarEnabled = this.isTitleBarEnabled(); + this.onUpdateStateChange(this.updateService.state); + } + })); + + this._register(this.layoutService.onDidChangePartVisibility(e => { + if (e.partId === Parts.TITLEBAR_PART) { + this.titleBarEnabled = this.isTitleBarEnabled(); this.onUpdateStateChange(this.updateService.state); } })); @@ -265,6 +275,11 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu this.registerGlobalActivityActions(); } + private isTitleBarEnabled(): boolean { + return this.configurationService.getValue('update.titleBar') !== 'none' + && this.layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow); + } + private async onUpdateStateChange(state: UpdateState): Promise { this.updateStateContextKey.set(state.type); @@ -465,36 +480,43 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu }) ] }); - } else if ((isWindows && this.productService.target !== 'user') || this.shouldShowNotification()) { - - const actions = [{ - label: nls.localize('updateNow', "Update Now"), - run: () => this.updateService.quitAndInstall() - }, { - label: nls.localize('later', "Later"), - run: () => { } - }]; - - const productVersion = state.update.productVersion; - if (productVersion) { - actions.push({ - label: nls.localize('releaseNotes', "Release Notes"), - run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); - } - }); + } else { + // Dismiss stale overwrite notification if the overwrite resolved without finding a newer update. + if (this.overwriteNotificationHandle) { + this.overwriteNotificationHandle.close(); + this.overwriteNotificationHandle = undefined; } - // windows user fast updates and mac - this.notificationService.prompt( - severity.Info, - nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), - actions, - { - sticky: true, - priority: NotificationPriority.OPTIONAL + if ((isWindows && this.productService.target !== 'user') || this.shouldShowNotification()) { + const actions = [{ + label: nls.localize('updateNow', "Update Now"), + run: () => this.updateService.quitAndInstall() + }, { + label: nls.localize('later', "Later"), + run: () => { } + }]; + + const productVersion = state.update.productVersion; + if (productVersion) { + actions.push({ + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } + }); } - ); + + // windows user fast updates and mac + this.notificationService.prompt( + severity.Info, + nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), + actions, + { + sticky: true, + priority: NotificationPriority.OPTIONAL + } + ); + } } } diff --git a/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts b/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts index 4ed3e130eea..9051baf1c59 100644 --- a/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts +++ b/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts @@ -37,7 +37,7 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc return; // Electron only } - this.tooltip = this._register(instantiationService.createInstance(UpdateTooltip)); + this.tooltip = this._register(instantiationService.createInstance(UpdateTooltip, false)); this._register(updateService.onStateChange(this.onStateChange.bind(this))); this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -67,10 +67,11 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc this.lastStateType = state.type; } + this.tooltip.renderState(state); switch (state.type) { case StateType.CheckingForUpdates: this.updateEntry( - localize('updateStatus.checkingForUpdates', "$(loading~spin) Checking for updates..."), + '$(loading~spin) ' + localize('updateStatus.checkingForUpdates', "Checking for updates..."), localize('updateStatus.checkingForUpdatesAria', "Checking for updates"), ShowTooltipCommand, ); @@ -78,7 +79,7 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc case StateType.AvailableForDownload: this.updateEntry( - localize('updateStatus.updateAvailableStatus', "$(circle-filled) Update available, click to download."), + '$(circle-filled) ' + localize('updateStatus.updateAvailableStatus', "Update available, click to download."), localize('updateStatus.updateAvailableAria', "Update available, click to download."), 'update.downloadNow' ); @@ -94,7 +95,7 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc case StateType.Downloaded: this.updateEntry( - localize('updateStatus.updateReadyStatus', "$(circle-filled) Update downloaded, click to install."), + '$(circle-filled) ' + localize('updateStatus.updateReadyStatus', "Update downloaded, click to install."), localize('updateStatus.updateReadyAria', "Update downloaded, click to install."), 'update.install' ); @@ -110,7 +111,7 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc case StateType.Ready: this.updateEntry( - localize('updateStatus.restartToUpdateStatus', "$(circle-filled) Update is ready, click to restart."), + '$(circle-filled) ' + localize('updateStatus.restartToUpdateStatus', "Update is ready, click to restart."), localize('updateStatus.restartToUpdateAria', "Update is ready, click to restart."), 'update.restart' ); @@ -118,7 +119,7 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc case StateType.Overwriting: this.updateEntry( - localize('updateStatus.downloadingNewerUpdateStatus', "$(loading~spin) Downloading update..."), + '$(loading~spin) ' + localize('updateStatus.downloadingNewerUpdateStatus', "Downloading update..."), localize('updateStatus.downloadingNewerUpdateAria', "Downloading a newer update"), ShowTooltipCommand ); @@ -154,21 +155,21 @@ export class UpdateStatusBarContribution extends Disposable implements IWorkbenc private getDownloadingText({ downloadedBytes, totalBytes }: Downloading): string { if (downloadedBytes !== undefined && totalBytes !== undefined && totalBytes > 0) { const percent = computeProgressPercent(downloadedBytes, totalBytes) ?? 0; - return localize('updateStatus.downloadUpdateProgressStatus', "$(loading~spin) Downloading update: {0} / {1} • {2}%", + return '$(loading~spin) ' + localize('updateStatus.downloadUpdateProgressStatus', "Downloading update: {0} / {1} • {2}%", formatBytes(downloadedBytes), formatBytes(totalBytes), percent); } else { - return localize('updateStatus.downloadUpdateStatus', "$(loading~spin) Downloading update..."); + return '$(loading~spin) ' + localize('updateStatus.downloadUpdateStatus', "Downloading update..."); } } private getUpdatingText({ currentProgress, maxProgress }: Updating): string { const percentage = computeProgressPercent(currentProgress, maxProgress); if (percentage !== undefined) { - return localize('updateStatus.installingUpdateProgressStatus', "$(loading~spin) Installing update: {0}%", percentage); + return '$(loading~spin) ' + localize('updateStatus.installingUpdateProgressStatus', "Installing update: {0}%", percentage); } else { - return localize('updateStatus.installingUpdateStatus', "$(loading~spin) Installing update..."); + return '$(loading~spin) ' + localize('updateStatus.installingUpdateStatus', "Installing update..."); } } } diff --git a/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts b/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts index 93be7e36170..e76e7a1baf5 100644 --- a/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts +++ b/src/vs/workbench/contrib/update/browser/updateTitleBarEntry.ts @@ -6,7 +6,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IManagedHoverContent } from '../../../../base/browser/ui/hover/hover.js'; -import { IAction } from '../../../../base/common/actions.js'; +import { IAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { isWeb } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; @@ -14,21 +14,32 @@ import { IActionViewItemService } from '../../../../platform/actions/browser/act import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { DisablementReason, IUpdateService, State, StateType } from '../../../../platform/update/common/update.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { computeProgressPercent, tryParseVersion } from '../common/updateUtils.js'; +import { IHostService } from '../../../services/host/browser/host.js'; +import { computeProgressPercent, isMajorMinorVersionChange } from '../common/updateUtils.js'; import './media/updateTitleBarEntry.css'; import { UpdateTooltip } from './updateTooltip.js'; const UPDATE_TITLE_BAR_ACTION_ID = 'workbench.actions.updateIndicator'; const UPDATE_TITLE_BAR_CONTEXT = new RawContextKey('updateTitleBar', false); -const LAST_KNOWN_VERSION_KEY = 'updateTitleBar/lastKnownVersion'; + const ACTIONABLE_STATES: readonly StateType[] = [StateType.AvailableForDownload, StateType.Downloaded, StateType.Ready]; +const DETAILED_STATES: readonly StateType[] = [...ACTIONABLE_STATES, StateType.CheckingForUpdates, StateType.Downloading, StateType.Updating, StateType.Overwriting]; + +const LAST_KNOWN_VERSION_KEY = 'updateTitleBarEntry/lastKnownVersion'; + +interface ILastKnownVersion { + readonly version: string; + readonly commit: string | undefined; + readonly timestamp: number; +} registerAction2(class UpdateIndicatorTitleBarAction extends Action2 { constructor() { @@ -37,8 +48,8 @@ registerAction2(class UpdateIndicatorTitleBarAction extends Action2 { title: localize('updateIndicatorTitleBarAction', 'Update'), f1: false, menu: [{ - id: MenuId.CommandCenter, - order: 10003, + id: MenuId.TitleBarAdjacentCenter, + order: 0, when: UPDATE_TITLE_BAR_CONTEXT, }] }); @@ -51,10 +62,18 @@ registerAction2(class UpdateIndicatorTitleBarAction extends Action2 { * Displays update status and actions in the title bar. */ export class UpdateTitleBarContribution extends Disposable implements IWorkbenchContribution { + private readonly context!: IContextKey; + private readonly tooltip!: UpdateTooltip; + private mode: 'always' | 'detailed' | 'actionable' | 'none' = 'none'; + private state!: State; + private entry: UpdateTitleBarEntry | undefined; + private tooltipVisible = false; + constructor( @IActionViewItemService actionViewItemService: IActionViewItemService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, + @IHostService private readonly hostService: IHostService, @IInstantiationService instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, @IStorageService private readonly storageService: IStorageService, @@ -66,80 +85,117 @@ export class UpdateTitleBarContribution extends Disposable implements IWorkbench return; // Electron only } - const context = UPDATE_TITLE_BAR_CONTEXT.bindTo(contextKeyService); + this.context = UPDATE_TITLE_BAR_CONTEXT.bindTo(contextKeyService); + this.tooltip = this._register(instantiationService.createInstance(UpdateTooltip, true)); - const updateContext = () => { - const mode = configurationService.getValue('update.titleBar'); - const state = updateService.state.type; - context.set(mode === 'detailed' || mode === 'actionable' && ACTIONABLE_STATES.includes(state)); - }; - - let entry: UpdateTitleBarEntry | undefined; - let showTooltipOnRender = false; - - this._register(actionViewItemService.register( - MenuId.CommandCenter, - UPDATE_TITLE_BAR_ACTION_ID, - (action, options) => { - entry = instantiationService.createInstance(UpdateTitleBarEntry, action, options, updateContext, showTooltipOnRender); - showTooltipOnRender = false; - return entry; - } - )); - - const onStateChange = () => { - if (this.shouldShowTooltip(updateService.state)) { - if (context.get()) { - entry?.showTooltip(); - } else { - context.set(true); - showTooltipOnRender = true; - } - } else { - updateContext(); - } - }; - - this._register(updateService.onStateChange(onStateChange)); + this.mode = configurationService.getValue('update.titleBar') as typeof this.mode; this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('update.titleBar')) { - updateContext(); + this.mode = configurationService.getValue('update.titleBar') as typeof this.mode; + this.onStateChange(); } })); - onStateChange(); + this.state = updateService.state; + this._register(updateService.onStateChange((state) => { + this.state = state; + this.onStateChange(); + })); + + this._register(actionViewItemService.register( + MenuId.TitleBarAdjacentCenter, + UPDATE_TITLE_BAR_ACTION_ID, + (action, options) => { + this.entry = instantiationService.createInstance(UpdateTitleBarEntry, action, options, this.tooltip, () => { + this.tooltipVisible = false; + this.updateContext(); + }); + if (this.tooltipVisible) { + this.entry.showTooltip(); + } + return this.entry; + } + )); + + void this.onStateChange(true); } - private shouldShowTooltip(state: State): boolean { - switch (state.type) { - case StateType.Disabled: - return state.reason === DisablementReason.InvalidConfiguration || state.reason === DisablementReason.RunningAsAdmin; - case StateType.Idle: - return !!state.error || state.notAvailable || this.isMajorMinorVersionChange(); - case StateType.AvailableForDownload: - case StateType.Downloaded: - case StateType.Ready: - return true; + private updateContext() { + switch (this.mode) { + case 'always': + this.context.set(true); + break; + case 'detailed': + this.context.set(DETAILED_STATES.includes(this.state.type)); + break; + case 'actionable': + this.context.set(ACTIONABLE_STATES.includes(this.state.type)); + break; default: - return false; + this.context.set(false); + break; } } - private isMajorMinorVersionChange(): boolean { - const currentVersion = this.productService.version; - const lastKnownVersion = this.storageService.get(LAST_KNOWN_VERSION_KEY, StorageScope.APPLICATION); - this.storageService.store(LAST_KNOWN_VERSION_KEY, currentVersion, StorageScope.APPLICATION, StorageTarget.MACHINE); - if (!lastKnownVersion) { + private async onStateChange(startup = false) { + this.updateContext(); + if (this.mode === 'none') { + return; + } + + if (this.tooltipVisible || !await this.hostService.hadLastFocus()) { + this.tooltip.renderState(this.state); + return; + } + + let showTooltip = startup && this.detectVersionChange(); + if (showTooltip) { + this.tooltip.renderPostInstall(); + } else { + this.tooltip.renderState(this.state); + switch (this.state.type) { + case StateType.Disabled: + if (startup) { + const reason = this.state.reason; + showTooltip = reason === DisablementReason.InvalidConfiguration || reason === DisablementReason.RunningAsAdmin; + } + break; + case StateType.Idle: + showTooltip = !!this.state.error || !!this.state.notAvailable; + break; + } + } + + if (showTooltip) { + this.tooltipVisible = true; + this.context.set(true); + this.entry?.showTooltip(); + } + } + + private detectVersionChange() { + let from: ILastKnownVersion | undefined; + try { + from = this.storageService.getObject(LAST_KNOWN_VERSION_KEY, StorageScope.APPLICATION); + } catch { } + + const to: ILastKnownVersion = { + version: this.productService.version, + commit: this.productService.commit, + timestamp: Date.now(), + }; + + if (from?.commit === to.commit) { return false; } - const current = tryParseVersion(currentVersion); - const last = tryParseVersion(lastKnownVersion); - if (!current || !last) { - return false; + this.storageService.store(LAST_KNOWN_VERSION_KEY, JSON.stringify(to), StorageScope.APPLICATION, StorageTarget.MACHINE); + + if (from) { + return isMajorMinorVersionChange(from.version, to.version); } - return current.major !== last.major || current.minor !== last.minor; + return false; } } @@ -148,24 +204,22 @@ export class UpdateTitleBarContribution extends Disposable implements IWorkbench */ export class UpdateTitleBarEntry extends BaseActionViewItem { private content: HTMLElement | undefined; - private readonly tooltip: UpdateTooltip; + private showTooltipOnRender = false; constructor( action: IAction, options: IBaseActionViewItemOptions, - private readonly onDisposeTooltip: () => void, - private showTooltipOnRender: boolean, + private readonly tooltip: UpdateTooltip, + private readonly onUserDismissedTooltip: () => void, @ICommandService private readonly commandService: ICommandService, @IHoverService private readonly hoverService: IHoverService, - @IInstantiationService instantiationService: IInstantiationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @IUpdateService private readonly updateService: IUpdateService, ) { super(undefined, action, options); this.action.run = () => this.runAction(); - this.tooltip = this._register(instantiationService.createInstance(UpdateTooltip)); - - this._register(this.updateService.onStateChange(state => this.updateContent(state))); + this._register(this.updateService.onStateChange(state => this.onStateChange(state))); } public override render(container: HTMLElement) { @@ -173,7 +227,7 @@ export class UpdateTitleBarEntry extends BaseActionViewItem { this.content = dom.append(container, dom.$('.update-indicator')); this.updateTooltip(); - this.updateContent(this.updateService.state); + this.onStateChange(this.updateService.state); if (this.showTooltipOnRender) { this.showTooltipOnRender = false; @@ -181,29 +235,9 @@ export class UpdateTitleBarEntry extends BaseActionViewItem { } } - protected override getHoverContents(): IManagedHoverContent { - return this.tooltip.domNode; - } - - private runAction() { - switch (this.updateService.state.type) { - case StateType.AvailableForDownload: - this.commandService.executeCommand('update.downloadNow'); - break; - case StateType.Downloaded: - this.commandService.executeCommand('update.install'); - break; - case StateType.Ready: - this.commandService.executeCommand('update.restart'); - break; - default: - this.showTooltip(); - break; - } - } - - public showTooltip() { + public showTooltip(focus = false) { if (!this.content?.isConnected) { + this.showTooltipOnRender = true; return; } @@ -211,14 +245,43 @@ export class UpdateTitleBarEntry extends BaseActionViewItem { content: this.tooltip.domNode, target: { targetElements: [this.content], - dispose: () => this.onDisposeTooltip(), + dispose: () => { + if (!!this.content?.isConnected) { + this.onUserDismissedTooltip(); + } + } }, persistence: { sticky: true }, - appearance: { showPointer: true }, - }, true); + appearance: { showPointer: true, compact: true }, + }, focus); } - private updateContent(state: State) { + protected override getHoverContents(): IManagedHoverContent { + return this.tooltip.domNode; + } + + private async runAction() { + let commandId: string | undefined; + switch (this.updateService.state.type) { + case StateType.AvailableForDownload: + commandId = 'update.downloadNow'; + break; + case StateType.Downloaded: + commandId = 'update.install'; + break; + case StateType.Ready: + commandId = 'update.restart'; + break; + default: + this.showTooltip(true); + return; + } + + this.telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'titlebar' }); + await this.commandService.executeCommand(commandId); + } + + private onStateChange(state: State) { if (!this.content) { return; } diff --git a/src/vs/workbench/contrib/update/browser/updateTooltip.ts b/src/vs/workbench/contrib/update/browser/updateTooltip.ts index ee3c452c3be..56e15f6a0cf 100644 --- a/src/vs/workbench/contrib/update/browser/updateTooltip.ts +++ b/src/vs/workbench/contrib/update/browser/updateTooltip.ts @@ -6,17 +6,23 @@ import * as dom from '../../../../base/browser/dom.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { toAction } from '../../../../base/common/actions.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js'; +import { IMarkdownRendererService, openLinkFromMarkdown } from '../../../../platform/markdown/browser/markdownRenderer.js'; import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { AvailableForDownload, Disabled, DisablementReason, Downloaded, Downloading, Idle, IUpdate, IUpdateService, Overwriting, Ready, State, StateType, Updating } from '../../../../platform/update/common/update.js'; -import { computeDownloadSpeed, computeDownloadTimeRemaining, computeProgressPercent, formatBytes, formatDate, formatTimeRemaining, tryParseDate } from '../common/updateUtils.js'; +import { asTextOrError, IRequestService } from '../../../../platform/request/common/request.js'; +import { AvailableForDownload, Disabled, DisablementReason, Downloaded, Downloading, Idle, IUpdate, Overwriting, Ready, State, StateType, Updating } from '../../../../platform/update/common/update.js'; +import { computeDownloadSpeed, computeDownloadTimeRemaining, computeProgressPercent, formatBytes, formatDate, formatTimeRemaining, getUpdateInfoUrl, tryParseDate } from '../common/updateUtils.js'; import './media/updateTooltip.css'; /** @@ -29,9 +35,12 @@ export class UpdateTooltip extends Disposable { private readonly titleNode: HTMLElement; // Product info section + private readonly productInfoNode: HTMLElement; private readonly productNameNode: HTMLElement; private readonly currentVersionNode: HTMLElement; + private readonly currentVersionCopyValue: { value: string }; private readonly latestVersionNode: HTMLElement; + private readonly latestVersionCopyValue: { value: string }; private readonly releaseDateNode: HTMLElement; private readonly releaseNotesLink: HTMLAnchorElement; @@ -46,18 +55,26 @@ export class UpdateTooltip extends Disposable { private readonly timeRemainingNode: HTMLElement; private readonly speedInfoNode: HTMLElement; + // Update markdown section + private readonly markdownContainer: HTMLElement; + private readonly markdown = this._register(new MutableDisposable()); + // State-specific message private readonly messageNode: HTMLElement; private releaseNotesVersion: string | undefined; constructor( + private readonly hostedByTitleBar: boolean, + @IClipboardService private readonly clipboardService: IClipboardService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IHoverService private readonly hoverService: IHoverService, + @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @IMeteredConnectionService private readonly meteredConnectionService: IMeteredConnectionService, + @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @IUpdateService updateService: IUpdateService, + @IRequestService private readonly requestService: IRequestService, ) { super(); @@ -76,19 +93,25 @@ export class UpdateTooltip extends Disposable { }), { icon: true, label: false }); // Product info section - const productInfo = dom.append(this.domNode, dom.$('.product-info')); + this.productInfoNode = dom.append(this.domNode, dom.$('.product-info')); - const logoContainer = dom.append(productInfo, dom.$('.product-logo')); + const logoContainer = dom.append(this.productInfoNode, dom.$('.product-logo')); logoContainer.setAttribute('role', 'img'); logoContainer.setAttribute('aria-label', this.productService.nameLong); - const details = dom.append(productInfo, dom.$('.product-details')); + const details = dom.append(this.productInfoNode, dom.$('.product-details')); this.productNameNode = dom.append(details, dom.$('.product-name')); this.productNameNode.textContent = this.productService.nameLong; - this.currentVersionNode = dom.append(details, dom.$('.product-version')); - this.latestVersionNode = dom.append(details, dom.$('.product-version')); + const currentVersionRow = this.createVersionRow(details); + this.currentVersionNode = currentVersionRow.label; + this.currentVersionCopyValue = currentVersionRow.copyValue; + + const latestVersionRow = this.createVersionRow(details); + this.latestVersionNode = latestVersionRow.label; + this.latestVersionCopyValue = latestVersionRow.copyValue; + this.releaseDateNode = dom.append(details, dom.$('.product-release-date')); this.releaseNotesLink = dom.append(details, dom.$('a.release-notes-link')) as HTMLAnchorElement; @@ -115,15 +138,14 @@ export class UpdateTooltip extends Disposable { this.timeRemainingNode = dom.append(this.downloadStatsContainer, dom.$('.time-remaining')); this.speedInfoNode = dom.append(this.downloadStatsContainer, dom.$('.speed-info')); + // Update markdown section + this.markdownContainer = dom.append(this.domNode, dom.$('.update-markdown')); + // State-specific message this.messageNode = dom.append(this.domNode, dom.$('.state-message')); // Populate static product info this.updateCurrentVersion(); - - // Subscribe to state changes - this._register(updateService.onStateChange(state => this.onStateChange(state))); - this.onStateChange(updateService.state); } private updateCurrentVersion() { @@ -133,18 +155,25 @@ export class UpdateTooltip extends Disposable { this.currentVersionNode.textContent = currentCommitId ? localize('updateTooltip.currentVersionLabelWithCommit', "Current Version: {0} ({1})", productVersion, currentCommitId) : localize('updateTooltip.currentVersionLabel', "Current Version: {0}", productVersion); - this.currentVersionNode.style.display = ''; + this.currentVersionCopyValue.value = currentCommitId ? `${productVersion} (${this.productService.commit})` : productVersion; + this.currentVersionNode.parentElement!.style.display = ''; } else { - this.currentVersionNode.style.display = 'none'; + this.currentVersionNode.parentElement!.style.display = 'none'; } } - private onStateChange(state: State) { + private hideAll() { + this.productInfoNode.style.display = ''; this.progressContainer.style.display = 'none'; this.speedInfoNode.textContent = ''; this.timeRemainingNode.textContent = ''; this.messageNode.style.display = 'none'; + this.markdownContainer.style.display = 'none'; + this.markdown.clear(); + } + public renderState(state: State) { + this.hideAll(); switch (state.type) { case StateType.Uninitialized: this.renderUninitialized(); @@ -181,44 +210,44 @@ export class UpdateTooltip extends Disposable { private renderUninitialized() { this.renderTitleAndInfo(localize('updateTooltip.initializingTitle', "Initializing")); - this.showMessage(localize('updateTooltip.initializingMessage', "Initializing update service...")); + this.renderMessage(localize('updateTooltip.initializingMessage', "Initializing update service...")); } private renderDisabled({ reason }: Disabled) { this.renderTitleAndInfo(localize('updateTooltip.updatesDisabledTitle', "Updates Disabled")); switch (reason) { case DisablementReason.NotBuilt: - this.showMessage( + this.renderMessage( localize('updateTooltip.disabledNotBuilt', "Updates are not available for this build."), Codicon.info); break; case DisablementReason.DisabledByEnvironment: - this.showMessage( + this.renderMessage( localize('updateTooltip.disabledByEnvironment', "Updates are disabled by the --disable-updates command line flag."), Codicon.warning); break; case DisablementReason.ManuallyDisabled: - this.showMessage( + this.renderMessage( localize('updateTooltip.disabledManually', "Updates are manually disabled. Change the \"update.mode\" setting to enable."), Codicon.warning); break; case DisablementReason.Policy: - this.showMessage( + this.renderMessage( localize('updateTooltip.disabledByPolicy', "Updates are disabled by organization policy."), Codicon.info); break; case DisablementReason.MissingConfiguration: - this.showMessage( + this.renderMessage( localize('updateTooltip.disabledMissingConfig', "Updates are disabled because no update URL is configured."), Codicon.info); break; case DisablementReason.InvalidConfiguration: - this.showMessage( + this.renderMessage( localize('updateTooltip.disabledInvalidConfig', "Updates are disabled because the update URL is invalid."), Codicon.error); break; case DisablementReason.RunningAsAdmin: - this.showMessage( + this.renderMessage( localize( 'updateTooltip.disabledRunningAsAdmin', "Updates are not available when running a user install of {0} as administrator.", @@ -226,7 +255,7 @@ export class UpdateTooltip extends Disposable { Codicon.warning); break; default: - this.showMessage(localize('updateTooltip.disabledGeneric', "Updates are disabled."), Codicon.warning); + this.renderMessage(localize('updateTooltip.disabledGeneric', "Updates are disabled."), Codicon.warning); break; } } @@ -234,34 +263,34 @@ export class UpdateTooltip extends Disposable { private renderIdle({ error, notAvailable }: Idle) { if (error) { this.renderTitleAndInfo(localize('updateTooltip.updateErrorTitle', "Update Error")); - this.showMessage(error, Codicon.error); + this.renderMessage(error, Codicon.error); return; } if (notAvailable) { this.renderTitleAndInfo(localize('updateTooltip.noUpdateAvailableTitle', "No Update Available")); - this.showMessage(localize('updateTooltip.noUpdateAvailableMessage', "There are no updates currently available."), Codicon.info); + this.renderMessage(localize('updateTooltip.noUpdateAvailableMessage', "There are no updates currently available."), Codicon.info); return; } this.renderTitleAndInfo(localize('updateTooltip.upToDateTitle', "Up to Date")); switch (this.configurationService.getValue('update.mode')) { case 'none': - this.showMessage(localize('updateTooltip.autoUpdateNone', "Automatic updates are disabled."), Codicon.warning); + this.renderMessage(localize('updateTooltip.autoUpdateNone', "Automatic updates are disabled."), Codicon.warning); break; case 'manual': - this.showMessage(localize('updateTooltip.autoUpdateManual', "Automatic updates will be checked but not installed automatically.")); + this.renderMessage(localize('updateTooltip.autoUpdateManual', "Automatic updates will be checked but not installed automatically.")); break; case 'start': - this.showMessage(localize('updateTooltip.autoUpdateStart', "Updates will be applied on restart.")); + this.renderMessage(localize('updateTooltip.autoUpdateStart', "Updates will be applied on restart.")); break; case 'default': if (this.meteredConnectionService.isConnectionMetered) { - this.showMessage( + this.renderMessage( localize('updateTooltip.meteredConnectionMessage', "Automatic updates are paused because the network connection is metered."), Codicon.radioTower); } else { - this.showMessage( + this.renderMessage( localize('updateTooltip.autoUpdateDefault', "Automatic updates are enabled. Happy Coding!"), Codicon.smiley); } @@ -271,11 +300,14 @@ export class UpdateTooltip extends Disposable { private renderCheckingForUpdates() { this.renderTitleAndInfo(localize('updateTooltip.checkingForUpdatesTitle', "Checking for Updates")); - this.showMessage(localize('updateTooltip.checkingPleaseWait', "Checking for updates, please wait...")); + this.renderMessage(localize('updateTooltip.checkingPleaseWait', "Checking for updates, please wait...")); } private renderAvailableForDownload({ update }: AvailableForDownload) { this.renderTitleAndInfo(localize('updateTooltip.updateAvailableTitle', "Update Available"), update); + if (this.hostedByTitleBar) { + this.renderMessage(localize('updateTooltip.clickToDownload', "Click the Update button to download.")); + } } private renderDownloading(state: Downloading) { @@ -301,12 +333,15 @@ export class UpdateTooltip extends Disposable { this.downloadStatsContainer.style.display = ''; } else { - this.showMessage(localize('updateTooltip.downloadingPleaseWait', "Downloading update, please wait...")); + this.renderMessage(localize('updateTooltip.downloadingPleaseWait', "Downloading update, please wait...")); } } private renderDownloaded({ update }: Downloaded) { this.renderTitleAndInfo(localize('updateTooltip.updateReadyTitle', "Update is Ready to Install"), update); + if (this.hostedByTitleBar) { + this.renderMessage(localize('updateTooltip.clickToInstall', "Click the Update button to install.")); + } } private renderUpdating({ update, currentProgress, maxProgress }: Updating) { @@ -319,17 +354,61 @@ export class UpdateTooltip extends Disposable { this.progressSizeNode.textContent = ''; this.progressContainer.style.display = ''; } else { - this.showMessage(localize('updateTooltip.installingPleaseWait', "Installing update, please wait...")); + this.renderMessage(localize('updateTooltip.installingPleaseWait', "Installing update, please wait...")); } } private renderReady({ update }: Ready) { this.renderTitleAndInfo(localize('updateTooltip.updateInstalledTitle', "Update Installed"), update); + if (this.hostedByTitleBar) { + this.renderMessage(localize('updateTooltip.clickToRestart', "Click the Update button to restart and apply.")); + } } private renderOverwriting({ update }: Overwriting) { this.renderTitleAndInfo(localize('updateTooltip.downloadingNewerUpdateTitle', "Downloading Newer Update"), update); - this.showMessage(localize('updateTooltip.downloadingNewerPleaseWait', "A newer update was released. Downloading, please wait...")); + this.renderMessage(localize('updateTooltip.downloadingNewerPleaseWait', "A newer update was released. Downloading, please wait...")); + } + + public async renderPostInstall() { + this.hideAll(); + this.renderTitleAndInfo(localize('updateTooltip.installedDefaultTitle', "New Update Installed")); + this.renderMessage( + localize('updateTooltip.installedDefaultMessage', "See release notes for details on what's new in this release."), + Codicon.info); + + let text = null; + try { + const url = getUpdateInfoUrl(this.productService.version); + const context = await this.requestService.request({ url, callSite: 'updateTooltip' }, CancellationToken.None); + text = await asTextOrError(context); + } catch { } + + if (!text) { + return; + } + + this.titleNode.textContent = localize('updateTooltip.installedTitle', "New in {0}", this.productService.version); + this.productInfoNode.style.display = 'none'; + this.messageNode.style.display = 'none'; + + const rendered = this.markdownRendererService.render( + new MarkdownString(text, { + isTrusted: true, + supportHtml: true, + supportThemeIcons: true, + }), + { + actionHandler: (link, mdStr) => { + openLinkFromMarkdown(this.openerService, link, mdStr.isTrusted); + this.hoverService.hideHover(true); + }, + }); + + this.markdown.value = rendered; + dom.clearNode(this.markdownContainer); + this.markdownContainer.appendChild(rendered.element); + this.markdownContainer.style.display = ''; } private renderTitleAndInfo(title: string, update?: IUpdate) { @@ -342,9 +421,10 @@ export class UpdateTooltip extends Disposable { this.latestVersionNode.textContent = updateCommitId ? localize('updateTooltip.latestVersionLabelWithCommit', "Latest Version: {0} ({1})", version, updateCommitId) : localize('updateTooltip.latestVersionLabel', "Latest Version: {0}", version); - this.latestVersionNode.style.display = ''; + this.latestVersionCopyValue.value = updateCommitId ? `${version} (${update.version})` : version; + this.latestVersionNode.parentElement!.style.display = ''; } else { - this.latestVersionNode.style.display = 'none'; + this.latestVersionNode.parentElement!.style.display = 'none'; } // Release date @@ -361,7 +441,7 @@ export class UpdateTooltip extends Disposable { this.releaseNotesLink.style.display = this.releaseNotesVersion ? '' : 'none'; } - private showMessage(message: string, icon?: ThemeIcon) { + private renderMessage(message: string, icon?: ThemeIcon) { dom.clearNode(this.messageNode); if (icon) { const iconNode = dom.append(this.messageNode, dom.$('.state-message-icon')); @@ -371,6 +451,31 @@ export class UpdateTooltip extends Disposable { this.messageNode.style.display = ''; } + private createVersionRow(parent: HTMLElement): { label: HTMLElement; copyValue: { value: string } } { + const row = dom.append(parent, dom.$('.product-version')); + const label = dom.append(row, dom.$('span')); + const copyValue = { value: '' }; + + const copyButton = dom.append(row, dom.$('a.copy-version-button')); + copyButton.setAttribute('role', 'button'); + copyButton.setAttribute('tabindex', '0'); + const title = localize('updateTooltip.copyVersion', "Copy"); + copyButton.title = title; + copyButton.setAttribute('aria-label', title); + + const copyIcon = dom.append(copyButton, dom.$('.copy-icon')); + copyIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.copy)); + this._register(dom.addDisposableListener(copyButton, 'click', e => { + e.preventDefault(); + e.stopPropagation(); + if (copyValue.value) { + this.clipboardService.writeText(copyValue.value); + } + })); + + return { label, copyValue }; + } + private runCommandAndClose(command: string, ...args: unknown[]) { this.commandService.executeCommand(command, ...args); this.hoverService.hideHover(true); diff --git a/src/vs/workbench/contrib/update/common/updateUtils.ts b/src/vs/workbench/contrib/update/common/updateUtils.ts index 3060873d29e..ae1c66e8812 100644 --- a/src/vs/workbench/contrib/update/common/updateUtils.ts +++ b/src/vs/workbench/contrib/update/common/updateUtils.ts @@ -216,3 +216,12 @@ export function preprocessError(error?: string): string | undefined { 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information' ); } + +/** + * Determines whether there is a major or minor version change between two versions. + */ +export function isMajorMinorVersionChange(previousVersion?: string, newVersion?: string): boolean { + const previous = tryParseVersion(previousVersion); + const current = tryParseVersion(newVersion); + return !!previous && !!current && (previous.major !== current.major || previous.minor !== current.minor); +} diff --git a/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts b/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts index dddde3072ee..6e6f8b33589 100644 --- a/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts +++ b/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; @@ -11,7 +12,7 @@ import { TestInstantiationService } from '../../../../../platform/instantiation/ import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { SimpleSettingRenderer } from '../../../markdown/browser/markdownSettingRenderer.js'; import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; -import { renderReleaseNotesMarkdown } from '../../browser/releaseNotesEditor.js'; +import { processConditionalBlocks, renderReleaseNotesMarkdown } from '../../browser/releaseNotesEditor.js'; import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; @@ -103,3 +104,128 @@ Navigation End --> await assertSnapshot(result.toString()); }); }); + +suite('Conditional blocks', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('IN_PRODUCT block is revealed when IN_PRODUCT is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT'])); + assert.ok(result.includes('in-product content')); + assert.ok(!result.includes('%IF')); + assert.ok(result.includes('before')); + assert.ok(result.includes('after')); + }); + + test('WEB block is removed when only IN_PRODUCT is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT'])); + assert.ok(!result.includes('web-only content')); + assert.ok(result.includes('before')); + assert.ok(result.includes('after')); + }); + + test('STABLE block is revealed when STABLE is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'STABLE'])); + assert.ok(result.includes('stable content')); + assert.ok(!result.includes('%IF')); + }); + + test('STABLE block is removed when INSIDERS is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'INSIDERS'])); + assert.ok(!result.includes('stable content')); + assert.ok(result.includes('before')); + assert.ok(result.includes('after')); + }); + + test('INSIDERS block is revealed when INSIDERS is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'INSIDERS'])); + assert.ok(result.includes('insiders content')); + assert.ok(!result.includes('%IF')); + }); + + test('INSIDERS block is removed when STABLE is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'STABLE'])); + assert.ok(!result.includes('insiders content')); + }); + + test('Conditions are case-insensitive', () => { + const text = ''; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT'])); + assert.ok(result.includes('content')); + assert.ok(!result.includes('%IF')); + }); + + test('Multiple conditional blocks in same document', () => { + const text = [ + 'shared content', + '', + '', + '', + '', + 'more shared content', + ].join('\n'); + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'STABLE'])); + assert.ok(result.includes('shared content')); + assert.ok(result.includes('in-product only')); + assert.ok(!result.includes('web only')); + assert.ok(result.includes('stable only')); + assert.ok(!result.includes('insiders only')); + assert.ok(result.includes('more shared content')); + }); + + test('renderReleaseNotesMarkdown passes stable quality correctly', async function () { + const instantiationService = store.add(new TestInstantiationService()); + const extensionService = instantiationService.get(IExtensionService); + const languageService = instantiationService.get(ILanguageService); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); + + const content = [ + '## Title', + '', + '', + ].join('\n'); + const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer), 'stable'); + const html = result.toString(); + assert.ok(html.includes('stable content')); + assert.ok(!html.includes('insiders content')); + }); + + test('renderReleaseNotesMarkdown passes insider quality correctly', async function () { + const instantiationService = store.add(new TestInstantiationService()); + const extensionService = instantiationService.get(IExtensionService); + const languageService = instantiationService.get(ILanguageService); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); + + const content = [ + '## Title', + '', + '', + ].join('\n'); + const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer), 'insider'); + const html = result.toString(); + assert.ok(!html.includes('stable content')); + assert.ok(html.includes('insiders content')); + }); +}); diff --git a/src/vs/workbench/contrib/update/test/common/updateUtils.test.ts b/src/vs/workbench/contrib/update/test/common/updateUtils.test.ts index cfeb6123415..b6d7cf0ff9b 100644 --- a/src/vs/workbench/contrib/update/test/common/updateUtils.test.ts +++ b/src/vs/workbench/contrib/update/test/common/updateUtils.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Downloading, StateType } from '../../../../../platform/update/common/update.js'; -import { computeDownloadSpeed, computeDownloadTimeRemaining, computeProgressPercent, computeUpdateInfoVersion, formatBytes, formatDate, formatTimeRemaining, getUpdateInfoUrl, tryParseDate } from '../../common/updateUtils.js'; +import { computeDownloadSpeed, computeDownloadTimeRemaining, computeProgressPercent, computeUpdateInfoVersion, formatBytes, formatDate, formatTimeRemaining, getUpdateInfoUrl, isMajorMinorVersionChange, tryParseDate } from '../../common/updateUtils.js'; suite('UpdateUtils', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -245,4 +245,39 @@ suite('UpdateUtils', () => { assert.ok(result.includes('2024')); }); }); + + suite('isMajorMinorVersionChange', () => { + test('returns true for major version change', () => { + assert.strictEqual(isMajorMinorVersionChange('1.90.0', '2.0.0'), true); + }); + + test('returns true for minor version change', () => { + assert.strictEqual(isMajorMinorVersionChange('1.90.0', '1.91.0'), true); + }); + + test('returns false for patch-only change', () => { + assert.strictEqual(isMajorMinorVersionChange('1.90.0', '1.90.1'), false); + }); + + test('returns false for identical versions', () => { + assert.strictEqual(isMajorMinorVersionChange('1.90.0', '1.90.0'), false); + }); + + test('returns false when previous version is undefined', () => { + assert.strictEqual(isMajorMinorVersionChange(undefined, '1.90.0'), false); + }); + + test('returns false when new version is undefined', () => { + assert.strictEqual(isMajorMinorVersionChange('1.90.0', undefined), false); + }); + + test('returns false when both versions are undefined', () => { + assert.strictEqual(isMajorMinorVersionChange(undefined, undefined), false); + }); + + test('returns false for unparseable versions', () => { + assert.strictEqual(isMajorMinorVersionChange('invalid', '1.90.0'), false); + assert.strictEqual(isMajorMinorVersionChange('1.90.0', 'invalid'), false); + }); + }); }); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 1368ec8f482..dfbb6b0fb95 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -26,7 +26,7 @@ import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/edit import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEditorInputSerializer } from './userDataProfilesEditor.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IUserDataProfilesEditor } from '../common/userDataProfile.js'; @@ -107,7 +107,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } private async openProfilesEditor(): Promise { - const editor = await this.editorService.openEditor(new UserDataProfilesEditorInput(this.instantiationService), undefined, MODAL_GROUP); + const editor = await this.editorService.openEditor(new UserDataProfilesEditorInput(this.instantiationService)); return editor as IUserDataProfilesEditor; } @@ -388,7 +388,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements run(accessor: ServicesAccessor) { const editorService = accessor.get(IEditorService); const instantiationService = accessor.get(IInstantiationService); - return editorService.openEditor(new UserDataProfilesEditorInput(instantiationService), undefined, MODAL_GROUP); + return editorService.openEditor(new UserDataProfilesEditorInput(instantiationService)); } })); disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index 5898c8d6867..bf4ac12e905 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -16,7 +16,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IUserDataProfile, IUserDataProfilesService, ProfileResourceType } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; -import { IEditorOpenContext, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js'; +import { EditorInputCapabilities, IEditorOpenContext, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { IUserDataProfilesEditor } from '../common/userDataProfile.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; @@ -2251,6 +2251,10 @@ export class UserDataProfilesEditorInput extends EditorInput { } } + override get capabilities(): EditorInputCapabilities { + return EditorInputCapabilities.RequiresModal; + } + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, ) { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 9e6ca135e08..0f10d3f8f27 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -37,7 +37,7 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../chat/c import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ChatWidget } from '../../chat/browser/widget/chatWidget.js'; import { IAgentSessionsService } from '../../chat/browser/agentSessions/agentSessionsService.js'; -import { AgentSessionProviders } from '../../chat/browser/agentSessions/agentSessions.js'; +import { AgentSessionProviders, AgentSessionTarget } from '../../chat/browser/agentSessions/agentSessions.js'; import { IAgentSession } from '../../chat/browser/agentSessions/agentSessionsModel.js'; import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput, AgentSessionsWelcomeWorkspaceKind } from './agentSessionsWelcomeInput.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; @@ -132,7 +132,7 @@ export class AgentSessionsWelcomePage extends EditorPane { private readonly contentDisposables = this._register(new DisposableStore()); private contextService: IContextKeyService; private walkthroughs: IResolvedWalkthrough[] = []; - private _selectedSessionProvider: AgentSessionProviders = AgentSessionProviders.Local; + private _selectedSessionProvider: AgentSessionTarget = AgentSessionProviders.Local; private _selectedWorkspace: IWorkspacePickerItem | undefined; private _recentTrustedWorkspaces: Array = []; private _isEmptyWorkspace: boolean = false; @@ -313,8 +313,8 @@ export class AgentSessionsWelcomePage extends EditorPane { const scopedInstantiationService = this.contentDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))); // Create a delegate for the session target picker with independent local state - const onDidChangeActiveSessionProvider = this.contentDisposables.add(new Emitter()); - const recreateSessionForProvider = async (provider: AgentSessionProviders) => { + const onDidChangeActiveSessionProvider = this.contentDisposables.add(new Emitter()); + const recreateSessionForProvider = async (provider: AgentSessionTarget) => { if (this.chatWidget && this.chatModelRef) { this.chatWidget.setModel(undefined); this.chatModelRef.dispose(); @@ -333,7 +333,7 @@ export class AgentSessionsWelcomePage extends EditorPane { }; const sessionTypePickerDelegate: ISessionTypePickerDelegate = { getActiveSessionProvider: () => this._selectedSessionProvider, - setActiveSessionProvider: (provider: AgentSessionProviders) => { + setActiveSessionProvider: (provider: AgentSessionTarget) => { this._selectedSessionProvider = provider; onDidChangeActiveSessionProvider.fire(provider); try { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css index a28e8a83592..d4bc58c5786 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css @@ -40,6 +40,7 @@ .agentSessionsWelcome-header h1.product-name { font-size: 32px; font-weight: 400; + line-height: 1; margin: 0 0 12px 0; color: var(--vscode-foreground); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 1159e4ce41e..d13453552f1 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -17,7 +17,7 @@ import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWo import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; @@ -747,7 +747,7 @@ registerAction2(class extends Action2 { const input = instantiationService.createInstance(WorkspaceTrustEditorInput); - editorService.openEditor(input, { pinned: true }, MODAL_GROUP); + editorService.openEditor(input, { pinned: true }); return; } }); diff --git a/src/vs/workbench/services/accounts/browser/defaultAccount.ts b/src/vs/workbench/services/accounts/browser/defaultAccount.ts index 3e2724e96a5..2992998a9c3 100644 --- a/src/vs/workbench/services/accounts/browser/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/browser/defaultAccount.ts @@ -644,7 +644,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } this.logService.debug('[DefaultAccount] Fetching token entitlements from:', tokenEntitlementsUrl); - const response = await this.request(tokenEntitlementsUrl, 'GET', undefined, sessions, CancellationToken.None); + const response = await this.request(tokenEntitlementsUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.tokenEntitlements'); if (!response) { return undefined; } @@ -688,7 +688,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } this.logService.debug('[DefaultAccount] Fetching entitlements from:', entitlementUrl); - const response = await this.request(entitlementUrl, 'GET', undefined, sessions, CancellationToken.None); + const response = await this.request(entitlementUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.entitlements'); if (!response) { return undefined; } @@ -731,7 +731,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } this.logService.debug('[DefaultAccount] Fetching MCP registry data from:', mcpRegistryDataUrl); - const response = await this.request(mcpRegistryDataUrl, 'GET', undefined, sessions, CancellationToken.None); + const response = await this.request(mcpRegistryDataUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.mcpRegistryProvider'); if (!response) { return undefined; } @@ -759,9 +759,9 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid } } - private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise; - private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken): Promise; - private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise { + private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise; + private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise; + private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise { let lastResponse: IRequestContext | undefined; for (const session of sessions) { @@ -777,7 +777,8 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid disableCache: true, headers: { 'Authorization': `Bearer ${session.accessToken}` - } + }, + callSite }, token); const status = response.res.statusCode; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index e9e6d9d2cca..1a80b0b1e3e 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -490,6 +490,18 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.chatEditingSessionApplySubmenu', "Submenu for apply actions in the Chat Editing session changes toolbar."), proposed: 'chatSessionsProvider' }, + { + key: 'chat/input/editing/sessionTitleToolbar', + id: MenuId.ChatEditingSessionTitleToolbar, + description: localize('menus.chatEditingSessionTitleToolbar', "The Chat Editing widget toolbar menu for session title."), + proposed: 'chatSessionsProvider' + }, + { + key: 'chat/input/editing/sessionChangeToolbar', + id: MenuId.ChatEditingSessionChangeToolbar, + description: localize('menus.chatEditingSessionChangeToolbar', "The Chat Editing widget toolbar menu for session changes."), + proposed: 'chatSessionsProvider' + }, { // TODO: rename this to something like: `chatSessions/item/inline` key: 'chat/chatSessions', diff --git a/src/vs/workbench/services/browserElements/browser/browserElementsService.ts b/src/vs/workbench/services/browserElements/browser/browserElementsService.ts index 0f56a1edf3a..1a94f8a529c 100644 --- a/src/vs/workbench/services/browserElements/browser/browserElementsService.ts +++ b/src/vs/workbench/services/browserElements/browser/browserElementsService.ts @@ -16,6 +16,8 @@ export interface IBrowserElementsService { // no browser implementation yet getElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator | undefined): Promise; + getFocusedElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator | undefined): Promise; + startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise; startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise; diff --git a/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts b/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts index a98c4de1ba5..614ffc444f9 100644 --- a/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts +++ b/src/vs/workbench/services/browserElements/browser/webBrowserElementsService.ts @@ -18,6 +18,10 @@ class WebBrowserElementsService implements IBrowserElementsService { throw new Error('Not implemented'); } + async getFocusedElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator | undefined): Promise { + throw new Error('Not implemented'); + } + async startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/services/browserElements/electron-browser/browserElementsService.ts b/src/vs/workbench/services/browserElements/electron-browser/browserElementsService.ts index fa2ddc77288..1d88e15309b 100644 --- a/src/vs/workbench/services/browserElements/electron-browser/browserElementsService.ts +++ b/src/vs/workbench/services/browserElements/electron-browser/browserElementsService.ts @@ -90,6 +90,22 @@ class WorkbenchBrowserElementsService implements IBrowserElementsService { disposable.dispose(); } } + + async getFocusedElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator | undefined): Promise { + if (!locator) { + return undefined; + } + const cancelSelectionId = cancelSelectionIdPool++; + const onCancelChannel = `vscode:cancelElementSelection${cancelSelectionId}`; + const disposable = token.onCancellationRequested(() => { + ipcRenderer.send(onCancelChannel, cancelSelectionId); + }); + try { + return await this.simpleBrowser.getFocusedElementData(rect, token, locator, cancelSelectionId); + } finally { + disposable.dispose(); + } + } } registerSingleton(IBrowserElementsService, WorkbenchBrowserElementsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index 919d79dc291..02c83ae04b9 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -772,9 +772,9 @@ export class ChatEntitlementRequests extends Disposable { return quotas; } - private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise; - private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken): Promise; - private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise { + private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise; + private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise; + private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string): Promise { let lastRequest: IRequestContext | undefined; for (const session of sessions) { @@ -790,7 +790,8 @@ export class ChatEntitlementRequests extends Disposable { disableCache: true, headers: { 'Authorization': `Bearer ${session.accessToken}` - } + }, + callSite }, token); const status = response.res.statusCode; @@ -843,7 +844,7 @@ export class ChatEntitlementRequests extends Disposable { public_code_suggestions: 'enabled' }; - const response = await this.request(defaultChatAgent.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None); + const response = await this.request(defaultChatAgent.entitlementSignupLimitedUrl, 'POST', body, sessions, CancellationToken.None, 'chatEntitlementService.signUpFree'); if (!response) { const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat entitlement] sign-up: no response'); return retry ? this.doSignUpFree(sessions) : { errorCode: 1 }; diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index 15f7c61d9b6..ac9cd324172 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -82,6 +82,7 @@ export enum VariableKind { Command = 'command', Input = 'input', ExtensionInstallFolder = 'extensionInstallFolder', + TaskVar = 'taskVar', WorkspaceFolder = 'workspaceFolder', Cwd = 'cwd', diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 76db1269702..dfc94868782 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -706,6 +706,20 @@ suite('Configuration Resolver Service', () => { }); }); + test('contributed taskVar variable', () => { + const url = 'http://localhost:5678'; + const variable = 'taskVar:componentExplorerUrl'; + const configuration = { + 'url': '${taskVar:componentExplorerUrl}/___explorer', + }; + configurationResolverService!.contributeVariable(variable, async () => { return url; }); + return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration).then(result => { + assert.deepStrictEqual({ ...result }, { + 'url': `${url}/___explorer` + }); + }); + }); + test('resolveWithEnvironment', async () => { const env = { 'VAR_1': 'VAL_1', diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 6300496bd4e..8786b4ed5f6 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -229,7 +229,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const title = nls.localize('openFileOrFolder.title', 'Open File or Folder'); const availableFileSystems = this.addFileSchemaIfNeeded(schema); - const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); + const uris = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); + const uri = uris?.[0]; if (uri) { const stat = await this.fileService.stat(uri); @@ -251,7 +252,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const title = nls.localize('openFile.title', 'Open File'); const availableFileSystems = this.addFileSchemaIfNeeded(schema); - const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); + const uris = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); + const uri = uris?.[0]; if (uri) { this.addFileToRecentlyOpened(uri); @@ -271,7 +273,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const title = nls.localize('openFolder.title', 'Open Folder'); const availableFileSystems = this.addFileSchemaIfNeeded(schema, true); - const uri = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); + const uris = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }); + const uri = uris?.[0]; if (uri) { return this.hostService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority }); } @@ -282,7 +285,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }]; const availableFileSystems = this.addFileSchemaIfNeeded(schema, true); - const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }); + const uris = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }); + const uri = uris?.[0]; if (uri) { return this.hostService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority }); } @@ -316,16 +320,14 @@ export abstract class AbstractFileDialogService implements IFileDialogService { options.availableFileSystems = this.addFileSchemaIfNeeded(schema, options.canSelectFolders); } - const uri = await this.pickResource(options); - - return uri ? [uri] : undefined; + return this.pickResource(options); } protected getSimpleFileDialog(): ISimpleFileDialog { return this.instantiationService.createInstance(SimpleFileDialog); } - private pickResource(options: IOpenDialogOptions): Promise { + private pickResource(options: IOpenDialogOptions): Promise { return this.getSimpleFileDialog().showOpenDialog(options); } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 65b1d6b58ce..3a269eae761 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -105,7 +105,7 @@ enum UpdateResult { export const RemoteFileDialogContext = new RawContextKey('remoteFileDialogVisible', false); export interface ISimpleFileDialog extends IDisposable { - showOpenDialog(options: IOpenDialogOptions): Promise; + showOpenDialog(options: IOpenDialogOptions): Promise; showSaveDialog(options: ISaveDialogOptions): Promise; } @@ -130,6 +130,23 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { private badPath: string | undefined; private remoteAgentEnvironment: IRemoteAgentEnvironment | null | undefined; private separator: string = '/'; + + /** + * When set, the dialog is scoped to a specific URI authority (e.g. + * for browsing an `agenthost://{authority}/...` filesystem that + * uses per-connection authorities rather than the global + * {@link remoteAuthority}). + */ + private scopedAuthority: string | undefined; + /** + * Path prefix that the label formatter strips from URIs in the + * scoped scheme (e.g. `/file/-` for agent host URIs that encode + * the original scheme and authority as leading path segments). + * + * Stripped by {@link pathFromUri} and re-applied by + * {@link remoteUriFrom} so the user sees clean paths. + */ + private scopedPathPrefix: string = ''; private readonly onBusyChangeEmitter = this._register(new Emitter()); private updatingPromise: CancelablePromise | undefined; @@ -189,8 +206,10 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { return this.filePickBox.busy; } - public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { + public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); + this.scopedAuthority = this.getScopedAuthority(options.defaultUri); + this.scopedPathPrefix = options.defaultUri && this.scopedAuthority ? this.computeScopedPathPrefix(options.defaultUri) : ''; this.userHome = await this.getUserHome(); this.trueHome = await this.getUserHome(true); const newOptions = this.getOptions(options); @@ -198,11 +217,17 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { return Promise.resolve(undefined); } this.options = newOptions; - return this.pickResource(); + const result = await this.pickResource(); + if (Array.isArray(result)) { + return result; + } + return result ? [result] : undefined; } public async showSaveDialog(options: ISaveDialogOptions): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); + this.scopedAuthority = this.getScopedAuthority(options.defaultUri); + this.scopedPathPrefix = options.defaultUri && this.scopedAuthority ? this.computeScopedPathPrefix(options.defaultUri) : ''; this.userHome = await this.getUserHome(); this.trueHome = await this.getUserHome(true); this.requiresTrailing = true; @@ -215,8 +240,8 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { this.options.canSelectFiles = true; return new Promise((resolve) => { - this.pickResource(true).then(folderUri => { - resolve(folderUri); + this.pickResource(true).then(result => { + resolve(Array.isArray(result) ? result[0] : result); }); }); } @@ -247,6 +272,13 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { if (!path.startsWith('\\\\')) { path = path.replace(/\\/g, '/'); } + // When scoped to a specific authority (e.g. agenthost://host/...), + // construct the URI directly with the authority to avoid + // toLocalResource stripping or replacing it. + // Re-add the scopedPathPrefix that was stripped in pathFromUri. + if (this.scopedAuthority) { + return URI.from({ scheme: this.scheme, authority: this.scopedAuthority, path: this.scopedPathPrefix + path, query: hintUri?.query, fragment: hintUri?.fragment }); + } const uri: URI = this.scheme === Schemas.file ? URI.file(path) : URI.from({ scheme: this.scheme, path, query: hintUri?.query, fragment: hintUri?.fragment }); // If the default scheme is file, then we don't care about the remote authority or the hint authority const authority = (uri.scheme === Schemas.file) ? undefined : (this.remoteAuthority ?? hintUri?.authority); @@ -268,6 +300,41 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { return Schemas.file; } + /** + * Returns the per-URI authority from {@link defaultUri} if the dialog + * should be scoped to a specific authority (e.g. `agenthost://host/...`). + * + * Returns `undefined` when the authority matches the global + * {@link remoteAuthority} (standard SSH remotes), since that path is + * already handled by the existing logic. + */ + private getScopedAuthority(defaultUri: URI | undefined): string | undefined { + if (defaultUri + && defaultUri.scheme === this.scheme + && defaultUri.authority + && defaultUri.authority !== this.remoteAuthority) { + return defaultUri.authority; + } + return undefined; + } + + /** + * Computes the path prefix that the label formatter strips for the + * scoped scheme, by comparing the raw URI path with the label + * service's formatted output. + * + * For example, an agent host URI with path `/file/-/Users/roblou` + * formats as `/Users/roblou`, so the prefix is `/file/-`. + */ + private computeScopedPathPrefix(uri: URI): string { + const fullPath = uri.path; + const displayPath = this.labelService.getUriLabel(uri); + if (displayPath && fullPath.endsWith(displayPath)) { + return fullPath.substring(0, fullPath.length - displayPath.length); + } + return ''; + } + private async getRemoteAgentEnvironment(): Promise { if (this.remoteAgentEnvironment === undefined) { this.remoteAgentEnvironment = await this.remoteAgentService.getEnvironment(); @@ -276,17 +343,31 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { } protected getUserHome(trueHome = false): Promise { + // When scoped to a custom authority, the platform userHome is not + // meaningful (it would return a local file:// path). Use the root + // of the scoped filesystem as the home directory instead. + // Include the scopedPathPrefix so the URI resolves correctly. + if (this.scopedAuthority) { + return Promise.resolve(URI.from({ scheme: this.scheme, authority: this.scopedAuthority, path: this.scopedPathPrefix + '/' })); + } return trueHome ? this.pathService.userHome({ preferLocal: this.scheme === Schemas.file }) : this.fileDialogService.preferredHome(this.scheme); } - private async pickResource(isSave: boolean = false): Promise { + private normalizeUri(uri: URI): URI { + uri = resources.addTrailingPathSeparator(uri, this.separator); // Ensures that c: is c:/ since this comes from user input and can be incorrect. + // To be consistent, we should never have a trailing path separator on directories (or anything else). Will not remove from c:/. + uri = resources.removeTrailingPathSeparator(uri); + return uri; + } + + private async pickResource(isSave: boolean = false): Promise { this.allowFolderSelection = !!this.options.canSelectFolders; this.allowFileSelection = !!this.options.canSelectFiles; - this.separator = this.labelService.getSeparator(this.scheme, this.remoteAuthority); + this.separator = this.scopedAuthority ? '/' : this.labelService.getSeparator(this.scheme, this.remoteAuthority); this.hidden = false; - this.isWindows = await this.checkIsWindowsOS(); + this.isWindows = this.scopedAuthority ? false : await this.checkIsWindowsOS(); let homedir: URI = this.options.defaultUri ? this.options.defaultUri : this.workspaceContextService.getWorkspace().folders[0].uri; let stat: IFileStatWithPartialMetadata | undefined; const ext: string = resources.extname(homedir); @@ -302,7 +383,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { } } - return new Promise((resolve) => { + return new Promise((resolve) => { this.filePickBox = this._register(this.quickInputService.createQuickPick()); this.busy = true; this.filePickBox.matchOnLabel = false; @@ -345,13 +426,15 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { this.filePickBox.value = this.pathFromUri(this.currentFolder, true); this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; - const doResolve = (uri: URI | undefined) => { - if (uri) { - uri = resources.addTrailingPathSeparator(uri, this.separator); // Ensures that c: is c:/ since this comes from user input and can be incorrect. - // To be consistent, we should never have a trailing path separator on directories (or anything else). Will not remove from c:/. - uri = resources.removeTrailingPathSeparator(uri); + const doResolve = (uriOrUris: URI | URI[] | undefined) => { + if (uriOrUris) { + if (Array.isArray(uriOrUris)) { + uriOrUris = uriOrUris.map(uri => this.normalizeUri(uri)); + } else { + uriOrUris = this.normalizeUri(uriOrUris); + } } - resolve(uri); + resolve(uriOrUris); this.contextKey.set(false); this.dispose(); }; @@ -373,7 +456,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { }); } else { return this.fileDialogService.showOpenDialog(this.options).then(result => { - doResolve(result ? result[0] : undefined); + doResolve(result); }); } })); @@ -970,7 +1053,20 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { } private pathFromUri(uri: URI, endWithSeparator: boolean = false): string { - let result: string = normalizeDriveLetter(uri.fsPath, this.isWindows).replace(/\n/g, ''); + // For authority-scoped schemes, use the raw path component instead + // of fsPath, which would prepend the authority as a UNC prefix. + // Strip the scopedPathPrefix so the user sees clean paths + // (e.g. `/Users/roblou/code` instead of `/file/-/Users/roblou/code`). + let result: string; + if (this.scopedAuthority) { + let path = uri.path; + if (this.scopedPathPrefix && path.startsWith(this.scopedPathPrefix)) { + path = path.substring(this.scopedPathPrefix.length); + } + result = path.replace(/\n/g, ''); + } else { + result = normalizeDriveLetter(uri.fsPath, this.isWindows).replace(/\n/g, ''); + } if (this.separator === '/') { result = result.replace(/\\/g, this.separator); } else { @@ -1011,7 +1107,19 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { } private async createBackItem(currFolder: URI): Promise { - const fileRepresentationCurr = this.currentFolder.with({ scheme: Schemas.file, authority: '' }); + // For authority-scoped URIs with a path prefix, treat the prefix + // root as the filesystem root so the user cannot navigate above it. + if (this.scopedPathPrefix) { + const pathAfterPrefix = currFolder.path.substring(this.scopedPathPrefix.length); + if (pathAfterPrefix === '/' || pathAfterPrefix === '') { + return undefined; + } + } + // For authority-scoped URIs, compare within the original scheme so + // that the authority is preserved and the root is detected correctly. + const compareScheme = this.scopedAuthority ? this.scheme : Schemas.file; + const compareAuthority = this.scopedAuthority ?? ''; + const fileRepresentationCurr = this.currentFolder.with({ scheme: compareScheme, authority: compareAuthority }); const fileRepresentationParent = resources.dirname(fileRepresentationCurr); if (!resources.isEqual(fileRepresentationCurr, fileRepresentationParent)) { const parentFolder = resources.dirname(currFolder); diff --git a/src/vs/workbench/services/dialogs/test/browser/simpleFileDialog.test.ts b/src/vs/workbench/services/dialogs/test/browser/simpleFileDialog.test.ts new file mode 100644 index 00000000000..c4ba94eb142 --- /dev/null +++ b/src/vs/workbench/services/dialogs/test/browser/simpleFileDialog.test.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { AGENT_HOST_SCHEME, AGENT_HOST_LABEL_FORMATTER, agentHostAuthority } from '../../../../../platform/agentHost/common/agentHostUri.js'; +import { agentHostUri } from '../../../../../platform/agentHost/common/agentHostFileSystemProvider.js'; + +/** + * Tests for the scoped path prefix logic used by SimpleFileDialog. + * + * SimpleFileDialog is tightly coupled to many services and difficult to + * instantiate in isolation. Instead of mocking the full dialog, we test + * the underlying data transformations that drive the fix: + * + * 1. computeScopedPathPrefix - derived from comparing the raw URI path + * with the label-service-formatted output. + * 2. pathFromUri - stripping the prefix from the raw path. + * 3. remoteUriFrom - re-adding the prefix to user input. + */ +suite('SimpleFileDialog - scoped path prefix', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Replicates the stripPathSegments logic from the label service to + * produce the display path that the label formatter would return. + */ + function labelFormatterDisplay(path: string, stripSegments: number): string { + let pos = 0; + for (let i = 0; i < stripSegments; i++) { + const next = path.indexOf('/', pos + 1); + if (next === -1) { + break; + } + pos = next; + } + return path.substring(pos); + } + + /** + * Replicates SimpleFileDialog.computeScopedPathPrefix: + * compares raw URI path with formatted display path to find the prefix. + */ + function computeScopedPathPrefix(uri: URI, displayPath: string): string { + const fullPath = uri.path; + if (displayPath && fullPath.endsWith(displayPath)) { + return fullPath.substring(0, fullPath.length - displayPath.length); + } + return ''; + } + + /** + * Replicates the scoped branch of SimpleFileDialog.pathFromUri: + * strips the prefix from the raw URI path. + */ + function pathFromUri(uri: URI, prefix: string, endWithSeparator: boolean = false): string { + let path = uri.path; + if (prefix && path.startsWith(prefix)) { + path = path.substring(prefix.length); + } + let result = path.replace(/\n/g, ''); + result = result.replace(/\\/g, '/'); + if (endWithSeparator && !result.endsWith('/')) { + result = result + '/'; + } + return result; + } + + /** + * Replicates the scoped branch of SimpleFileDialog.remoteUriFrom: + * re-adds the prefix to construct a proper URI. + */ + function remoteUriFrom(path: string, scheme: string, authority: string, prefix: string): URI { + return URI.from({ scheme, authority, path: prefix + path }); + } + + test('computeScopedPathPrefix extracts prefix for agent host URI', () => { + const authority = agentHostAuthority('localhost:8089'); + const uri = agentHostUri(authority, '/Users/roblou/code'); + + const displayPath = labelFormatterDisplay(uri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!); + const prefix = computeScopedPathPrefix(uri, displayPath); + + assert.strictEqual(prefix, '/file/-'); + assert.strictEqual(displayPath, '/Users/roblou/code'); + }); + + test('computeScopedPathPrefix works for URI with original authority', () => { + const authority = agentHostAuthority('localhost:8089'); + const originalUri = URI.from({ scheme: 'agenthost-content', authority: 'session1', path: '/snap/before' }); + const uri = URI.from({ + scheme: AGENT_HOST_SCHEME, + authority, + path: `/${originalUri.scheme}/${originalUri.authority}${originalUri.path}`, + }); + + const displayPath = labelFormatterDisplay(uri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!); + const prefix = computeScopedPathPrefix(uri, displayPath); + + assert.strictEqual(prefix, '/agenthost-content/session1'); + assert.strictEqual(displayPath, '/snap/before'); + }); + + test('computeScopedPathPrefix returns empty for scheme without stripping', () => { + const uri = URI.from({ scheme: 'file', path: '/Users/roblou/code' }); + // If display matches the full path, prefix is empty + const prefix = computeScopedPathPrefix(uri, '/Users/roblou/code'); + assert.strictEqual(prefix, ''); + }); + + test('pathFromUri strips prefix to show clean path', () => { + const authority = agentHostAuthority('localhost:8089'); + const uri = agentHostUri(authority, '/Users/roblou/code'); + const prefix = '/file/-'; + + assert.strictEqual(pathFromUri(uri, prefix), '/Users/roblou/code'); + }); + + test('pathFromUri with trailing separator', () => { + const authority = agentHostAuthority('localhost:8089'); + const uri = agentHostUri(authority, '/Users/roblou/code'); + const prefix = '/file/-'; + + assert.strictEqual(pathFromUri(uri, prefix, true), '/Users/roblou/code/'); + }); + + test('pathFromUri without prefix returns raw path', () => { + const uri = URI.from({ scheme: 'file', path: '/Users/roblou/code' }); + assert.strictEqual(pathFromUri(uri, ''), '/Users/roblou/code'); + }); + + test('remoteUriFrom re-adds prefix to reconstruct encoded URI', () => { + const authority = agentHostAuthority('localhost:8089'); + const prefix = '/file/-'; + const cleanPath = '/Users/roblou/code'; + + const result = remoteUriFrom(cleanPath, AGENT_HOST_SCHEME, authority, prefix); + + assert.strictEqual(result.scheme, AGENT_HOST_SCHEME); + assert.strictEqual(result.authority, authority); + assert.strictEqual(result.path, '/file/-/Users/roblou/code'); + }); + + test('full round-trip: URI -> pathFromUri -> remoteUriFrom -> same URI', () => { + const authority = agentHostAuthority('localhost:8089'); + const originalPath = '/Users/roblou/code/vscode'; + const uri = agentHostUri(authority, originalPath); + + // Compute prefix + const displayPath = labelFormatterDisplay(uri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!); + const prefix = computeScopedPathPrefix(uri, displayPath); + + // pathFromUri extracts clean path + const cleanPath = pathFromUri(uri, prefix); + assert.strictEqual(cleanPath, originalPath); + + // remoteUriFrom reconstructs the original URI + const reconstructed = remoteUriFrom(cleanPath, AGENT_HOST_SCHEME, authority, prefix); + assert.strictEqual(reconstructed.path, uri.path); + assert.strictEqual(reconstructed.scheme, uri.scheme); + assert.strictEqual(reconstructed.authority, uri.authority); + }); + + test('createBackItem root detection with prefix', () => { + const authority = agentHostAuthority('localhost:8089'); + const prefix = '/file/-'; + + // Simulate root folder: path = prefix + '/' + const rootUri = URI.from({ scheme: AGENT_HOST_SCHEME, authority, path: prefix + '/' }); + const pathAfterPrefix = rootUri.path.substring(prefix.length); + assert.strictEqual(pathAfterPrefix === '/' || pathAfterPrefix === '', true, 'root should be detected'); + + // Simulate non-root folder + const subUri = URI.from({ scheme: AGENT_HOST_SCHEME, authority, path: prefix + '/Users/roblou' }); + const subPathAfterPrefix = subUri.path.substring(prefix.length); + assert.notStrictEqual(subPathAfterPrefix, '/'); + assert.notStrictEqual(subPathAfterPrefix, ''); + }); +}); diff --git a/src/vs/workbench/services/dialogs/test/electron-browser/fileDialogService.test.ts b/src/vs/workbench/services/dialogs/test/electron-browser/fileDialogService.test.ts index 8f55c547ee9..6406763524e 100644 --- a/src/vs/workbench/services/dialogs/test/electron-browser/fileDialogService.test.ts +++ b/src/vs/workbench/services/dialogs/test/electron-browser/fileDialogService.test.ts @@ -89,10 +89,10 @@ suite('FileDialogService', function () { test('Local - open/save workspaces availableFilesystems', async function () { class TestSimpleFileDialog implements ISimpleFileDialog { - async showOpenDialog(options: IOpenDialogOptions): Promise { + async showOpenDialog(options: IOpenDialogOptions): Promise { assert.strictEqual(options.availableFileSystems?.length, 1); assert.strictEqual(options.availableFileSystems[0], Schemas.file); - return testFile; + return [testFile]; } async showSaveDialog(options: ISaveDialogOptions): Promise { assert.strictEqual(options.availableFileSystems?.length, 1); @@ -111,10 +111,10 @@ suite('FileDialogService', function () { test('Virtual - open/save workspaces availableFilesystems', async function () { class TestSimpleFileDialog { - async showOpenDialog(options: IOpenDialogOptions): Promise { + async showOpenDialog(options: IOpenDialogOptions): Promise { assert.strictEqual(options.availableFileSystems?.length, 1); assert.strictEqual(options.availableFileSystems[0], Schemas.file); - return testFile; + return [testFile]; } async showSaveDialog(options: ISaveDialogOptions): Promise { assert.strictEqual(options.availableFileSystems?.length, 1); @@ -137,11 +137,11 @@ suite('FileDialogService', function () { test('Remote - open/save workspaces availableFilesystems', async function () { class TestSimpleFileDialog implements ISimpleFileDialog { - async showOpenDialog(options: IOpenDialogOptions): Promise { + async showOpenDialog(options: IOpenDialogOptions): Promise { assert.strictEqual(options.availableFileSystems?.length, 2); assert.strictEqual(options.availableFileSystems[0], Schemas.vscodeRemote); assert.strictEqual(options.availableFileSystems[1], Schemas.file); - return testFile; + return [testFile]; } async showSaveDialog(options: ISaveDialogOptions): Promise { assert.strictEqual(options.availableFileSystems?.length, 2); @@ -170,8 +170,8 @@ suite('FileDialogService', function () { test('Remote - filters default files/folders to RA (#195938)', async function () { class TestSimpleFileDialog implements ISimpleFileDialog { - async showOpenDialog(): Promise { - return testFile; + async showOpenDialog(): Promise { + return [testFile]; } async showSaveDialog(): Promise { return testFile; diff --git a/src/vs/workbench/services/editor/common/editorGroupFinder.ts b/src/vs/workbench/services/editor/common/editorGroupFinder.ts index e7f6ae7e6bc..16bdc11dcf3 100644 --- a/src/vs/workbench/services/editor/common/editorGroupFinder.ts +++ b/src/vs/workbench/services/editor/common/editorGroupFinder.ts @@ -11,19 +11,21 @@ import { EditorInput } from '../../../common/editor/editorInput.js'; import { IEditorGroup, GroupsOrder, preferredSideBySideGroupDirection, IEditorGroupsService, IModalEditorPart } from './editorGroupsService.js'; import { AUX_WINDOW_GROUP, AUX_WINDOW_GROUP_TYPE, MODAL_GROUP, MODAL_GROUP_TYPE, PreferredGroup, SIDE_GROUP } from './editorService.js'; +type FindGroupResult = Promise<[IEditorGroup, EditorActivation | undefined]> | [IEditorGroup, EditorActivation | undefined]; + /** * Finds the target `IEditorGroup` given the instructions provided * that is best for the editor and matches the preferred group if * possible. */ -export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: Exclude | undefined): [IEditorGroup, EditorActivation | undefined]; -export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: Exclude | undefined): [IEditorGroup, EditorActivation | undefined]; -export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: Exclude | undefined): [IEditorGroup, EditorActivation | undefined]; +export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: Exclude | undefined): FindGroupResult; +export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: Exclude | undefined): FindGroupResult; +export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: Exclude | undefined): FindGroupResult; export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>; export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>; export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>; -export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): Promise<[IEditorGroup, EditorActivation | undefined]> | [IEditorGroup, EditorActivation | undefined]; -export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): Promise<[IEditorGroup, EditorActivation | undefined]> | [IEditorGroup, EditorActivation | undefined] { +export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): FindGroupResult; +export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): FindGroupResult { const editorGroupService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); @@ -35,18 +37,22 @@ export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOpt return handleGroupResult(group, editor, preferredGroup, editorGroupService, configurationService); } -function handleGroupResult(group: IEditorGroup, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined, editorGroupService: IEditorGroupsService, configurationService: IConfigurationService): [IEditorGroup, EditorActivation | undefined] { +function handleGroupResult(group: IEditorGroup, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined, editorGroupService: IEditorGroupsService, configurationService: IConfigurationService): FindGroupResult { const modalEditorPart = editorGroupService.activeModalEditorPart; const modalEditorMode = configurationService.getValue('workbench.editor.useModal'); - if (modalEditorPart && preferredGroup !== MODAL_GROUP && modalEditorMode !== 'all') { + const editorInput = isEditorInputWithOptions(editor) ? editor.editor : isEditorInput(editor) ? editor : undefined; + const requiresModal = editorInput instanceof EditorInput && editorInput.hasCapability(EditorInputCapabilities.RequiresModal); + if (modalEditorPart && preferredGroup !== MODAL_GROUP && modalEditorMode !== 'all' && !requiresModal) { // Only allow to open in modal group if MODAL_GROUP is explicitly requested - group = handleModalEditorPart(group, editor, modalEditorPart, editorGroupService); + // or when the setting is configured to open all editors modal or when the + // editor has the RequiresModal capability. + return handleModalEditorPart(group, editor, modalEditorPart, editorGroupService, preferredGroup); } return handleGroupActivation(group, editor, preferredGroup, editorGroupService); } -function handleModalEditorPart(group: IEditorGroup, editor: EditorInputWithOptions | IUntypedEditorInput, modalEditorPart: IModalEditorPart, editorGroupService: IEditorGroupsService): IEditorGroup { +async function handleModalEditorPart(group: IEditorGroup, editor: EditorInputWithOptions | IUntypedEditorInput, modalEditorPart: IModalEditorPart, editorGroupService: IEditorGroupsService, preferredGroup: PreferredGroup | undefined): Promise<[IEditorGroup, EditorActivation | undefined]> { const options = editor.options; // If the resolved group is part of the modal, redirect @@ -57,10 +63,10 @@ function handleModalEditorPart(group: IEditorGroup, editor: EditorInputWithOptio // Try to close the modal editor part unless preserveFocus is set if (!options?.preserveFocus) { - modalEditorPart.close(); + await modalEditorPart.close(); } - return group; + return handleGroupActivation(group, editor, preferredGroup, editorGroupService); } function handleGroupActivation(group: IEditorGroup, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined, editorGroupService: IEditorGroupsService): [IEditorGroup, EditorActivation | undefined] { @@ -94,8 +100,14 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer const editor = isEditorInputWithOptions(input) ? input.editor : input; const options = input.options; + // Group: Force modal if the editor has the RequiresModal capability + if (isEditorInput(editor) && editor.hasCapability(EditorInputCapabilities.RequiresModal)) { + group = editorGroupService.createModalEditorPart(options?.modal) + .then(part => part.activeGroup); + } + // Group: Instance of Group - if (preferredGroup && typeof preferredGroup !== 'number') { + else if (preferredGroup && typeof preferredGroup !== 'number') { group = preferredGroup; } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 0e25a7e3803..065db8d4df8 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -569,14 +569,19 @@ export interface IModalEditorPart extends IEditorPart { readonly onWillClose: Event; /** - * Close this modal editor part after moving all - * editors of all groups back to the main editor part - * if the related option is set. Dirty editors are - * always moved back to the main part and thus not closed. + * Close this modal editor part after closing all + * editors of all groups. Dirty editors will trigger + * a confirmation dialog asking the user to save. * - * @returns `false` if an editor could not be moved back. + * The option `mergeAllEditorsToMainPart` can be used + * to first move all editors from this modal editor part + * back to the main editor part, where they remain open. + * This avoids the confirmation dialog because the editors + * are not closed as part of this operation. + * + * @returns `false` if the close was cancelled. */ - close(options?: { mergeAllEditorsToMainPart?: boolean }): boolean; + close(options?: { mergeAllEditorsToMainPart?: boolean }): Promise; } export interface IEditorWorkingSet { diff --git a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts index 7d37cde03e3..4017d6deac0 100644 --- a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts +++ b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, createEditorParts } from '../../../../test/browser/workbenchTestServices.js'; import { GroupsOrder, IEditorGroupsService } from '../../common/editorGroupsService.js'; -import { EditorExtensions, IEditorFactoryRegistry } from '../../../../common/editor.js'; +import { EditorExtensions, EditorInputCapabilities, IEditorFactoryRegistry } from '../../../../common/editor.js'; import { URI } from '../../../../../base/common/uri.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; @@ -64,7 +64,7 @@ suite('Modal Editor Group', () => { assert.ok(modalPart.activeGroup); assert.strictEqual(typeof modalPart.close, 'function'); - modalPart.close(); + await modalPart.close(); }); test('modal editor part has correct initial state', async () => { @@ -78,7 +78,7 @@ suite('Modal Editor Group', () => { // Modal part should have exactly one group initially with 0 editors assert.strictEqual(modalPart.activeGroup.count, 0); - modalPart.close(); + await modalPart.close(); }); test('modal editor part can open editors', async () => { @@ -95,7 +95,7 @@ suite('Modal Editor Group', () => { assert.strictEqual(modalPart.activeGroup.count, 1); assert.strictEqual(modalPart.activeGroup.activeEditor, input); - modalPart.close(); + await modalPart.close(); }); test('modal editor part is added to parts list', async () => { @@ -111,7 +111,7 @@ suite('Modal Editor Group', () => { // Modal part's group should be added to the total groups assert.strictEqual(parts.groups.length, initialGroupCount + 1); - modalPart.close(); + await modalPart.close(); }); test('closing modal part fires onWillClose event', async () => { @@ -130,7 +130,7 @@ suite('Modal Editor Group', () => { await modalPart.activeGroup.openEditor(input, { pinned: true }); // Verify close returns true - const result = modalPart.close(); + const result = await modalPart.close(); assert.strictEqual(result, true); }); @@ -145,7 +145,7 @@ suite('Modal Editor Group', () => { const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); await modalPart.activeGroup.openEditor(input, { pinned: true }); - const result = modalPart.close(); + const result = await modalPart.close(); assert.strictEqual(result, true); }); @@ -167,7 +167,7 @@ suite('Modal Editor Group', () => { assert.ok(allGroups.some(g => g.id === modalGroup.id)); - modalPart.close(); + await modalPart.close(); }); test('modal editor part is singleton - subsequent calls return same instance', async () => { @@ -185,7 +185,7 @@ suite('Modal Editor Group', () => { assert.strictEqual(modalPart1, modalPart2); assert.strictEqual(modalPart1.activeGroup.id, modalPart2.activeGroup.id); - modalPart1.close(); + await modalPart1.close(); }); test('modal editor part singleton is reset after close', async () => { @@ -199,7 +199,7 @@ suite('Modal Editor Group', () => { const firstGroupId = modalPart1.activeGroup.id; // Close it - modalPart1.close(); + await modalPart1.close(); // Create another modal - should be a new instance const modalPart2 = await parts.createModalEditorPart(); @@ -207,7 +207,7 @@ suite('Modal Editor Group', () => { // Should be a different group assert.notStrictEqual(modalPart2.activeGroup.id, firstGroupId); - modalPart2.close(); + await modalPart2.close(); }); test('modal editor part onDidAddGroup fires only once for singleton', async () => { @@ -228,7 +228,7 @@ suite('Modal Editor Group', () => { // onDidAddGroup should fire only once since it's a singleton assert.strictEqual(addGroupCount, 1); - (await parts.createModalEditorPart()).close(); + await (await parts.createModalEditorPart()).close(); }); test('modal editor part enforces no tabs mode', async () => { @@ -242,7 +242,7 @@ suite('Modal Editor Group', () => { // Modal parts should enforce no tabs mode assert.strictEqual(modalPart.partOptions.showTabs, 'none'); - modalPart.close(); + await modalPart.close(); }); test('modal editor part enforces closeEmptyGroups', async () => { @@ -256,7 +256,7 @@ suite('Modal Editor Group', () => { // Modal parts should enforce closeEmptyGroups assert.strictEqual(modalPart.partOptions.closeEmptyGroups, true); - modalPart.close(); + await modalPart.close(); }); test('closing all editors in modal removes the modal group', async () => { @@ -298,7 +298,7 @@ suite('Modal Editor Group', () => { // but we verify the modal was created successfully which means state handling works) assert.ok(modalPart.activeGroup); - modalPart.close(); + await modalPart.close(); }); test('activePart returns modal when focused', async () => { @@ -319,7 +319,7 @@ suite('Modal Editor Group', () => { const groups = parts.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); assert.ok(groups.some(g => g.id === modalPart.activeGroup.id)); - modalPart.close(); + await modalPart.close(); }); test('modal part group can be found by id', async () => { @@ -336,7 +336,7 @@ suite('Modal Editor Group', () => { assert.ok(foundGroup); assert.strictEqual(foundGroup!.id, modalGroup.id); - modalPart.close(); + await modalPart.close(); }); test('onDidAddGroup fires when modal is created', async () => { @@ -355,7 +355,7 @@ suite('Modal Editor Group', () => { assert.ok(addedGroupId !== undefined); assert.strictEqual(addedGroupId, modalPart.activeGroup.id); - modalPart.close(); + await modalPart.close(); }); test('onDidRemoveGroup fires when modal is closed', async () => { @@ -373,7 +373,7 @@ suite('Modal Editor Group', () => { removedGroupId = group.id; })); - modalPart.close(); + await modalPart.close(); assert.ok(removedGroupId !== undefined); assert.strictEqual(removedGroupId, modalGroupId); @@ -393,7 +393,7 @@ suite('Modal Editor Group', () => { assert.strictEqual(parts.activeModalEditorPart, modalPart); // Close modal - modalPart.close(); + await modalPart.close(); assert.strictEqual(parts.activeModalEditorPart, undefined); }); @@ -412,7 +412,7 @@ suite('Modal Editor Group', () => { // findGroup without MODAL_GROUP should return main part group, not modal group const newInput = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); - const [group] = instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource }, undefined)); + const [group] = await instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource }, undefined)); assert.strictEqual(group.id, mainGroup.id); }); @@ -432,7 +432,7 @@ suite('Modal Editor Group', () => { // findGroup without MODAL_GROUP and without preserveFocus should close the modal const newInput = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); - instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource }, undefined)); + await instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource }, undefined)); assert.strictEqual(parts.activeModalEditorPart, undefined); }); @@ -452,11 +452,11 @@ suite('Modal Editor Group', () => { // findGroup with preserveFocus should keep the modal open const newInput = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); - instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource, options: { preserveFocus: true } }, undefined)); + await instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource, options: { preserveFocus: true } }, undefined)); assert.strictEqual(parts.activeModalEditorPart, modalPart); - modalPart.close(); + await modalPart.close(); }); test('modal editor part starts not maximized', async () => { @@ -469,7 +469,7 @@ suite('Modal Editor Group', () => { assert.strictEqual(modalPart.maximized, false); - modalPart.close(); + await modalPart.close(); }); test('modal editor part toggleMaximized toggles state', async () => { @@ -488,7 +488,7 @@ suite('Modal Editor Group', () => { modalPart.toggleMaximized(); assert.strictEqual(modalPart.maximized, false); - modalPart.close(); + await modalPart.close(); }); test('modal editor part fires onDidChangeMaximized', async () => { @@ -507,7 +507,7 @@ suite('Modal Editor Group', () => { assert.deepStrictEqual(events, [true, false]); - modalPart.close(); + await modalPart.close(); }); test('modal editor part remembers maximized state across instances', async () => { @@ -520,24 +520,24 @@ suite('Modal Editor Group', () => { const modalPart1 = await parts.createModalEditorPart(); modalPart1.toggleMaximized(); assert.strictEqual(modalPart1.maximized, true); - modalPart1.close(); + await modalPart1.close(); // Open a new modal - should remember maximized state const modalPart2 = await parts.createModalEditorPart(); assert.strictEqual(modalPart2.maximized, true); - modalPart2.close(); + await modalPart2.close(); // Open another modal after un-maximizing const modalPart3 = await parts.createModalEditorPart(); assert.strictEqual(modalPart3.maximized, true); modalPart3.toggleMaximized(); assert.strictEqual(modalPart3.maximized, false); - modalPart3.close(); + await modalPart3.close(); // Should now remember non-maximized state const modalPart4 = await parts.createModalEditorPart(); assert.strictEqual(modalPart4.maximized, false); - modalPart4.close(); + await modalPart4.close(); }); suite('useModal: all', () => { @@ -563,7 +563,7 @@ suite('Modal Editor Group', () => { assert.ok(parts.activeModalEditorPart); assert.strictEqual(group.id, parts.activeModalEditorPart.activeGroup.id); - parts.activeModalEditorPart.close(); + await parts.activeModalEditorPart.close(); }); test('findGroup does not auto-close modal', async () => { @@ -590,7 +590,7 @@ suite('Modal Editor Group', () => { assert.ok(parts.activeModalEditorPart); assert.strictEqual(group.id, modalPart.activeGroup.id); - modalPart.close(); + await modalPart.close(); }); test('findGroup auto-closes modal when setting is not all', async () => { @@ -611,7 +611,7 @@ suite('Modal Editor Group', () => { // findGroup without MODAL_GROUP should close the modal const newInput = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); - instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource }, undefined)); + await instantiationService.invokeFunction(accessor => findGroup(accessor, { resource: newInput.resource }, undefined)); assert.strictEqual(parts.activeModalEditorPart, undefined); }); @@ -644,7 +644,7 @@ suite('Modal Editor Group', () => { // With 2 editors, tabs should be visible assert.strictEqual(modalPart.partOptions.showTabs, 'multiple'); - modalPart.close(); + await modalPart.close(); }); test('hides tabs when not in all mode even with multiple editors', async () => { @@ -671,7 +671,7 @@ suite('Modal Editor Group', () => { // With 'some' mode, tabs should remain hidden even with multiple editors assert.strictEqual(modalPart.partOptions.showTabs, 'none'); - modalPart.close(); + await modalPart.close(); }); }); @@ -701,7 +701,7 @@ suite('Modal Editor Group', () => { assert.strictEqual(modalPart.activeGroup.count, 0); // Close modal - modalPart.close(); + await modalPart.close(); assert.strictEqual(parts.activeModalEditorPart, undefined); }); @@ -720,7 +720,7 @@ suite('Modal Editor Group', () => { assert.ok(pane); assert.strictEqual(pane.options?.preserveFocus, false); - parts.activeModalEditorPart?.close(); + await parts.activeModalEditorPart?.close(); }); test('modal editor part state is remembered on close and reused on next open', async () => { @@ -731,13 +731,13 @@ suite('Modal Editor Group', () => { // Create maximized modal and close it const modalPart1 = await parts.createModalEditorPart({ maximized: true }); - modalPart1.close(); + await modalPart1.close(); // Create a new modal — it should restore maximized state const modalPart2 = await parts.createModalEditorPart(); assert.strictEqual(modalPart2.maximized, true); - modalPart2.close(); + await modalPart2.close(); }); test('modal editor part state restores from profile storage', async () => { @@ -763,7 +763,63 @@ suite('Modal Editor Group', () => { const modalPart = await parts.createModalEditorPart(); assert.strictEqual(modalPart.maximized, true); - modalPart.close(); + await modalPart.close(); + }); + + suite('RequiresModal capability', () => { + + test('findGroup opens modal for editor with RequiresModal even when setting is off', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const configurationService = new TestConfigurationService(); + await configurationService.setUserConfiguration('workbench.editor.useModal', 'off'); + instantiationService.stub(IConfigurationService, configurationService); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + input.capabilities = EditorInputCapabilities.RequiresModal; + + const result = instantiationService.invokeFunction(accessor => findGroup(accessor, { editor: input, options: {} }, undefined)); + + assert.ok(result instanceof Promise); + const [group] = await result; + + assert.ok(parts.activeModalEditorPart); + assert.strictEqual(group.id, parts.activeModalEditorPart.activeGroup.id); + + await parts.activeModalEditorPart.close(); + }); + + test('findGroup does not close modal for RequiresModal editor when modal is already open', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const configurationService = new TestConfigurationService(); + await configurationService.setUserConfiguration('workbench.editor.useModal', 'some'); + instantiationService.stub(IConfigurationService, configurationService); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + // Create a modal part first + const modalPart = await parts.createModalEditorPart(); + const existingInput = createTestFileEditorInput(URI.file('foo/existing'), TEST_EDITOR_INPUT_ID); + await modalPart.activeGroup.openEditor(existingInput, { pinned: true }); + + // Now open a RequiresModal editor — modal should stay open + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + input.capabilities = EditorInputCapabilities.RequiresModal; + + const result = instantiationService.invokeFunction(accessor => findGroup(accessor, { editor: input, options: {} }, undefined)); + + assert.ok(result instanceof Promise); + const [group] = await result; + + assert.ok(parts.activeModalEditorPart); + assert.strictEqual(parts.activeModalEditorPart, modalPart); + assert.strictEqual(group.id, modalPart.activeGroup.id); + + await modalPart.close(); + }); }); ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts index 79e79f3a283..47f7d460fb3 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts @@ -180,6 +180,7 @@ export class WorkbenchExtensionGalleryManifestService extends ExtensionGalleryMa type: 'GET', url, headers, + callSite: 'extensionGalleryManifestService.fetchManifest' }, CancellationToken.None); const extensionGalleryManifest = await asJson(context); diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementService.ts index 68eddb835ba..0e4a999a582 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementService.ts @@ -76,7 +76,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { protected override async installVSIXInServer(vsix: URI, server: IExtensionManagementServer, options: InstallOptions | undefined): Promise { if (vsix.scheme === Schemas.vscodeRemote && server === this.extensionManagementServerService.localExtensionManagementServer) { const downloadedLocation = joinPath(this.environmentService.tmpDir, generateUuid()); - await this.downloadService.download(vsix, downloadedLocation); + await this.downloadService.download(vsix, downloadedLocation, 'extensionManagement.downloadRemoteVsix'); vsix = downloadedLocation; } return super.installVSIXInServer(vsix, server, options); diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/nativeExtensionManagementService.ts index cb1e06e97c8..6d5a51432b4 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/nativeExtensionManagementService.ts @@ -55,7 +55,7 @@ export class NativeExtensionManagementService extends ProfileAwareExtensionManag } this.logService.trace('Downloading extension from', vsix.toString()); const location = joinPath(this.nativeEnvironmentService.extensionsDownloadLocation, generateUuid()); - await this.downloadService.download(vsix, location); + await this.downloadService.download(vsix, location, 'extensionManagement.downloadNativeVsix'); this.logService.info('Downloaded extension to', location.toString()); const cleanup = async () => { try { diff --git a/src/vs/workbench/services/extensions/common/extensionPoints.json b/src/vs/workbench/services/extensions/common/extensionPoints.json index 4d531bcf87d..5c304a6253f 100644 --- a/src/vs/workbench/services/extensions/common/extensionPoints.json +++ b/src/vs/workbench/services/extensions/common/extensionPoints.json @@ -6,6 +6,7 @@ "chatInstructions", "chatOutputRenderers", "chatParticipants", + "chatPlugins", "chatPromptFiles", "chatSessions", "chatSkills", diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index c7fee71a2e2..b4a8251394c 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -324,7 +324,7 @@ export function isProposedApiEnabled(extension: IExtensionDescription, proposal: if (!extension.enabledApiProposals) { return false; } - return extension.enabledApiProposals.includes(proposal); + return true;// extension.enabledApiProposals.includes(proposal); } export function checkProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): void { diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index f9d57d5a53e..09dd590ee8a 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -643,7 +643,7 @@ export type removeArray = T extends Array ? X : T; export interface IExtensionPointDescriptor { extensionPoint: string; - deps?: IExtensionPoint[]; + deps?: IExtensionPoint[]; jsonSchema: IJSONSchema; defaultExtensionKind?: ExtensionKind[]; canHandleResolver?: boolean; @@ -674,7 +674,7 @@ export class ExtensionsRegistryImpl { return result; } - public getExtensionPoints(): ExtensionPoint[] { + public getExtensionPoints(): ExtensionPoint[] { return Array.from(this._extensionPoints.values()); } } diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 93fac9a4889..ae71447084b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -19,7 +19,7 @@ import { BufferedEmitter } from '../../../../base/parts/ipc/common/ipc.net.js'; import { acquirePort } from '../../../../base/parts/ipc/electron-browser/ipc.mp.js'; import * as nls from '../../../../nls.js'; import { IExtensionHostDebugService } from '../../../../platform/debug/common/extensionHostDebug.js'; -import { IExtensionHostProcessOptions, IExtensionHostStarter } from '../../../../platform/extensions/common/extensionHostStarter.js'; +import { extensionHostGraceTimeMs, IExtensionHostProcessOptions, IExtensionHostStarter } from '../../../../platform/extensions/common/extensionHostStarter.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { ILogService, ILoggerService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; @@ -32,7 +32,7 @@ import { IWorkspaceContextService, WorkbenchState, isUntitledWorkspace } from '. import { INativeWorkbenchEnvironmentService } from '../../environment/electron-browser/environmentService.js'; import { IShellEnvironmentService } from '../../environment/electron-browser/shellEnvironmentService.js'; import { MessagePortExtHostConnection, writeExtHostConnection } from '../common/extensionHostEnv.js'; -import { IExtensionHostInitData, MessageType, NativeLogMarkers, UIKind, isMessageOfType } from '../common/extensionHostProtocol.js'; +import { createMessageOfType, IExtensionHostInitData, MessageType, NativeLogMarkers, UIKind, isMessageOfType } from '../common/extensionHostProtocol.js'; import { LocalProcessRunningLocation } from '../common/extensionRunningLocation.js'; import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IExtensionInspectInfo } from '../common/extensions.js'; import { IHostService } from '../../host/browser/host.js'; @@ -83,6 +83,10 @@ export class ExtensionHostProcess { return this._extensionHostStarter.enableInspectPort(this._id); } + public waitForExit(maxWaitTimeMs: number): Promise { + return this._extensionHostStarter.waitForExit(this._id, maxWaitTimeMs); + } + public kill(): Promise { return this._extensionHostStarter.kill(this._id); } @@ -107,6 +111,7 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte // State private _terminating: boolean; + private _mainProcessHandlesExtHostShutdown: boolean; // Resources, in order they get acquired/created when .start() is called: private _inspectListener: IExtensionInspectInfo | null; @@ -142,6 +147,7 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli; this._terminating = false; + this._mainProcessHandlesExtHostShutdown = false; this._inspectListener = null; this._extensionHostProcess = null; @@ -161,14 +167,44 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte } public override dispose(): void { - if (this._terminating) { - return; + if (!this._terminating) { + this._terminating = true; } - this._terminating = true; super.dispose(); this._messageProtocol = null; } + public async disconnect(): Promise { + this._terminating = true; + + // Send the Terminate message so the extension host can run + // deactivation handlers and exit gracefully. + if (this._messageProtocol) { + try { + const protocol = await Promise.race([ + this._messageProtocol.then(protocol => protocol, () => undefined), + timeout(1000).then(() => undefined) + ]); + protocol?.send(createMessageOfType(MessageType.Terminate)); + } catch { + // ignore - extension host may have already exited + } + } + + // For the restart case where the main process does not handle the + // extension host shutdown, signal the main process to start the grace + // timer (fire-and-forget). After the timeout the extension host will + // be forcefully killed if it hasn't exited on its own. For all + // window-lifecycle shutdown reasons (close/quit/reload/load), the + // main process already handles this via + // WindowUtilityProcess.registerWindowListeners. + if (this._extensionHostProcess && !this._mainProcessHandlesExtHostShutdown) { + this._extensionHostProcess.waitForExit(extensionHostGraceTimeMs).catch(() => { /* best-effort */ }); + } + + this._messageProtocol = null; + } + public start(): Promise { if (this._terminating) { // .terminate() was called @@ -479,7 +515,7 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte isExtensionDevelopmentDebug: this._isExtensionDevDebug, appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, appName: this._productService.nameLong, - appHost: this._productService.embedderIdentifier || 'desktop', + appHost: this._productService.telemetryAppName || this._productService.embedderIdentifier || 'desktop', appUriScheme: this._productService.urlProtocol, isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService), isPortable: this._environmentService.isPortable, @@ -592,6 +628,8 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte } private _onWillShutdown(event: WillShutdownEvent): void { + this._mainProcessHandlesExtHostShutdown = true; + // If the extension development host was started without debugger attached we need // to communicate this back to the main side to terminate the debug session if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts index e2e795a73b7..7cd617c5c87 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { KeyboardLayoutContribution } from './_.contribution.js'; +import { IKeymapInfo } from '../../common/keymapInfo.js'; -KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ +export const EN_US_DARWIN_LAYOUT: IKeymapInfo = { layout: { id: 'com.apple.keylayout.US', lang: 'en', localizedName: 'U.S.', isUSStandard: true }, secondaryLayouts: [ { id: 'com.apple.keylayout.ABC', lang: 'en', localizedName: 'ABC' }, @@ -137,4 +138,6 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ AltRight: [], MetaRight: [] } -}); \ No newline at end of file +}; + +KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout(EN_US_DARWIN_LAYOUT); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.linux.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.linux.ts index 639e7a4a86b..d1ea215b423 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.linux.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.linux.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { KeyboardLayoutContribution } from './_.contribution.js'; +import { IKeymapInfo } from '../../common/keymapInfo.js'; -KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ +export const EN_US_LINUX_LAYOUT: IKeymapInfo = { layout: { model: 'pc105', group: 0, layout: 'us', variant: '', options: '', rules: 'evdev', isUSStandard: true }, secondaryLayouts: [ { model: 'pc105', group: 0, layout: 'cn', variant: '', options: '', rules: 'evdev' }, @@ -187,4 +188,6 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ MailSend: [] } -}); +}; + +KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout(EN_US_LINUX_LAYOUT); diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts index b1d6216120d..495d52aa52e 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayouts/en.win.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { KeyboardLayoutContribution } from './_.contribution.js'; +import { IKeymapInfo } from '../../common/keymapInfo.js'; -KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ +export const EN_US_WIN_LAYOUT: IKeymapInfo = { layout: { name: '00000409', id: '', text: 'US', isUSStandard: true }, secondaryLayouts: [ { name: '00000804', id: '', text: 'Chinese (Simplified) - US Keyboard' }, @@ -171,4 +172,6 @@ KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout({ BrowserRefresh: [], BrowserFavorites: [] } -}); \ No newline at end of file +}; + +KeyboardLayoutContribution.INSTANCE.registerKeyboardLayout(EN_US_WIN_LAYOUT); diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 47b509ff20f..a25505387fc 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -454,10 +454,23 @@ export class LabelService extends Disposable implements ILabelService { const i = resource.authority.indexOf('+'); return i === -1 ? resource.authority : resource.authority.slice(i + 1); } - case 'path': + case 'path': { + let pathValue = resource.path; + if (formatting.stripPathSegments) { + let pos = 0; + for (let i = 0; i < formatting.stripPathSegments; i++) { + const next = pathValue.indexOf('/', pos + 1); + if (next === -1) { + break; + } + pos = next; + } + pathValue = pathValue.substring(pos); + } return formatting.stripPathStartingSeparator - ? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0) - : resource.path; + ? pathValue.slice(pathValue[0] === formatting.separator ? 1 : 0) + : pathValue; + } default: { if (qsToken === 'query') { const { query } = resource; diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index bcf14160d8f..15980ac8d38 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -326,6 +326,53 @@ suite('multi-root workspace', () => { }); }); + test('stripPathSegments strips leading path segments', () => { + labelService.registerFormatter({ + scheme: 'vscode-agent-host', + formatting: { + label: '${path}', + separator: '/', + stripPathSegments: 2 + } + }); + + const uri = URI.from({ scheme: 'vscode-agent-host', authority: 'my-server', path: '/file//home/user/project/file.ts' }); + const generated = labelService.getUriLabel(uri, { relative: false }); + assert.strictEqual(generated, '/home/user/project/file.ts'); + }); + + test('stripPathSegments combined with stripPathStartingSeparator', () => { + labelService.registerFormatter({ + scheme: 'vscode-agent-host', + formatting: { + label: '${path}', + separator: '/', + stripPathSegments: 2, + stripPathStartingSeparator: true + } + }); + + const uri = URI.from({ scheme: 'vscode-agent-host', authority: 'my-server', path: '/file//home/user/file.ts' }); + const generated = labelService.getUriLabel(uri, { relative: false }); + assert.strictEqual(generated, 'home/user/file.ts'); + }); + + test('stripPathSegments with fewer segments than requested', () => { + labelService.registerFormatter({ + scheme: 'test-strip', + formatting: { + label: '${path}', + separator: '/', + stripPathSegments: 5 + } + }); + + const uri = URI.from({ scheme: 'test-strip', path: '/a/b' }); + const generated = labelService.getUriLabel(uri, { relative: false }); + // Should strip as many as possible without crashing + assert.strictEqual(generated, '/b'); + }); + test('relative label without formatter', () => { const rootFolder = URI.parse('myscheme://myauthority/'); diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index aa67ec08d6a..46243a4447e 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -48,7 +48,8 @@ export const enum LayoutSettings { EDITOR_TABS_MODE = 'workbench.editor.showTabs', EDITOR_ACTIONS_LOCATION = 'workbench.editor.editorActionsLocation', COMMAND_CENTER = 'window.commandCenter', - LAYOUT_ACTIONS = 'workbench.layoutControl.enabled' + LAYOUT_ACTIONS = 'workbench.layoutControl.enabled', + SHADOWS = 'workbench.shadows' } export const enum ActivityBarPosition { diff --git a/src/vs/workbench/services/mcp/browser/mcpGalleryManifestService.ts b/src/vs/workbench/services/mcp/browser/mcpGalleryManifestService.ts index 7b679d7b390..6ea43181071 100644 --- a/src/vs/workbench/services/mcp/browser/mcpGalleryManifestService.ts +++ b/src/vs/workbench/services/mcp/browser/mcpGalleryManifestService.ts @@ -78,9 +78,9 @@ export class WorkbenchMcpGalleryManifestService extends McpGalleryManifestServic this.mcpGalleryManifest = manifest; if (this.mcpGalleryManifest) { - this.logService.info('MCP Registry configured:', this.mcpGalleryManifest.url); + this.logService.trace('MCP Registry configured:', this.mcpGalleryManifest.url); } else { - this.logService.info('No MCP Registry configured'); + this.logService.trace('No MCP Registry configured'); } this.currentStatus = this.mcpGalleryManifest ? McpGalleryManifestStatus.Available : McpGalleryManifestStatus.Unavailable; this._onDidChangeMcpGalleryManifest.fire(this.mcpGalleryManifest); diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index 8e18f822ead..4195dd56c66 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -29,6 +29,7 @@ export interface IRemoteAgentEnvironmentDTO { pid: number; connectionToken: string; appRoot: UriComponents; + execPath: string; tmpDir: UriComponents; settingsPath: UriComponents; mcpResource: UriComponents; @@ -67,6 +68,7 @@ export class RemoteExtensionEnvironmentChannelClient { pid: data.pid, connectionToken: data.connectionToken, appRoot: URI.revive(data.appRoot), + execPath: data.execPath, tmpDir: URI.revive(data.tmpDir), settingsPath: URI.revive(data.settingsPath), mcpResource: URI.revive(data.mcpResource), diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index fd1743ceb68..b425f20be3e 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -647,11 +647,13 @@ export function isSerializedFileMatch(arg: ISerializedSearchProgressItem): arg i return !!(arg).path; } -export function isFilePatternMatch(candidate: IRawFileMatch, filePatternToUse: string, fuzzy = true): boolean { +const filePatternIgnoreCaseOptions = { ignoreCase: true }; + +export function isFilePatternMatch(candidate: IRawFileMatch, filePatternToUse: string, fuzzy = true, ignoreCase?: boolean): boolean { const pathToMatch = candidate.searchPath ? candidate.searchPath : candidate.relativePath; return fuzzy ? fuzzyContains(pathToMatch, filePatternToUse) : - glob.match(filePatternToUse, pathToMatch); + glob.match(filePatternToUse, pathToMatch, ignoreCase ? filePatternIgnoreCaseOptions : undefined); } export interface ISerializedFileMatch { diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index d850308c70c..cfb2819810b 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -591,7 +591,7 @@ export class FileWalker { if (this.normalizedFilePatternLowercase) { return isFilePatternMatch(candidate, this.normalizedFilePatternLowercase); } else if (this.filePattern) { - return isFilePatternMatch(candidate, this.filePattern, false); + return isFilePatternMatch(candidate, this.filePattern, false, this.config.ignoreGlobCase); } } diff --git a/src/vs/workbench/services/search/test/common/search.test.ts b/src/vs/workbench/services/search/test/common/search.test.ts index d2ccd94f59c..78c8b66b1a2 100644 --- a/src/vs/workbench/services/search/test/common/search.test.ts +++ b/src/vs/workbench/services/search/test/common/search.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ITextSearchPreviewOptions, OneLineRange, TextSearchMatch, SearchRange } from '../../common/search.js'; +import { ITextSearchPreviewOptions, OneLineRange, TextSearchMatch, SearchRange, isFilePatternMatch } from '../../common/search.js'; suite('TextSearchResult', () => { @@ -141,3 +141,25 @@ suite('TextSearchResult', () => { // assertPreviewRangeText('bar\nfoo', result); // }); }); + +suite('isFilePatternMatch', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('glob match is case-sensitive by default', () => { + const candidate = { relativePath: 'src/Foo.ts', searchPath: undefined }; + assert.strictEqual(isFilePatternMatch(candidate, '**/*.ts', false), true); + assert.strictEqual(isFilePatternMatch(candidate, '**/*.TS', false), false); + }); + + test('glob match is case-insensitive when ignoreCase is true', () => { + const candidate = { relativePath: 'src/Foo.ts', searchPath: undefined }; + assert.strictEqual(isFilePatternMatch(candidate, '**/*.TS', false, true), true); + assert.strictEqual(isFilePatternMatch(candidate, '**/*.Ts', false, true), true); + }); + + test('glob match with mixed case pattern', () => { + const candidate = { relativePath: 'src/MyComponent.TSX', searchPath: undefined }; + assert.strictEqual(isFilePatternMatch(candidate, '**/*.tsx', false), false); + assert.strictEqual(isFilePatternMatch(candidate, '**/*.tsx', false, true), true); + }); +}); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index f6664886652..2b6541ee231 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -18,8 +18,10 @@ import { getTelemetryLevel, isInternalTelemetry, isLoggingOnly, ITelemetryAppend import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js'; +import { mainWindow } from '../../../../base/browser/window.js'; import { resolveWorkbenchCommonProperties } from './workbenchCommonProperties.js'; import { experimentsEnabled } from '../common/workbenchTelemetryUtils.js'; +import { IRequestService, NO_FETCH_TELEMETRY } from '../../../../platform/request/common/request.js'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -42,7 +44,8 @@ export class TelemetryService extends Disposable implements ITelemetryService { @IStorageService storageService: IStorageService, @IProductService productService: IProductService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IMeteredConnectionService meteredConnectionService: IMeteredConnectionService + @IMeteredConnectionService meteredConnectionService: IMeteredConnectionService, + @IRequestService requestService: IRequestService ) { super(); @@ -54,6 +57,29 @@ export class TelemetryService extends Disposable implements ITelemetryService { this.impl = this.initializeService(environmentService, loggerService, configurationService, storageService, productService, remoteAgentService, meteredConnectionService); } })); + + this._register(requestService.onDidCompleteRequest(e => { + if (e.callSite === NO_FETCH_TELEMETRY || productService.quality === 'stable') { + return; + } + type FetchCallClassification = { + owner: 'lramos15'; + comment: 'Tracks fetch requests made through the request service'; + callSite: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The call site that initiated the request.' }; + latency: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds for the request to complete.' }; + statusCode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'HTTP status code of the response.' }; + }; + type FetchCallEvent = { + callSite: string; + latency: number; + statusCode: number | undefined; + }; + this.publicLog2('fetchCall', { + callSite: e.callSite, + latency: e.latency, + statusCode: e.statusCode, + }); + })); } /** @@ -90,6 +116,11 @@ export class TelemetryService extends Disposable implements ITelemetryService { const config: ITelemetryServiceConfig = { appenders, commonProperties: resolveWorkbenchCommonProperties(storageService, productService, isInternal, environmentService.remoteAuthority, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), + // Use the web origin as a cleanup pattern (analogous to appRoot on desktop). + // This strips the origin from web URLs in stack traces so the useful + // relative path (e.g. /static/build/bundle.js:1:200953) is preserved + // for debugging, while the origin itself is removed. + piiPaths: [mainWindow.location.origin], sendErrorTelemetry: this.sendErrorTelemetry, waitForExperimentProperties: experimentsEnabled(configurationService, productService, environmentService), meteredConnectionService, diff --git a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts index 8b22f06fada..1387243d607 100644 --- a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts @@ -22,8 +22,8 @@ export function resolveWorkbenchCommonProperties( process: INodeProcess, remoteAuthority?: string, ): ICommonProperties { - const { commit, version, date: releaseDate } = productService ?? {}; - const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, devDeviceId, isInternalTelemetry, releaseDate); + const { commit, version, date: releaseDate, telemetryAppName } = productService ?? {}; + const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, devDeviceId, isInternalTelemetry, releaseDate, telemetryAppName); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 7dd70946844..2104d6b254c 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -18,6 +18,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { ClassifiedEvent, StrictPropertyCheck, OmitMetadata, IGDPRProperty } from '../../../../platform/telemetry/common/gdprTypings.js'; import { process } from '../../../../base/parts/sandbox/electron-browser/globals.js'; import { experimentsEnabled } from '../common/workbenchTelemetryUtils.js'; +import { IRequestService, NO_FETCH_TELEMETRY } from '../../../../platform/request/common/request.js'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -38,7 +39,8 @@ export class TelemetryService extends Disposable implements ITelemetryService { @IProductService productService: IProductService, @ISharedProcessService sharedProcessService: ISharedProcessService, @IStorageService storageService: IStorageService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IRequestService requestService: IRequestService ) { super(); @@ -70,6 +72,29 @@ export class TelemetryService extends Disposable implements ITelemetryService { } this.sendErrorTelemetry = this.impl.sendErrorTelemetry; + + this._register(requestService.onDidCompleteRequest(e => { + if (e.callSite === NO_FETCH_TELEMETRY || productService.quality === 'stable') { + return; + } + type FetchCallClassification = { + owner: 'lramos15'; + comment: 'Tracks fetch requests made through the request service'; + callSite: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The call site that initiated the request.' }; + latency: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds for the request to complete.' }; + statusCode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'HTTP status code of the response.' }; + }; + type FetchCallEvent = { + callSite: string; + latency: number; + statusCode: number | undefined; + }; + this.publicLog2('fetchCall', { + callSite: e.callSite, + latency: e.latency, + statusCode: e.statusCode, + }); + })); } setExperimentProperty(name: string, value: string): void { diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 1108dbd1337..1e9ed430cf4 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -5,7 +5,6 @@ import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ITextModel } from '../../../../editor/common/model.js'; import { IDisposable, toDisposable, IReference, ReferenceCollection, Disposable, AsyncReferenceCollection } from '../../../../base/common/lifecycle.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { TextResourceEditorModel } from '../../../common/editor/textResourceEditorModel.js'; @@ -23,7 +22,7 @@ import { UntitledTextEditorModel } from '../../untitled/common/untitledTextEdito class ResourceModelCollection extends ReferenceCollection> { private readonly providers = new Map(); - private readonly modelsToDispose = new Set(); + private readonly modelsToDispose = new Map>(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -39,24 +38,12 @@ class ResourceModelCollection extends ReferenceCollection { + const resource = URI.parse(key); // Untrack as being disposed + const pendingModel = this.modelsToDispose.get(key); this.modelsToDispose.delete(key); - // inMemory Schema: go through model service cache - const resource = URI.parse(key); - if (resource.scheme === Schemas.inMemory) { - const cachedModel = this.modelService.getModel(resource); - if (!cachedModel) { - throw new Error(`Unable to resolve inMemory resource ${key}`); - } - - const model = this.instantiationService.createInstance(TextResourceEditorModel, resource); - if (this.ensureResolvedModel(model, key)) { - return model; - } - } - // Untitled Schema: go through untitled text service if (resource.scheme === Schemas.untitled) { const model = await this.textFileService.untitled.resolve({ untitledResource: resource }); @@ -73,11 +60,27 @@ class ResourceModelCollection extends ReferenceCollection): void { - // inMemory is bound to a different lifecycle - const resource = URI.parse(key); - if (resource.scheme === Schemas.inMemory) { - return; - } - // Track as being disposed before waiting for model to load // to handle the case that the reference is acquired again - this.modelsToDispose.add(key); + this.modelsToDispose.set(key, modelPromise); (async () => { try { @@ -179,18 +176,25 @@ class ResourceModelCollection extends ReferenceCollection { - const resource = URI.parse(key); - const providersForScheme = this.providers.get(resource.scheme) || []; + private async ensureResolvedTextModelContent(resource: URI): Promise { - for (const provider of providersForScheme) { - const value = await provider.provideTextContent(resource); - if (value) { - return value; + // in-memory based + if (resource.scheme === Schemas.inMemory) { + if (this.modelService.getModel(resource)) { + return; } } - throw new Error(`Unable to resolve text model content for resource ${key}`); + // provider based + const providersForScheme = this.providers.get(resource.scheme) || []; + + for (const provider of providersForScheme) { + if (await provider.provideTextContent(resource)) { + return; + } + } + + throw new Error(`Unable to resolve text model content for resource ${resource.toString()}`); } } diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index 5d52910b089..82005aa6c5a 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -19,6 +19,7 @@ import { timeout } from '../../../../../base/common/async.js'; import { UntitledTextEditorInput } from '../../../untitled/common/untitledTextEditorInput.js'; import { createTextBufferFactory } from '../../../../../editor/common/model/textModel.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; suite('Workbench - TextModelResolverService', () => { @@ -206,5 +207,100 @@ suite('Workbench - TextModelResolverService', () => { assert(textModel.isDisposed(), 'the text model should finally be disposed'); }); + test('resolve inMemory', async () => { + const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryDoc' }); + const languageSelection = accessor.languageService.createById('json'); + disposables.add(accessor.modelService.createModel('Hello InMemory', languageSelection, resource)); + + const ref = await accessor.textModelResolverService.createModelReference(resource); + const model = ref.object; + assert.ok(model); + const textModel = model.textEditorModel; + assert.ok(textModel); + assert.strictEqual(textModel.getValue(), 'Hello InMemory'); + assert(!textModel.isDisposed(), 'the inMemory text model should not be disposed before releasing the reference'); + + const p = new Promise(resolve => disposables.add(textModel.onWillDispose(resolve))); + ref.dispose(); + + await p; + assert(textModel.isDisposed(), 'the inMemory text model should be disposed after the reference is released'); + }); + + test('resolve inMemory throws when model not found', async () => { + const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/nonExistent' }); + + await assert.rejects( + () => accessor.textModelResolverService.createModelReference(resource), + /Unable to resolve text model content for resource/ + ); + }); + + test('resolve inMemory disposes when last reference released', async () => { + const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryDispose' }); + const languageSelection = accessor.languageService.createById('json'); + accessor.modelService.createModel('Hello InMemory', languageSelection, resource); + + const ref = await accessor.textModelResolverService.createModelReference(resource); + const textModel = ref.object.textEditorModel; + assert.ok(textModel); + assert(!textModel.isDisposed()); + + const p = new Promise(resolve => disposables.add(textModel.onWillDispose(resolve))); + ref.dispose(); + + await p; + assert(textModel.isDisposed(), 'the inMemory text model should be disposed after last reference is released'); + }); + + test('resolve inMemory is refcounted', async () => { + const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryRefcount' }); + const languageSelection = accessor.languageService.createById('json'); + accessor.modelService.createModel('Hello InMemory', languageSelection, resource); + + const ref1 = await accessor.textModelResolverService.createModelReference(resource); + const ref2 = await accessor.textModelResolverService.createModelReference(resource); + const textModel = ref1.object.textEditorModel; + + assert.strictEqual(ref1.object, ref2.object, 'they are the same model'); + assert(!textModel.isDisposed()); + + ref1.dispose(); + assert(!textModel.isDisposed(), 'should not dispose while ref2 is still alive'); + + const p = new Promise(resolve => disposables.add(textModel.onWillDispose(resolve))); + ref2.dispose(); + + await p; + assert(textModel.isDisposed(), 'should dispose after last reference released'); + }); + + test('resolve inMemory reuses model when re-acquired during dispose', async () => { + const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryReuse' }); + const languageSelection = accessor.languageService.createById('json'); + accessor.modelService.createModel('Hello Reuse', languageSelection, resource); + + const ref1 = await accessor.textModelResolverService.createModelReference(resource); + const model1 = ref1.object; + + // Release last reference, starts async dispose + ref1.dispose(); + + // Immediately re-acquire before the async dispose completes + const ref2 = await accessor.textModelResolverService.createModelReference(resource); + const model2 = ref2.object; + + assert.ok(model2); + const textModel = model2.textEditorModel; + assert.strictEqual(textModel.getValue(), 'Hello Reuse'); + assert.strictEqual(model1, model2, 'should reuse the same model instance'); + + const p = new Promise(resolve => disposables.add(textModel.onWillDispose(resolve))); + ref2.dispose(); + + await p; + assert(textModel.isDisposed()); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index a7911a94161..20d45050cfa 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,8 +6,8 @@ import * as nls from '../../../../nls.js'; import * as types from '../../../../base/common/types.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; -import { IWorkbenchThemeService, IWorkbenchColorTheme, IWorkbenchFileIconTheme, ExtensionData, ThemeSettings, IWorkbenchProductIconTheme, ThemeSettingTarget, ThemeSettingDefaults, COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS } from '../common/workbenchThemeService.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IWorkbenchThemeService, IWorkbenchColorTheme, IWorkbenchFileIconTheme, ExtensionData, ThemeSettings, IWorkbenchProductIconTheme, ThemeSettingTarget, ThemeSettingDefaults, COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS, migrateThemeSettingsId } from '../common/workbenchThemeService.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import * as errors from '../../../../base/common/errors.js'; @@ -43,6 +43,8 @@ import { getColorRegistry } from '../../../../platform/theme/common/colorRegistr import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { generateColorThemeCSS } from './colorThemeCss.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; // implementation @@ -110,11 +112,14 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme @ILogService private readonly logService: ILogService, @IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService, @IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService, - @ILanguageService private readonly languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService, + @INotificationService private readonly notificationService: INotificationService, + @ICommandService private readonly commandService: ICommandService ) { super(); this.container = layoutService.mainContainer; - this.settings = new ThemeConfiguration(configurationService, hostColorService); + const isNewUser = this.storageService.isNew(StorageScope.APPLICATION); + this.settings = new ThemeConfiguration(configurationService, hostColorService, isNewUser); this.colorThemeRegistry = this._register(new ThemeRegistry(colorThemesExtPoint, ColorThemeData.fromExtensionTheme)); this.colorThemeWatcher = this._register(new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this))); @@ -190,7 +195,7 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme delayer.schedule(); } - private initialize(): Promise<[IWorkbenchColorTheme | null, IWorkbenchFileIconTheme | null, IWorkbenchProductIconTheme | null]> { + private async initialize(): Promise<[IWorkbenchColorTheme | null, IWorkbenchFileIconTheme | null, IWorkbenchProductIconTheme | null]> { const extDevLocs = this.environmentService.extensionDevelopmentLocationURI; const extDevLoc = extDevLocs && extDevLocs.length === 1 ? extDevLocs[0] : undefined; // in dev mode, switch to a theme provided by the extension under dev. @@ -240,7 +245,87 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme }; - return Promise.all([initializeColorTheme(), initializeFileIconTheme(), initializeProductIconTheme()]); + this.migrateColorThemeSettings(); + await this.migrateAutoDetectColorScheme(); + const result = await Promise.all([initializeColorTheme(), initializeFileIconTheme(), initializeProductIconTheme()]); + this.showNewDefaultThemeNotification(); + return result; + } + + private static readonly NEW_THEME_NOTIFICATION_KEY = 'workbench.newDefaultThemeNotification'; + + private showNewDefaultThemeNotification(): void { + const newDefaultThemes = new Set([ThemeSettingDefaults.COLOR_THEME_DARK, ThemeSettingDefaults.COLOR_THEME_LIGHT]); + if (newDefaultThemes.has(this.currentColorTheme.settingsId)) { + return; // already using a new default theme + } + if (this.storageService.getBoolean(WorkbenchThemeService.NEW_THEME_NOTIFICATION_KEY, StorageScope.APPLICATION)) { + return; // already shown + } + + const handle = this.notificationService.prompt( + Severity.Info, + nls.localize('newDefaultTheme', "New default themes are available for VS Code."), + [{ + label: nls.localize('tryNewTheme', "Try Them Out"), + run: () => this.commandService.executeCommand('workbench.action.tryNewDefaultThemes') + }] + ); + this._register(Event.once(handle.onDidClose)(() => { + this.storageService.store(WorkbenchThemeService.NEW_THEME_NOTIFICATION_KEY, true, StorageScope.APPLICATION, StorageTarget.USER); + })); + } + + /** + * Migrates legacy theme setting values to their current equivalents, + * writing back the migrated value so settings sync distributes the correct ID. + */ + private migrateColorThemeSettings(): void { + const themeSettings = [ + ThemeSettings.COLOR_THEME, + ThemeSettings.PREFERRED_DARK_THEME, + ThemeSettings.PREFERRED_LIGHT_THEME, + ThemeSettings.PREFERRED_HC_DARK_THEME, + ThemeSettings.PREFERRED_HC_LIGHT_THEME, + ]; + for (const key of themeSettings) { + const inspection = this.configurationService.inspect(key); + for (const [target, value] of [ + [ConfigurationTarget.USER, inspection.userValue], + [ConfigurationTarget.USER_REMOTE, inspection.userRemoteValue], + [ConfigurationTarget.WORKSPACE, inspection.workspaceValue], + ] as const) { + if (value) { + const migrated = migrateThemeSettingsId(value); + if (migrated !== value) { + this.configurationService.updateValue(key, migrated, target); + } + } + } + } + } + + /** + * For new users who haven't explicitly configured `window.autoDetectColorScheme`, + * persist `true` so that auto-detect becomes the default going forward. + */ + private async migrateAutoDetectColorScheme(): Promise { + if (!this.storageService.isNew(StorageScope.APPLICATION)) { + return; + } + + // Ensure that user data (including synced settings) has finished initializing + // so we do not overwrite values that arrive via settings sync. + await this.userDataInitializationService.whenInitializationFinished(); + + const inspection = this.configurationService.inspect(ThemeSettings.DETECT_COLOR_SCHEME); + + // Treat any of userValue, userLocalValue, or userRemoteValue as an explicit configuration. + if (inspection.userValue === undefined + && inspection.userLocalValue === undefined + && inspection.userRemoteValue === undefined) { + await this.configurationService.updateValue(ThemeSettings.DETECT_COLOR_SCHEME, true, ConfigurationTarget.USER); + } } private installConfigurationListener() { diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 60e2174d19d..e364109cdae 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -282,7 +282,19 @@ const colorSchemeToPreferred = { }; export class ThemeConfiguration { - constructor(private configurationService: IConfigurationService, private hostColorService: IHostColorSchemeService) { + constructor(private configurationService: IConfigurationService, private hostColorService: IHostColorSchemeService, private readonly isNewUser: boolean = false) { + } + + private shouldAutoDetectColorScheme(): boolean { + const { value, userValue, userLocalValue, userRemoteValue } = this.configurationService.inspect(ThemeSettings.DETECT_COLOR_SCHEME); + if (value) { + return true; + } + if (this.isNewUser) { + const hasUserScopedValue = userValue !== undefined || userLocalValue !== undefined || userRemoteValue !== undefined; + return !hasUserScopedValue; + } + return false; } public get colorTheme(): string { @@ -336,14 +348,14 @@ export class ThemeConfiguration { if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && this.hostColorService.highContrast) { return this.hostColorService.dark ? ColorScheme.HIGH_CONTRAST_DARK : ColorScheme.HIGH_CONTRAST_LIGHT; } - if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { + if (this.shouldAutoDetectColorScheme()) { return this.hostColorService.dark ? ColorScheme.DARK : ColorScheme.LIGHT; } return undefined; } public isDetectingColorScheme(): boolean { - return this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME); + return this.shouldAutoDetectColorScheme(); } public getColorThemeSettingId(): ThemeSettings { diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index aa8af78316c..e54c6549bc2 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -8,7 +8,7 @@ import * as nls from '../../../../nls.js'; import * as types from '../../../../base/common/types.js'; import * as resources from '../../../../base/common/resources.js'; import { ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js'; -import { ExtensionData, IThemeExtensionPoint } from './workbenchThemeService.js'; +import { ExtensionData, IThemeExtensionPoint, migrateThemeSettingsId } from './workbenchThemeService.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; @@ -39,11 +39,11 @@ export function registerColorThemeExtensionPoint() { type: 'string' }, uiTheme: { - description: nls.localize('vscode.extension.contributes.themes.uiTheme', 'Base theme defining the colors around the editor: \'vs\' is the light color theme, \'vs-dark\' is the dark color theme. \'hc-black\' is the dark high contrast theme, \'hc-light\' is the light high contrast theme.'), + markdownDescription: nls.localize('vscode.extension.contributes.themes.uiTheme', 'Base theme defining the colors around the editor: `vs` is the light color theme, `vs-dark` is the dark color theme. `hc-black` is the dark high contrast theme, `hc-light` is the light high contrast theme.'), enum: [ThemeTypeSelector.VS, ThemeTypeSelector.VS_DARK, ThemeTypeSelector.HC_BLACK, ThemeTypeSelector.HC_LIGHT] }, path: { - description: nls.localize('vscode.extension.contributes.themes.path', 'Path of the tmTheme file. The path is relative to the extension folder and is typically \'./colorthemes/awesome-color-theme.json\'.'), + markdownDescription: nls.localize('vscode.extension.contributes.themes.path', 'Path of the tmTheme file. The path is relative to the extension folder and is typically `./colorthemes/awesome-color-theme.json`.'), type: 'string' } }, @@ -267,13 +267,14 @@ export class ThemeRegistry implements IDisposable { } public findThemeBySettingsId(settingsId: string | null, defaultSettingsId?: string): T | undefined { - if (this.builtInTheme && this.builtInTheme.settingsId === settingsId) { + const migratedId = settingsId ? migrateThemeSettingsId(settingsId) : settingsId; + if (this.builtInTheme && this.builtInTheme.settingsId === migratedId) { return this.builtInTheme; } const allThemes = this.getThemes(); let defaultTheme: T | undefined = undefined; for (const t of allThemes) { - if (t.settingsId === settingsId) { + if (t.settingsId === migratedId) { return t; } if (t.settingsId === defaultSettingsId) { diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 8fefa7297ca..040489ccad0 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -11,7 +11,6 @@ import { ConfigurationTarget } from '../../../../platform/configuration/common/c import { isBoolean, isString } from '../../../../base/common/types.js'; import { IconContribution, IconDefinition } from '../../../../platform/theme/common/iconRegistry.js'; import { ColorScheme, ThemeTypeSelector } from '../../../../platform/theme/common/theme.js'; -import product from '../../../../platform/product/common/product.js'; export const IWorkbenchThemeService = refineServiceDecorator(IThemeService); @@ -39,21 +38,33 @@ export enum ThemeSettings { SYSTEM_COLOR_THEME = 'window.systemColorTheme' } -const isOSS = !product.quality; - export namespace ThemeSettingDefaults { - export const COLOR_THEME_DARK = isOSS ? 'Experimental Dark' : 'Default Dark Modern'; - export const COLOR_THEME_LIGHT = isOSS ? 'Experimental Light' : 'Default Light Modern'; + export const COLOR_THEME_DARK = 'VS Code Dark'; + export const COLOR_THEME_LIGHT = 'VS Code Light'; export const COLOR_THEME_HC_DARK = 'Default High Contrast'; export const COLOR_THEME_HC_LIGHT = 'Default High Contrast Light'; - export const COLOR_THEME_DARK_OLD = 'Default Dark+'; - export const COLOR_THEME_LIGHT_OLD = 'Default Light+'; - export const FILE_ICON_THEME = 'vs-seti'; export const PRODUCT_ICON_THEME = 'Default'; } +/** + * Migrates legacy theme settings IDs to their current equivalents. + * Theme IDs were simplified: "Default" prefix was removed from built-in themes, + * and "Experimental" prefix was replaced when VS Code themes became GA. + */ +export function migrateThemeSettingsId(settingsId: string): string { + switch (settingsId) { + case 'Default Dark Modern': return 'Dark Modern'; + case 'Default Light Modern': return 'Light Modern'; + case 'Default Dark+': return 'Dark+'; + case 'Default Light+': return 'Light+'; + case 'Experimental Dark': return 'VS Code Dark'; + case 'Experimental Light': return 'VS Code Light'; + } + return settingsId; +} + export const COLOR_THEME_DARK_INITIAL_COLORS = { 'actionBar.toggledBackground': '#383a49', 'activityBar.activeBorder': '#0078D4', diff --git a/src/vs/workbench/services/themes/test/common/workbenchThemeService.test.ts b/src/vs/workbench/services/themes/test/common/workbenchThemeService.test.ts new file mode 100644 index 00000000000..bdb41d71db5 --- /dev/null +++ b/src/vs/workbench/services/themes/test/common/workbenchThemeService.test.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { migrateThemeSettingsId } from '../../common/workbenchThemeService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ThemeConfiguration } from '../../common/themeConfiguration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IHostColorSchemeService } from '../../common/hostColorSchemeService.js'; +import { ColorScheme } from '../../../../../platform/theme/common/theme.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IConfigurationOverrides, IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; + +suite('WorkbenchThemeService', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('migrateThemeSettingsId', () => { + + test('migrates Default-prefixed theme IDs', () => { + assert.deepStrictEqual( + ['Default Dark Modern', 'Default Light Modern', 'Default Dark+', 'Default Light+'].map(migrateThemeSettingsId), + ['Dark Modern', 'Light Modern', 'Dark+', 'Light+'] + ); + }); + + test('migrates Experimental theme IDs to VS Code themes', () => { + assert.deepStrictEqual( + ['Experimental Dark', 'Experimental Light'].map(migrateThemeSettingsId), + ['VS Code Dark', 'VS Code Light'] + ); + }); + + test('returns unknown IDs unchanged', () => { + assert.deepStrictEqual( + ['Dark Modern', 'VS Code Dark', 'Some Custom Theme', ''].map(migrateThemeSettingsId), + ['Dark Modern', 'VS Code Dark', 'Some Custom Theme', ''] + ); + }); + }); + + suite('ThemeConfiguration', () => { + + function createHostColorSchemeService(dark: boolean, highContrast: boolean = false): IHostColorSchemeService { + return { + _serviceBrand: undefined, + dark, + highContrast, + onDidChangeColorScheme: Event.None, + }; + } + + /** + * Creates a config service that separates the resolved value from the user value, + * matching production behaviour where getValue() returns the schema default + * while inspect().userValue is undefined when no explicit user value is set. + */ + function createConfigServiceWithDefaults( + userConfig: Record, + defaults: Record + ): TestConfigurationService { + const configService = new TestConfigurationService(userConfig); + const originalInspect = configService.inspect.bind(configService); + configService.inspect = (key: string, overrides?: IConfigurationOverrides): IConfigurationValue => { + if (Object.prototype.hasOwnProperty.call(userConfig, key)) { + return originalInspect(key, overrides); + } + // No explicit user value: return the default as the resolved value + const defaultVal = defaults[key] as T; + return { + value: defaultVal, + defaultValue: defaultVal, + userValue: undefined, + userLocalValue: undefined, + }; + }; + const originalGetValue = configService.getValue.bind(configService); + configService.getValue = (arg1?: string | IConfigurationOverrides, arg2?: IConfigurationOverrides): T => { + const result = originalGetValue(arg1, arg2); + if (result === undefined && typeof arg1 === 'string' && Object.prototype.hasOwnProperty.call(defaults, arg1)) { + return defaults[arg1] as T; + } + return result as T; + }; + return configService; + } + + test('new user with no explicit setting gets auto-detect on light OS', () => { + const configService = new TestConfigurationService(); + const hostColor = createHostColorSchemeService(false); + const config = new ThemeConfiguration(configService, hostColor, true); + + assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT); + assert.deepStrictEqual(config.isDetectingColorScheme(), true); + }); + + test('new user with no explicit setting gets auto-detect on dark OS', () => { + const configService = new TestConfigurationService(); + const hostColor = createHostColorSchemeService(true); + const config = new ThemeConfiguration(configService, hostColor, true); + + assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.DARK); + assert.deepStrictEqual(config.isDetectingColorScheme(), true); + }); + + test('new user with no explicit setting and schema default false still gets auto-detect', () => { + // Simulates production: getValue() returns false (schema default) but userValue is undefined + const configService = createConfigServiceWithDefaults({}, { 'window.autoDetectColorScheme': false }); + const hostColor = createHostColorSchemeService(false); + const config = new ThemeConfiguration(configService, hostColor, true); + + assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT); + assert.deepStrictEqual(config.isDetectingColorScheme(), true); + }); + + test('new user with explicit false does not get auto-detect', () => { + const configService = new TestConfigurationService({ 'window.autoDetectColorScheme': false }); + const hostColor = createHostColorSchemeService(false); + const config = new ThemeConfiguration(configService, hostColor, true); + + assert.deepStrictEqual(config.getPreferredColorScheme(), undefined); + assert.deepStrictEqual(config.isDetectingColorScheme(), false); + }); + + test('existing user without explicit setting does not get auto-detect', () => { + const configService = new TestConfigurationService(); + const hostColor = createHostColorSchemeService(false); + const config = new ThemeConfiguration(configService, hostColor, false); + + assert.deepStrictEqual(config.getPreferredColorScheme(), undefined); + assert.deepStrictEqual(config.isDetectingColorScheme(), false); + }); + + test('existing user with explicit true gets auto-detect', () => { + const configService = new TestConfigurationService({ 'window.autoDetectColorScheme': true }); + const hostColor = createHostColorSchemeService(false); + const config = new ThemeConfiguration(configService, hostColor, false); + + assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT); + assert.deepStrictEqual(config.isDetectingColorScheme(), true); + }); + + test('high contrast OS takes priority over auto-detect for new user', () => { + const configService = new TestConfigurationService({ 'window.autoDetectHighContrast': true }); + const hostColor = createHostColorSchemeService(true, true); + const config = new ThemeConfiguration(configService, hostColor, true); + + assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.HIGH_CONTRAST_DARK); + assert.deepStrictEqual(config.isDetectingColorScheme(), true); + }); + + test('high contrast light OS takes priority over auto-detect for new user', () => { + const configService = new TestConfigurationService({ 'window.autoDetectHighContrast': true }); + const hostColor = createHostColorSchemeService(false, true); + const config = new ThemeConfiguration(configService, hostColor, true); + + assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.HIGH_CONTRAST_LIGHT); + assert.deepStrictEqual(config.isDetectingColorScheme(), true); + }); + }); +}); diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index db73853690f..a45bd6dc1af 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -432,7 +432,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - const context = await this.requestService.request({ type: 'GET', url: resource.toString(true) }, CancellationToken.None); + const context = await this.requestService.request({ type: 'GET', url: resource.toString(true), callSite: 'userDataProfileImportExportService.resolveContent' }, CancellationToken.None); if (context.res.statusCode === 200) { return await asText(context); } else { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileInit.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileInit.ts index 6df19e5a98e..8e2b632d1fe 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileInit.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileInit.ts @@ -130,7 +130,7 @@ export class UserDataProfileInitializer implements IUserDataInitializer { } try { const url = URI.revive(this.environmentService.options.profile.contents).toString(true); - const context = await this.requestService.request({ type: 'GET', url }, CancellationToken.None); + const context = await this.requestService.request({ type: 'GET', url, callSite: 'userDataProfileInit.initializeProfile' }, CancellationToken.None); if (context.res.statusCode === 200) { return await asJson(context); } else { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index cc56d9a6ac5..43da4619741 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -161,7 +161,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse async getBuiltinProfileTemplates(): Promise { if (this.productService.profileTemplatesUrl) { try { - const context = await this.requestService.request({ type: 'GET', url: this.productService.profileTemplatesUrl }, CancellationToken.None); + const context = await this.requestService.request({ type: 'GET', url: this.productService.profileTemplatesUrl, callSite: 'userDataProfileManagement.getProfileTemplates' }, CancellationToken.None); if (context.res.statusCode === 200) { return (await asJson(context)) || []; } else { diff --git a/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts b/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts index 2c9e8f3f8e8..8ed090ac5b3 100644 --- a/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts +++ b/src/vs/workbench/services/workspaces/browser/workspaceTrustEditorInput.ts @@ -18,7 +18,7 @@ export class WorkspaceTrustEditorInput extends EditorInput { static readonly ID: string = 'workbench.input.workspaceTrust'; override get capabilities(): EditorInputCapabilities { - return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + return super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.RequiresModal; } override get typeId(): string { diff --git a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts index 2b098ab3158..2fcc86f53b8 100644 --- a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts @@ -135,7 +135,7 @@ function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionS }, }); - const renderer = instantiationService.createInstance(AgentSessionSectionRenderer); + const renderer = instantiationService.createInstance(AgentSessionSectionRenderer, {}); container.style.width = '350px'; container.style.height = 'auto'; diff --git a/src/vs/workbench/test/browser/componentFixtures/aiCustomizationListWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/aiCustomizationListWidget.fixture.ts new file mode 100644 index 00000000000..bafcf3d70ba --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/aiCustomizationListWidget.fixture.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ResourceSet } from '../../../../base/common/map.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IListService, ListService } from '../../../../platform/list/browser/listService.js'; +import { IWorkspace, IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { CustomizationHarness, ICustomizationHarnessService, IHarnessDescriptor, createVSCodeHarnessDescriptor } from '../../../contrib/chat/common/customizationHarnessService.js'; +import { IAgentPluginService } from '../../../contrib/chat/common/plugins/agentPluginService.js'; +import { IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js'; +import { PromptsType } from '../../../contrib/chat/common/promptSyntax/promptTypes.js'; +import { IPromptsService, IResolvedAgentFile, AgentFileType, PromptsStorage, IPromptPath } from '../../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { AICustomizationManagementSection } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { AICustomizationListWidget } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationListWidget.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IPathService } from '../../../services/path/common/pathService.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; +import { ParsedPromptFile, PromptHeader } from '../../../contrib/chat/common/promptSyntax/promptFileParser.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { isEqual } from '../../../../base/common/resources.js'; + +// Ensure color registrations are loaded +import '../../../../platform/theme/common/colors/inputColors.js'; +import '../../../../platform/theme/common/colors/listColors.js'; + +// ============================================================================ +// Mock helpers +// ============================================================================ + +const defaultFilter: IStorageSourceFilter = { + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension, PromptsStorage.plugin], +}; + +interface IFixtureInstructionFile { + readonly promptPath: IPromptPath; + readonly name?: string; + readonly description?: string; + readonly applyTo?: string; /** If set, this instruction file has an applyTo pattern that controls automatic inclusion when the context matches (or `**` for always). */ +} + +function createMockPromptsService(instructionFiles: IFixtureInstructionFile[], agentInstructionFiles: IResolvedAgentFile[] = []): IPromptsService { + return new class extends mock() { + override readonly onDidChangeCustomAgents = Event.None; + override readonly onDidChangeSlashCommands = Event.None; + override readonly onDidChangeSkills = Event.None; + override getDisabledPromptFiles(): ResourceSet { return new ResourceSet(); } + override async listPromptFiles(type: PromptsType) { + if (type === PromptsType.instructions) { + return instructionFiles.map(f => f.promptPath); + } + return []; + } + override async listAgentInstructions() { return agentInstructionFiles; } + override async getCustomAgents() { return []; } + override async parseNew(uri: URI): Promise { + const file = instructionFiles.find(f => isEqual(f.promptPath.uri, uri)); + const headerLines = []; + headerLines.push('---\n'); + if (file) { + if (file.name) { + headerLines.push(`name: ${file.name}\n`); + } + if (file.description) { + headerLines.push(`description: ${file.description}\n`); + } + if (file.applyTo) { + headerLines.push(`applyTo: "${file.applyTo}"\n`); + } + } + headerLines.push('---\n'); + const header = new PromptHeader( + new Range(2, 1, headerLines.length, 1), + uri, + headerLines + ); + return new ParsedPromptFile(uri, header); + } + }(); +} + +function createMockWorkspaceService(): IAICustomizationWorkspaceService { + const activeProjectRoot = observableValue('mockActiveProjectRoot', URI.file('/workspace')); + return new class extends mock() { + override readonly isSessionsWindow = false; + override readonly activeProjectRoot = activeProjectRoot; + override readonly hasOverrideProjectRoot = observableValue('hasOverride', false); + override getActiveProjectRoot() { return URI.file('/workspace'); } + override getStorageSourceFilter() { return defaultFilter; } + }(); +} + +function createMockHarnessService(): ICustomizationHarnessService { + const descriptor = createVSCodeHarnessDescriptor([PromptsStorage.extension]); + return new class extends mock() { + override readonly activeHarness = observableValue('activeHarness', CustomizationHarness.VSCode); + override readonly availableHarnesses = observableValue('harnesses', [descriptor]); + override getStorageSourceFilter() { return defaultFilter; } + override getActiveDescriptor() { return descriptor; } + override registerContributedHarness() { return { dispose() { } }; } + }(); +} + +function createMockWorkspaceContextService(): IWorkspaceContextService { + return new class extends mock() { + override readonly onDidChangeWorkspaceFolders = Event.None; + override getWorkspace(): IWorkspace { + return { id: 'test', folders: [] }; + } + }(); +} + +// ============================================================================ +// Render helper +// ============================================================================ + +async function renderInstructionsTab(ctx: ComponentFixtureContext, instructionFiles: IFixtureInstructionFile[], agentInstructionFiles: IResolvedAgentFile[] = []): Promise { + const width = 500; + const height = 400; + ctx.container.style.width = `${width}px`; + ctx.container.style.height = `${height}px`; + + const contextMenuService = new class extends mock() { + override onDidShowContextMenu = Event.None; + override onDidHideContextMenu = Event.None; + override showContextMenu(): void { } + }; + + const contextViewService = new class extends mock() { + override anchorAlignment = 0; + override showContextView() { return { close: () => { } }; } + override hideContextView(): void { } + override getContextViewElement(): HTMLElement { return ctx.container; } + override layout(): void { } + }; + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + reg.defineInstance(IContextMenuService, contextMenuService); + reg.defineInstance(IContextViewService, contextViewService); + registerWorkbenchServices(reg); + reg.define(IListService, ListService); + reg.defineInstance(IPromptsService, createMockPromptsService(instructionFiles, agentInstructionFiles)); + reg.defineInstance(IAICustomizationWorkspaceService, createMockWorkspaceService()); + reg.defineInstance(ICustomizationHarnessService, createMockHarnessService()); + reg.defineInstance(IWorkspaceContextService, createMockWorkspaceContextService()); + reg.defineInstance(IChatSessionsService, new class extends mock() { + override readonly onDidChangeCustomizations = Event.None; + override async getCustomizations() { return undefined; } + override getRegisteredChatSessionItemProviders() { return []; } + }()); + reg.defineInstance(IAgentPluginService, new class extends mock() { + override readonly plugins = observableValue('plugins', []); + }()); + reg.defineInstance(IFileService, new class extends mock() { + override readonly onDidFilesChange = Event.None; + }()); + reg.defineInstance(IProductService, new class extends mock() { }()); + reg.defineInstance(IPathService, new class extends mock() { + override readonly defaultUriScheme = 'file'; + override userHome(): URI; + override userHome(): Promise; + override userHome(): URI | Promise { return URI.file('/home/dev'); } + }()); + }, + }); + + const widget = ctx.disposableStore.add( + instantiationService.createInstance(AICustomizationListWidget) + ); + ctx.container.appendChild(widget.element); + await widget.setSection(AICustomizationManagementSection.Instructions); + widget.layout(height, width); +} + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ path: 'chat/aiCustomizations/' }, { + + InstructionsTabWithItems: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderInstructionsTab(ctx, [ + // Always-active instructions (no applyTo) + { promptPath: { uri: URI.file('/workspace/.github/instructions/coding-standards.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions }, name: 'Coding Standards', description: 'Repository-wide coding standards' }, + { promptPath: { uri: URI.file('/home/dev/.copilot/instructions/my-style.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions }, name: 'My Style', description: 'Personal coding style preferences' }, + // Always-included instruction (applyTo: **) + { promptPath: { uri: URI.file('/workspace/.github/instructions/general-guidelines.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions }, name: 'General Guidelines', description: 'General development guidelines', applyTo: '**' }, + // On-demand instructions (with applyTo pattern) + { promptPath: { uri: URI.file('/workspace/.github/instructions/testing-guidelines.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions }, name: 'Testing Guidelines', description: 'Testing best practices', applyTo: '**/*.test.ts' }, + { promptPath: { uri: URI.file('/workspace/.github/instructions/security-review.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions }, name: 'Security Review', description: 'Security review checklist', applyTo: 'src/auth/**' }, + { promptPath: { uri: URI.file('/home/dev/.copilot/instructions/typescript-rules.instructions.md'), storage: PromptsStorage.extension, type: PromptsType.instructions, extension: undefined!, source: undefined! }, name: 'TypeScript Rules', description: 'TypeScript conventions', applyTo: '**/*.ts' }, + ], [ + // Agent instruction files (AGENTS.md, copilot-instructions.md) + { uri: URI.file('/workspace/AGENTS.md'), realPath: undefined, type: AgentFileType.agentsMd }, + { uri: URI.file('/workspace/.github/copilot-instructions.md'), realPath: undefined, type: AgentFileType.copilotInstructionsMd }, + ]), + }), + + InstructionsTabEmpty: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderInstructionsTab(ctx, []), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/aiCustomizationManagementEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/aiCustomizationManagementEditor.fixture.ts new file mode 100644 index 00000000000..3837c0cd369 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/aiCustomizationManagementEditor.fixture.ts @@ -0,0 +1,939 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from '../../../../base/browser/dom.js'; +import { Dimension } from '../../../../base/browser/dom.js'; +import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js'; +import { mainWindow } from '../../../../base/browser/window.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Event } from '../../../../base/common/event.js'; +import { ResourceMap, ResourceSet } from '../../../../base/common/map.js'; +import { constObservable, observableValue } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IListService, ListService } from '../../../../platform/list/browser/listService.js'; +import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; +import { IRequestService } from '../../../../platform/request/common/request.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { IWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { IPathService } from '../../../services/path/common/pathService.js'; +import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js'; +import { IWebviewService } from '../../../contrib/webview/browser/webview.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../../contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { CustomizationHarness, ICustomizationHarnessService, IHarnessDescriptor, createVSCodeHarnessDescriptor, createClaudeHarnessDescriptor, createCliHarnessDescriptor, getCliUserRoots, getClaudeUserRoots } from '../../../contrib/chat/common/customizationHarnessService.js'; +import { IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js'; +import { PromptsType } from '../../../contrib/chat/common/promptSyntax/promptTypes.js'; +import { IPromptsService, IResolvedAgentFile, AgentFileType, PromptsStorage, IAgentSkill, IChatPromptSlashCommand } from '../../../contrib/chat/common/promptSyntax/service/promptsService.js'; +import { ParsedPromptFile } from '../../../contrib/chat/common/promptSyntax/promptFileParser.js'; +import { IAgentPluginService, IAgentPlugin } from '../../../contrib/chat/common/plugins/agentPluginService.js'; +import { IPluginMarketplaceService, IMarketplacePlugin, MarketplaceType, PluginSourceKind } from '../../../contrib/chat/common/plugins/pluginMarketplaceService.js'; +import { MarketplaceReferenceKind } from '../../../contrib/chat/common/plugins/marketplaceReference.js'; +import { IPluginInstallService } from '../../../contrib/chat/common/plugins/pluginInstallService.js'; +import { AICustomizationManagementEditor } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; +import { ContributionEnablementState } from '../../../contrib/chat/common/enablement.js'; +import { AICustomizationManagementEditorInput } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; +import { IMcpWorkbenchService, IWorkbenchMcpServer, IMcpService, McpServerInstallState } from '../../../contrib/mcp/common/mcpTypes.js'; +import { IMcpRegistry } from '../../../contrib/mcp/common/mcpRegistryTypes.js'; +import { IWorkbenchLocalMcpServer, LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js'; +import { McpListWidget } from '../../../contrib/chat/browser/aiCustomization/mcpListWidget.js'; +import { PluginListWidget } from '../../../contrib/chat/browser/aiCustomization/pluginListWidget.js'; +import { IIterativePager } from '../../../../base/common/paging.js'; +// eslint-disable-next-line local/code-import-patterns +import { IAgentFeedbackService } from '../../../../sessions/contrib/agentFeedback/browser/agentFeedbackService.js'; +// eslint-disable-next-line local/code-import-patterns +import { CodeReviewStateKind, ICodeReviewService, ICodeReviewState, IPRReviewState, PRReviewStateKind } from '../../../../sessions/contrib/codeReview/browser/codeReviewService.js'; +import { IChatEditingService } from '../../../contrib/chat/common/editing/chatEditingService.js'; +import { IAgentSessionsService } from '../../../contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; + +// Ensure theme colors & widget CSS are loaded +import '../../../../platform/theme/common/colors/inputColors.js'; +import '../../../../platform/theme/common/colors/listColors.js'; +import '../../../contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css'; + +// ============================================================================ +// Mock helpers +// ============================================================================ + +const userHome = URI.file('/home/dev'); +const BUILTIN_STORAGE = 'builtin'; + +interface IFixtureFile { + readonly uri: URI; + readonly storage: PromptsStorage; + readonly type: PromptsType; + readonly name?: string; + readonly description?: string; + readonly applyTo?: string; + readonly extensionId?: string; + readonly extensionDisplayName?: string; +} + +function createMockEditorGroup(): IEditorGroup { + return new class extends mock() { + override windowId = mainWindow.vscodeWindowId; + }(); +} + +function toExtensionInfo(file: IFixtureFile): { identifier: ExtensionIdentifier; displayName?: string } | undefined { + if (!file.extensionId) { + return undefined; + } + + return { + identifier: new ExtensionIdentifier(file.extensionId), + displayName: file.extensionDisplayName, + }; +} + +function createMockPromptsService(files: IFixtureFile[], agentInstructions: IResolvedAgentFile[]): IPromptsService { + const applyToMap = new ResourceMap(); + const descriptionMap = new ResourceMap(); + for (const f of files) { applyToMap.set(f.uri, f.applyTo); descriptionMap.set(f.uri, f.description); } + return new class extends mock() { + override readonly onDidChangeCustomAgents = Event.None; + override readonly onDidChangeSlashCommands = Event.None; + override readonly onDidChangeSkills = Event.None; + override readonly onDidChangeInstructions = Event.None; + override getDisabledPromptFiles(): ResourceSet { return new ResourceSet(); } + override async listPromptFiles(type: PromptsType, _token: CancellationToken) { + return files.filter(f => f.type === type).map(f => ({ + uri: f.uri, + storage: f.storage as PromptsStorage.local, + type: f.type, + name: f.name, + description: f.description, + extension: toExtensionInfo(f) as never, + })); + } + override async listAgentInstructions() { return agentInstructions; } + override async getCustomAgents() { + return files.filter(f => f.type === PromptsType.agent).map(a => ({ + uri: a.uri, name: a.name ?? 'agent', description: a.description, storage: a.storage, + source: { + storage: a.storage, + extensionId: a.extensionId ? new ExtensionIdentifier(a.extensionId) : undefined, + }, + })) as never[]; + } + override async parseNew(uri: URI, _token: CancellationToken): Promise { + const header = { + get applyTo() { return applyToMap.get(uri); }, + get description() { return descriptionMap.get(uri); }, + }; + return new ParsedPromptFile(uri, header as never); + } + override async getSourceFolders() { return [] as never[]; } + override async findAgentSkills(): Promise { + return files.filter(f => f.type === PromptsType.skill).map(f => ({ + uri: f.uri, + storage: f.storage, + name: f.name ?? 'skill', + description: f.description, + disableModelInvocation: false, + userInvocable: true, + when: undefined, + })); + } + override async getPromptSlashCommands(): Promise { + const promptFiles = files.filter(f => f.type === PromptsType.prompt); + const commands = await Promise.all(promptFiles.map(async f => { + const promptPath = { uri: f.uri, storage: f.storage, type: f.type, extension: toExtensionInfo(f) as never }; + const parsedPromptFile = await this.parseNew(f.uri, CancellationToken.None); + return { + name: f.name ?? 'prompt', + description: f.description, + argumentHint: undefined, + promptPath: promptPath as IChatPromptSlashCommand['promptPath'], + parsedPromptFile, + when: undefined, + }; + })); + return commands; + } + }(); +} + +function createMockHarnessService(activeHarness: CustomizationHarness, descriptors: readonly IHarnessDescriptor[]): ICustomizationHarnessService { + const active = observableValue('activeHarness', activeHarness); + return new class extends mock() { + override readonly activeHarness = active; + override readonly availableHarnesses = constObservable(descriptors); + override getStorageSourceFilter(type: PromptsType) { + const d = descriptors.find(h => h.id === active.get()) ?? descriptors[0]; + return d.getStorageSourceFilter(type); + } + override getActiveDescriptor() { + return descriptors.find(h => h.id === active.get()) ?? descriptors[0]; + } + override setActiveHarness(id: string) { active.set(id, undefined); } + override registerContributedHarness() { return { dispose() { } }; } + }(); +} + +function makeLocalMcpServer(id: string, label: string, scope: LocalMcpServerScope, description?: string): IWorkbenchMcpServer { + return new class extends mock() { + override readonly id = id; + override readonly name = id; + override readonly label = label; + override readonly description = description ?? ''; + override readonly installState = McpServerInstallState.Installed; + override readonly local = new class extends mock() { + override readonly id = id; + override readonly scope = scope; + }(); + }(); +} + +function createMockAgentFeedbackService(): IAgentFeedbackService { + return new class extends mock() { + override readonly onDidChangeFeedback = Event.None; + override readonly onDidChangeNavigation = Event.None; + override getFeedback() { return []; } + override getMostRecentSessionForResource() { return undefined; } + override async revealFeedback(): Promise { } + override getNextFeedback() { return undefined; } + override getNavigationBearing() { return { activeIdx: -1, totalCount: 0 }; } + override getNextNavigableItem() { return undefined; } + override setNavigationAnchor(): void { } + override clearFeedback(): void { } + override removeFeedback(): void { } + override async addFeedbackAndSubmit(): Promise { } + }(); +} + +function createMockCodeReviewService(): ICodeReviewService { + return new class extends mock() { + private readonly reviewState = observableValue('fixture.reviewState', { kind: CodeReviewStateKind.Idle }); + private readonly prReviewState = observableValue('fixture.prReviewState', { kind: PRReviewStateKind.None }); + + override getReviewState() { + return this.reviewState; + } + + override getPRReviewState() { + return this.prReviewState; + } + + override hasReview(): boolean { + return false; + } + + override requestReview(): void { } + override removeComment(): void { } + override dismissReview(): void { } + override async resolvePRReviewThread(): Promise { } + }(); +} + +// ============================================================================ +// Realistic test data — a project that has Copilot + Claude customizations +// ============================================================================ + +const allFiles: IFixtureFile[] = [ + // Instructions - extension (built-in + third-party) + { uri: URI.file('/extensions/github.copilot-chat/instructions/coding.instructions.md'), storage: PromptsStorage.extension, type: PromptsType.instructions, name: 'Copilot Coding', description: 'Built-in coding guidance', extensionId: 'GitHub.copilot-chat', extensionDisplayName: 'GitHub Copilot Chat' }, + { uri: URI.file('/extensions/acme.tools/instructions/team.instructions.md'), storage: PromptsStorage.extension, type: PromptsType.instructions, name: 'Team Conventions', description: 'Third-party extension instructions', extensionId: 'acme.tools', extensionDisplayName: 'Acme Tools' }, + // Instructions — workspace + { uri: URI.file('/workspace/.github/instructions/coding-standards.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Coding Standards', description: 'Repository-wide coding standards' }, + { uri: URI.file('/workspace/.github/instructions/testing.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Testing', description: 'Testing best practices', applyTo: '**/*.test.ts' }, + { uri: URI.file('/workspace/.github/instructions/security.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Security', description: 'Security review checklist', applyTo: 'src/auth/**' }, + { uri: URI.file('/workspace/.github/instructions/accessibility.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Accessibility', description: 'WCAG compliance guidelines', applyTo: '**/*.tsx' }, + { uri: URI.file('/workspace/.github/instructions/api-design.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'API Design', description: 'REST API design conventions' }, + { uri: URI.file('/workspace/.github/instructions/performance.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Performance', description: 'Performance optimization rules', applyTo: 'src/core/**' }, + { uri: URI.file('/workspace/.github/instructions/error-handling.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Error Handling', description: 'Error handling patterns' }, + { uri: URI.file('/workspace/.github/instructions/database.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Database', description: 'Database migration and query patterns', applyTo: 'src/db/**' }, + // Instructions — user + { uri: URI.file('/home/dev/.copilot/instructions/my-style.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'My Style', description: 'Personal coding style' }, + { uri: URI.file('/home/dev/.copilot/instructions/typescript-rules.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'TypeScript Rules', description: 'Strict TypeScript conventions' }, + { uri: URI.file('/home/dev/.copilot/instructions/commit-messages.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'Commit Messages', description: 'Conventional commit format' }, + // Instructions — Claude rules + { uri: URI.file('/workspace/.claude/rules/code-style.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Code Style', description: 'Claude code style rules' }, + { uri: URI.file('/workspace/.claude/rules/testing.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Testing', description: 'Claude testing conventions' }, + { uri: URI.file('/home/dev/.claude/rules/personal.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'Personal', description: 'Personal rules' }, + // Agents — workspace + { uri: URI.file('/workspace/.github/agents/reviewer.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Reviewer', description: 'Code review agent' }, + { uri: URI.file('/workspace/.github/agents/documenter.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Documenter', description: 'Documentation agent' }, + { uri: URI.file('/workspace/.github/agents/tester.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Tester', description: 'Test generation and validation' }, + { uri: URI.file('/workspace/.github/agents/refactorer.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Refactorer', description: 'Code refactoring specialist' }, + { uri: URI.file('/workspace/.github/agents/security-auditor.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Security Auditor', description: 'Security vulnerability scanner' }, + { uri: URI.file('/workspace/.github/agents/api-designer.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'API Designer', description: 'REST and GraphQL API design' }, + { uri: URI.file('/workspace/.github/agents/performance-tuner.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Performance Tuner', description: 'Performance profiling and optimization' }, + // Agents — user + { uri: URI.file('/home/dev/.copilot/agents/planner.agent.md'), storage: PromptsStorage.user, type: PromptsType.agent, name: 'Planner', description: 'Project planning agent' }, + { uri: URI.file('/home/dev/.copilot/agents/debugger.agent.md'), storage: PromptsStorage.user, type: PromptsType.agent, name: 'Debugger', description: 'Interactive debugging assistant' }, + { uri: URI.file('/home/dev/.copilot/agents/nls-helper.agent.md'), storage: PromptsStorage.user, type: PromptsType.agent, name: 'NLS Helper', description: 'Natural language searching code for clarity' }, + // Agents - extension (built-in + third-party) + { uri: URI.file('/extensions/github.copilot-chat/agents/workspace-guide.agent.md'), storage: PromptsStorage.extension, type: PromptsType.agent, name: 'Workspace Guide', description: 'Built-in workspace exploration agent', extensionId: 'GitHub.copilot-chat', extensionDisplayName: 'GitHub Copilot Chat' }, + { uri: URI.file('/extensions/acme.tools/agents/api-helper.agent.md'), storage: PromptsStorage.extension, type: PromptsType.agent, name: 'API Helper', description: 'Third-party API agent', extensionId: 'acme.tools', extensionDisplayName: 'Acme Tools' }, + // Skills — workspace + { uri: URI.file('/workspace/.github/skills/deploy/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Deploy', description: 'Deployment automation' }, + { uri: URI.file('/workspace/.github/skills/refactor/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Refactor', description: 'Code refactoring patterns' }, + { uri: URI.file('/workspace/.github/skills/unit-tests/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Unit Tests', description: 'Test generation and runner integration' }, + { uri: URI.file('/workspace/.github/skills/ci-fix/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'CI Fix', description: 'Diagnose and fix CI failures' }, + { uri: URI.file('/workspace/.github/skills/migration/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Migration', description: 'Database migration generation' }, + { uri: URI.file('/workspace/.github/skills/accessibility/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Accessibility', description: 'ARIA labels and keyboard navigation' }, + { uri: URI.file('/workspace/.github/skills/docker/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Docker', description: 'Dockerfile and compose generation' }, + { uri: URI.file('/workspace/.github/skills/api-docs/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'API Docs', description: 'OpenAPI spec generation' }, + // Skills — user + { uri: URI.file('/home/dev/.copilot/skills/git-workflow/SKILL.md'), storage: PromptsStorage.user, type: PromptsType.skill, name: 'Git Workflow', description: 'Branch and PR workflows' }, + { uri: URI.file('/home/dev/.copilot/skills/code-review/SKILL.md'), storage: PromptsStorage.user, type: PromptsType.skill, name: 'Code Review', description: 'Structured code review checklist' }, + // Skills - extension (built-in + third-party) + { uri: URI.file('/extensions/github.copilot-chat/skills/workspace/SKILL.md'), storage: PromptsStorage.extension, type: PromptsType.skill, name: 'Workspace Search', description: 'Built-in workspace search skill', extensionId: 'GitHub.copilot-chat', extensionDisplayName: 'GitHub Copilot Chat' }, + { uri: URI.file('/extensions/acme.tools/skills/audit/SKILL.md'), storage: PromptsStorage.extension, type: PromptsType.skill, name: 'Audit', description: 'Third-party audit skill', extensionId: 'acme.tools', extensionDisplayName: 'Acme Tools' }, + // Prompts — workspace + { uri: URI.file('/workspace/.github/prompts/explain.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Explain', description: 'Explain selected code' }, + { uri: URI.file('/workspace/.github/prompts/review.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Review', description: 'Review changes' }, + { uri: URI.file('/workspace/.github/prompts/fix-bug.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Fix Bug', description: 'Diagnose and fix a bug from issue' }, + { uri: URI.file('/workspace/.github/prompts/write-tests.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Write Tests', description: 'Generate unit tests for selection' }, + { uri: URI.file('/workspace/.github/prompts/add-docs.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Add Docs', description: 'Add JSDoc comments to functions' }, + { uri: URI.file('/workspace/.github/prompts/optimize.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Optimize', description: 'Optimize code for performance' }, + { uri: URI.file('/workspace/.github/prompts/convert-to-ts.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Convert to TS', description: 'Convert JavaScript to TypeScript' }, + { uri: URI.file('/workspace/.github/prompts/summarize-pr.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Summarize PR', description: 'Generate PR description from diff' }, + // Prompts — user + { uri: URI.file('/home/dev/.copilot/prompts/translate.prompt.md'), storage: PromptsStorage.user, type: PromptsType.prompt, name: 'Translate', description: 'Translate strings for i18n' }, + { uri: URI.file('/home/dev/.copilot/prompts/commit-msg.prompt.md'), storage: PromptsStorage.user, type: PromptsType.prompt, name: 'Commit Message', description: 'Generate conventional commit' }, + // Prompts - extension (built-in + third-party) + { uri: URI.file('/extensions/github.copilot-chat/prompts/trace.prompt.md'), storage: PromptsStorage.extension, type: PromptsType.prompt, name: 'Trace', description: 'Built-in tracing prompt', extensionId: 'GitHub.copilot-chat', extensionDisplayName: 'GitHub Copilot Chat' }, + { uri: URI.file('/extensions/acme.tools/prompts/lint.prompt.md'), storage: PromptsStorage.extension, type: PromptsType.prompt, name: 'Lint', description: 'Third-party lint prompt', extensionId: 'acme.tools', extensionDisplayName: 'Acme Tools' }, + // Hooks — workspace + { uri: URI.file('/workspace/.github/hooks/pre-commit.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'Pre-Commit Lint', description: 'Run linting before commit' }, + { uri: URI.file('/workspace/.github/hooks/post-save.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'Post-Save Format', description: 'Auto-format on save' }, + { uri: URI.file('/workspace/.github/hooks/on-test-fail.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'On Test Failure', description: 'Suggest fix when tests fail' }, + { uri: URI.file('/workspace/.github/hooks/pre-push.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'Pre-Push Check', description: 'Run type-check before push' }, + { uri: URI.file('/workspace/.github/hooks/post-create.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'Post-Create', description: 'Initialize boilerplate for new files' }, + { uri: URI.file('/workspace/.github/hooks/on-error.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'On Error', description: 'Log and report unhandled errors' }, + { uri: URI.file('/workspace/.github/hooks/post-tool-call.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'Post Tool Call', description: 'Echo confirmation after each tool call' }, + { uri: URI.file('/workspace/.github/hooks/on-build-fail.json'), storage: PromptsStorage.local, type: PromptsType.hook, name: 'On Build Failure', description: 'Auto-diagnose build errors' }, + // Hooks — user + { uri: URI.file('/home/dev/.copilot/hooks/daily-summary.json'), storage: PromptsStorage.user, type: PromptsType.hook, name: 'Daily Summary', description: 'Generate daily work summary' }, + { uri: URI.file('/home/dev/.copilot/hooks/backup-changes.json'), storage: PromptsStorage.user, type: PromptsType.hook, name: 'Backup Changes', description: 'Auto-stash uncommitted changes' }, +]; + +const agentInstructions: IResolvedAgentFile[] = [ + { uri: URI.file('/workspace/AGENTS.md'), realPath: undefined, type: AgentFileType.agentsMd }, + { uri: URI.file('/workspace/CLAUDE.md'), realPath: undefined, type: AgentFileType.claudeMd }, + { uri: URI.file('/workspace/.github/copilot-instructions.md'), realPath: undefined, type: AgentFileType.copilotInstructionsMd }, +]; + +const mcpWorkspaceServers = [ + makeLocalMcpServer('mcp-postgres', 'PostgreSQL', LocalMcpServerScope.Workspace, 'Database access'), + makeLocalMcpServer('mcp-github', 'GitHub', LocalMcpServerScope.Workspace, 'GitHub API'), + makeLocalMcpServer('mcp-redis', 'Redis', LocalMcpServerScope.Workspace, 'In-memory data store'), + makeLocalMcpServer('mcp-docker', 'Docker', LocalMcpServerScope.Workspace, 'Container management'), + makeLocalMcpServer('mcp-slack', 'Slack', LocalMcpServerScope.Workspace, 'Team messaging'), + makeLocalMcpServer('mcp-jira', 'Jira', LocalMcpServerScope.Workspace, 'Issue tracking'), + makeLocalMcpServer('mcp-aws', 'AWS', LocalMcpServerScope.Workspace, 'Amazon Web Services'), + makeLocalMcpServer('mcp-graphql', 'GraphQL', LocalMcpServerScope.Workspace, 'GraphQL API gateway'), +]; +const mcpUserServers = [ + makeLocalMcpServer('mcp-web-search', 'Web Search', LocalMcpServerScope.User, 'Search the web'), + makeLocalMcpServer('mcp-filesystem', 'Filesystem', LocalMcpServerScope.User, 'Local file operations'), + makeLocalMcpServer('mcp-puppeteer', 'Puppeteer', LocalMcpServerScope.User, 'Browser automation'), +]; +const mcpRuntimeServers = [ + { definition: { id: 'github-copilot-mcp', label: 'GitHub Copilot' }, collection: { id: 'ext.github.copilot/mcp', label: 'ext.github.copilot/mcp' }, enablement: constObservable(2), connectionState: constObservable({ state: 2 }) }, +]; + +interface IRenderEditorOptions { + readonly harness: CustomizationHarness; + readonly isSessionsWindow?: boolean; + readonly managementSections?: readonly AICustomizationManagementSection[]; + readonly availableHarnesses?: readonly IHarnessDescriptor[]; + readonly selectedSection?: AICustomizationManagementSection; + readonly scrollToBottom?: boolean; + readonly width?: number; + readonly height?: number; +} + +async function waitForAnimationFrames(count: number): Promise { + for (let i = 0; i < count; i++) { + await new Promise(resolve => mainWindow.requestAnimationFrame(() => resolve())); + } +} + +function getVisibleEditorSignature(container: HTMLElement): string { + const sectionCounts = [...container.querySelectorAll('.section-list-item')].map(item => item.textContent?.replace(/\s+/g, ' ').trim() ?? '').join('|'); + const visibleContent = [...container.querySelectorAll('.prompts-content-container, .mcp-content-container, .plugin-content-container')] + .find(node => node instanceof HTMLElement && node.style.display !== 'none'); + const visibleRows = visibleContent + ? [...visibleContent.querySelectorAll('.monaco-list-row')].map(row => row.textContent?.replace(/\s+/g, ' ').trim() ?? '').join('|') + : ''; + + return `${sectionCounts}@@${visibleRows}`; +} + +async function waitForEditorToSettle(container: HTMLElement): Promise { + let previousSignature = ''; + let stableIterations = 0; + + await new Promise(resolve => setTimeout(resolve, 150)); + + for (let i = 0; i < 20; i++) { + await waitForAnimationFrames(2); + await new Promise(resolve => setTimeout(resolve, 25)); + + const signature = getVisibleEditorSignature(container); + if (signature && signature === previousSignature) { + stableIterations++; + if (stableIterations >= 2) { + return; + } + } else { + stableIterations = 0; + previousSignature = signature; + } + } +} + +async function waitForVisibleScrollbarsToFade(container: HTMLElement): Promise { + const deadline = Date.now() + 4000; + + while (Date.now() < deadline) { + const hasVisibleScrollbar = [...container.querySelectorAll('.scrollbar.vertical')].some(scrollbar => { + const style = mainWindow.getComputedStyle(scrollbar); + return scrollbar.classList.contains('visible') && style.opacity !== '0'; + }); + + if (!hasVisibleScrollbar) { + return; + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +// ============================================================================ +// Render helper — creates the full management editor +// ============================================================================ + +async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditorOptions): Promise { + const width = options.width ?? 900; + const height = options.height ?? 600; + ctx.container.style.width = `${width}px`; + ctx.container.style.height = `${height}px`; + + const isSessionsWindow = options.isSessionsWindow ?? false; + const managementSections = options.managementSections ?? [ + AICustomizationManagementSection.Agents, + AICustomizationManagementSection.Skills, + AICustomizationManagementSection.Instructions, + AICustomizationManagementSection.Hooks, + AICustomizationManagementSection.Prompts, + AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Plugins, + ]; + const availableHarnesses = options.availableHarnesses ?? [ + createVSCodeHarnessDescriptor([PromptsStorage.extension, BUILTIN_STORAGE]), + createCliHarnessDescriptor(getCliUserRoots(userHome), []), + createClaudeHarnessDescriptor(getClaudeUserRoots(userHome), []), + ]; + + const allMcpServers = [...mcpWorkspaceServers, ...mcpUserServers]; + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + const harnessService = createMockHarnessService(options.harness, availableHarnesses); + const agentFeedbackService = createMockAgentFeedbackService(); + const codeReviewService = createMockCodeReviewService(); + registerWorkbenchServices(reg); + reg.define(IListService, ListService); + reg.defineInstance(IAgentFeedbackService, agentFeedbackService); + reg.defineInstance(ICodeReviewService, codeReviewService); + reg.defineInstance(IChatEditingService, new class extends mock() { + override readonly editingSessionsObs = constObservable([]); + }()); + reg.defineInstance(IAgentSessionsService, new class extends mock() { + override readonly model = new class extends mock() { + override readonly sessions = []; + }(); + override getSession() { return undefined; } + }()); + reg.defineInstance(IPromptsService, createMockPromptsService(allFiles, agentInstructions)); + reg.defineInstance(IAICustomizationWorkspaceService, new class extends mock() { + override readonly isSessionsWindow = isSessionsWindow; + override readonly activeProjectRoot = observableValue('root', URI.file('/workspace')); + override readonly hasOverrideProjectRoot = observableValue('hasOverride', false); + override getActiveProjectRoot() { return URI.file('/workspace'); } + override getStorageSourceFilter(type: PromptsType) { return harnessService.getStorageSourceFilter(type); } + override clearOverrideProjectRoot() { } + override setOverrideProjectRoot() { } + override readonly managementSections = managementSections; + override async generateCustomization() { } + }()); + reg.defineInstance(ICustomizationHarnessService, harnessService); + reg.defineInstance(IChatSessionsService, new class extends mock() { + override readonly onDidChangeCustomizations = Event.None; + override async getCustomizations() { return undefined; } + override getRegisteredChatSessionItemProviders() { return []; } + }()); + reg.defineInstance(IWorkspaceContextService, new class extends mock() { + override readonly onDidChangeWorkspaceFolders = Event.None; + override getWorkspace(): IWorkspace { return { id: 'test', folders: [] }; } + override getWorkbenchState(): WorkbenchState { return WorkbenchState.WORKSPACE; } + }()); + reg.defineInstance(IFileService, new class extends mock() { + override readonly onDidFilesChange = Event.None; + }()); + reg.defineInstance(IPathService, new class extends mock() { + override readonly defaultUriScheme = 'file'; + override userHome(): URI; + override userHome(): Promise; + override userHome(): URI | Promise { return userHome; } + }()); + reg.defineInstance(ITextModelService, new class extends mock() { }()); + reg.defineInstance(IWorkingCopyService, new class extends mock() { + override readonly onDidChangeDirty = Event.None; + }()); + reg.defineInstance(IFileDialogService, new class extends mock() { }()); + reg.defineInstance(IExtensionService, new class extends mock() { }()); + reg.defineInstance(IQuickInputService, new class extends mock() { }()); + reg.defineInstance(IRequestService, new class extends mock() { }()); + reg.defineInstance(IMarkdownRendererService, new class extends mock() { + override render() { + const rendered: IRenderedMarkdown = { + element: DOM.$('span'), + dispose() { }, + }; + return rendered; + } + }()); + reg.defineInstance(IWebviewService, new class extends mock() { }()); + reg.defineInstance(IMcpWorkbenchService, new class extends mock() { + override readonly onChange = Event.None; + override readonly onReset = Event.None; + override readonly local = allMcpServers; + override async queryLocal() { return allMcpServers; } + override canInstall() { return true as const; } + }()); + reg.defineInstance(IMcpService, new class extends mock() { + override readonly servers = constObservable(mcpRuntimeServers as never[]); + }()); + reg.defineInstance(IMcpRegistry, new class extends mock() { + override readonly collections = constObservable([]); + override readonly delegates = constObservable([]); + override readonly onDidChangeInputs = Event.None; + }()); + reg.defineInstance(IAgentPluginService, new class extends mock() { + override readonly plugins = constObservable(installedPlugins); + override readonly enablementModel = undefined as never; + }()); + reg.defineInstance(IPluginMarketplaceService, new class extends mock() { + override readonly installedPlugins = constObservable([]); + override readonly onDidChangeMarketplaces = Event.None; + }()); + reg.defineInstance(IPluginInstallService, new class extends mock() { }()); + reg.defineInstance(IProductService, new class extends mock() { + override readonly defaultChatAgent = new class extends mock>() { + override readonly chatExtensionId = 'GitHub.copilot-chat'; + }(); + }()); + }, + }); + + const editor = ctx.disposableStore.add( + instantiationService.createInstance(AICustomizationManagementEditor, createMockEditorGroup()) + ); + editor.create(ctx.container); + editor.layout(new Dimension(width, height)); + + await editor.setInput(AICustomizationManagementEditorInput.getOrCreate(), undefined, {}, CancellationToken.None); + + if (options.selectedSection) { + editor.selectSectionById(options.selectedSection); + } + + await waitForEditorToSettle(ctx.container); + + if (options.scrollToBottom) { + editor.revealLastItem(); + await waitForAnimationFrames(2); + await new Promise(resolve => setTimeout(resolve, 2400)); + await waitForVisibleScrollbarsToFade(ctx.container); + } +} + +// ============================================================================ +// MCP Browse Mode — standalone widget with gallery results +// ============================================================================ + +function makeGalleryServer(id: string, label: string, description: string, publisher: string): IWorkbenchMcpServer { + const galleryStub = new class extends mock>() { }(); + return new class extends mock() { + override readonly id = id; + override readonly name = id; + override readonly label = label; + override readonly description = description; + override readonly publisherDisplayName = publisher; + override readonly installState = McpServerInstallState.Uninstalled; + override readonly gallery = galleryStub; + override readonly local = undefined; + }(); +} + +const galleryServers = [ + makeGalleryServer('gallery-postgres', 'PostgreSQL', 'Access PostgreSQL databases with schema inspection and query tools', 'Microsoft'), + makeGalleryServer('gallery-github', 'GitHub', 'Repository management, issues, pull requests, and code search', 'GitHub'), + makeGalleryServer('gallery-slack', 'Slack', 'Send messages, manage channels, and search workspace history', 'Slack Technologies'), + makeGalleryServer('gallery-docker', 'Docker', 'Container lifecycle management and image operations', 'Docker Inc'), + makeGalleryServer('gallery-filesystem', 'Filesystem', 'Read, write, and navigate local files and directories', 'Microsoft'), + makeGalleryServer('gallery-brave', 'Brave Search', 'Web and local search powered by the Brave Search API', 'Brave Software'), + makeGalleryServer('gallery-puppeteer', 'Puppeteer', 'Browser automation with screenshots, navigation, and form filling', 'Google'), + makeGalleryServer('gallery-memory', 'Memory', 'Knowledge graph for persistent memory across conversations', 'Microsoft'), + makeGalleryServer('gallery-fetch', 'Fetch', 'Retrieve and convert web content to markdown for analysis', 'Microsoft'), + makeGalleryServer('gallery-sentry', 'Sentry', 'Error monitoring, issue tracking, and performance tracing', 'Sentry'), + makeGalleryServer('gallery-sqlite', 'SQLite', 'Query and manage SQLite databases with schema exploration', 'Community'), + makeGalleryServer('gallery-redis', 'Redis', 'In-memory data store operations and key management', 'Redis Ltd'), +]; + +async function renderMcpBrowseMode(ctx: ComponentFixtureContext): Promise { + const width = 650; + const height = 500; + ctx.container.style.width = `${width}px`; + ctx.container.style.height = `${height}px`; + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + reg.define(IListService, ListService); + reg.defineInstance(IMcpWorkbenchService, new class extends mock() { + override readonly onChange = Event.None; + override readonly onReset = Event.None; + override readonly local: IWorkbenchMcpServer[] = []; + override async queryLocal() { return []; } + override canInstall() { return true as const; } + override async queryGallery(): Promise> { + return { + firstPage: { items: galleryServers, hasMore: false }, + async getNextPage() { return { items: [], hasMore: false }; }, + }; + } + }()); + reg.defineInstance(IMcpService, new class extends mock() { + override readonly servers = constObservable([] as never[]); + }()); + reg.defineInstance(IMcpRegistry, new class extends mock() { + override readonly collections = constObservable([]); + override readonly delegates = constObservable([]); + override readonly onDidChangeInputs = Event.None; + }()); + reg.defineInstance(IAgentPluginService, new class extends mock() { + override readonly plugins = constObservable([]); + }()); + reg.defineInstance(IDialogService, new class extends mock() { }()); + reg.defineInstance(IAICustomizationWorkspaceService, new class extends mock() { + override readonly isSessionsWindow = false; + override readonly activeProjectRoot = observableValue('root', URI.file('/workspace')); + override readonly hasOverrideProjectRoot = observableValue('hasOverride', false); + override getActiveProjectRoot() { return URI.file('/workspace'); } + override getStorageSourceFilter() { + return { sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension, PromptsStorage.plugin] }; + } + }()); + reg.defineInstance(ICustomizationHarnessService, new class extends mock() { + override readonly activeHarness = observableValue('activeHarness', CustomizationHarness.VSCode); + override getActiveDescriptor() { return createVSCodeHarnessDescriptor([PromptsStorage.extension, BUILTIN_STORAGE]); } + override registerContributedHarness() { return { dispose() { } }; } + }()); + }, + }); + + const widget = ctx.disposableStore.add( + instantiationService.createInstance(McpListWidget) + ); + ctx.container.appendChild(widget.element); + widget.layout(height, width); + + // Click the Browse Marketplace button to enter browse mode + const browseButton = widget.element.querySelector('.list-add-button') as HTMLElement; + browseButton?.click(); + + // Wait for the gallery query to resolve + await new Promise(resolve => setTimeout(resolve, 50)); +} + +// ============================================================================ +// Plugin Browse Mode — standalone widget with marketplace results +// ============================================================================ + +function makeInstalledPlugin(name: string, uri: URI, enabled: boolean): IAgentPlugin { + return new class extends mock() { + override readonly uri = uri; + override readonly label = name; + override readonly enablement = constObservable(enabled ? ContributionEnablementState.EnabledProfile : ContributionEnablementState.DisabledProfile); + override readonly hooks = constObservable([]); + override readonly commands = constObservable([]); + override readonly skills = constObservable([]); + override readonly agents = constObservable([]); + override readonly instructions = constObservable([]); + override readonly mcpServerDefinitions = constObservable([]); + override remove() { } + }(); +} + +const installedPlugins: IAgentPlugin[] = [ + makeInstalledPlugin('Linear', URI.file('/workspace/.copilot/plugins/linear'), true), + makeInstalledPlugin('Sentry', URI.file('/workspace/.copilot/plugins/sentry'), true), + makeInstalledPlugin('Datadog', URI.file('/workspace/.copilot/plugins/datadog'), true), + makeInstalledPlugin('Notion', URI.file('/workspace/.copilot/plugins/notion'), true), + makeInstalledPlugin('Confluence', URI.file('/workspace/.copilot/plugins/confluence'), true), + makeInstalledPlugin('PagerDuty', URI.file('/workspace/.copilot/plugins/pagerduty'), false), + makeInstalledPlugin('LaunchDarkly', URI.file('/workspace/.copilot/plugins/launchdarkly'), true), + makeInstalledPlugin('CircleCI', URI.file('/workspace/.copilot/plugins/circleci'), true), + makeInstalledPlugin('Vercel', URI.file('/workspace/.copilot/plugins/vercel'), false), + makeInstalledPlugin('Supabase', URI.file('/workspace/.copilot/plugins/supabase'), true), +]; + +function makeMarketplacePlugin(name: string, description: string, repo: string): IMarketplacePlugin { + return { + name, + description, + version: '1.0.0', + source: repo, + sourceDescriptor: { kind: PluginSourceKind.GitHub, repo: `example/${repo}` }, + marketplace: 'copilot', + marketplaceReference: { rawValue: `example/${repo}`, displayLabel: repo, cloneUrl: `https://github.com/example/${repo}.git`, canonicalId: `github:example/${repo}`, cacheSegments: ['example', repo], kind: MarketplaceReferenceKind.GitHubShorthand }, + marketplaceType: MarketplaceType.Copilot, + }; +} + +const marketplacePlugins: IMarketplacePlugin[] = [ + makeMarketplacePlugin('Linear', 'Issue tracking and project management integration', 'linear-plugin'), + makeMarketplacePlugin('Sentry', 'Error monitoring and performance tracing', 'sentry-plugin'), + makeMarketplacePlugin('Datadog', 'Observability and monitoring dashboards', 'datadog-plugin'), + makeMarketplacePlugin('Notion', 'Knowledge base and documentation management', 'notion-plugin'), + makeMarketplacePlugin('Figma', 'Design system inspection and asset export', 'figma-plugin'), + makeMarketplacePlugin('Stripe', 'Payment processing and billing management', 'stripe-plugin'), + makeMarketplacePlugin('Twilio', 'Communication APIs for SMS and voice', 'twilio-plugin'), + makeMarketplacePlugin('Auth0', 'Identity and access management', 'auth0-plugin'), + makeMarketplacePlugin('Algolia', 'Search and discovery API integration', 'algolia-plugin'), + makeMarketplacePlugin('LaunchDarkly', 'Feature flag management and experimentation', 'launchdarkly-plugin'), + makeMarketplacePlugin('PlanetScale', 'Serverless MySQL database management', 'planetscale-plugin'), + makeMarketplacePlugin('Vercel', 'Deployment and preview environments', 'vercel-plugin'), +]; + +async function renderPluginBrowseMode(ctx: ComponentFixtureContext): Promise { + const width = 650; + const height = 500; + ctx.container.style.width = `${width}px`; + ctx.container.style.height = `${height}px`; + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + reg.define(IListService, ListService); + reg.defineInstance(IAgentPluginService, new class extends mock() { + override readonly plugins = constObservable([] as readonly IAgentPlugin[]); + override readonly enablementModel = undefined!; + }()); + reg.defineInstance(IPluginMarketplaceService, new class extends mock() { + override readonly installedPlugins = constObservable([]); + override readonly onDidChangeMarketplaces = Event.None; + override async fetchMarketplacePlugins() { return marketplacePlugins; } + }()); + reg.defineInstance(IPluginInstallService, new class extends mock() { + override getPluginInstallUri() { return URI.file('/dev/null'); } + }()); + }, + }); + + const widget = ctx.disposableStore.add( + instantiationService.createInstance(PluginListWidget) + ); + ctx.container.appendChild(widget.element); + widget.layout(height, width); + + // Click the Browse Marketplace button to enter browse mode + const browseButton = widget.element.querySelector('.list-add-button') as HTMLElement; + browseButton?.click(); + + // Wait for the marketplace query to resolve + await new Promise(resolve => setTimeout(resolve, 50)); +} + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ path: 'chat/aiCustomizations/' }, { + + // Full editor with Local (VS Code) harness — all sections visible, harness dropdown, + // Generate buttons, AGENTS.md shortcut, all storage groups + LocalHarness: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: ctx => renderEditor(ctx, { harness: CustomizationHarness.VSCode }), + }), + + // Full editor with Copilot CLI harness — no prompts section, CLI-specific + // root files and instruction filtering under .github/.copilot paths. + CliHarness: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { harness: CustomizationHarness.CLI }), + }), + + // Full editor with Claude harness — Prompts+Plugins hidden, Agents visible, + // "Add CLAUDE.md" button, "New Rule" dropdown, instruction filtering, bridged MCP badge + ClaudeHarness: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { harness: CustomizationHarness.Claude }), + }), + + // Sessions-window variant of the full editor with workspace override UX + // and sessions section ordering. + Sessions: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.CLI, + isSessionsWindow: true, + availableHarnesses: [ + createCliHarnessDescriptor(getCliUserRoots(userHome), [BUILTIN_STORAGE]), + ], + managementSections: [ + AICustomizationManagementSection.Agents, + AICustomizationManagementSection.Skills, + AICustomizationManagementSection.Instructions, + AICustomizationManagementSection.Prompts, + AICustomizationManagementSection.Hooks, + AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Plugins, + ], + }), + }), + + // MCP Servers tab with many servers to verify scrollable list layout + McpServersTab: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.McpServers, + }), + }), + + // Agents tab — workspace and user agents, scrollable + AgentsTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Agents, + }), + }), + + // Skills tab — workspace and user skills, scrollable + SkillsTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Skills, + }), + }), + + // Instructions tab — many instructions with applyTo patterns, scrollable + InstructionsTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Instructions, + }), + }), + + // Hooks tab — workspace and user hooks, scrollable + HooksTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Hooks, + }), + }), + + // Prompts tab — workspace and user prompts, scrollable + PromptsTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Prompts, + }), + }), + + // Plugins tab + PluginsTab: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Plugins, + }), + }), + + // MCP browse/marketplace mode — standalone widget with gallery results, scrollable + // Verifies fix for https://github.com/microsoft/vscode/issues/304139 + McpBrowseMode: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: renderMcpBrowseMode, + }), + + // Plugin browse/marketplace mode — standalone widget with marketplace results, scrollable + PluginBrowseMode: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: renderPluginBrowseMode, + }), + + // Scrolled-to-bottom variants — verify last items are fully visible above footer + PromptsTabScrolled: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Prompts, + scrollToBottom: true, + }), + }), + + McpServersTabScrolled: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.McpServers, + scrollToBottom: true, + }), + }), + + PluginsTabScrolled: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Plugins, + scrollToBottom: true, + }), + }), + + // Narrow viewport — catches badge clipping and layout overflow at small sizes + McpServersTabNarrow: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.McpServers, + width: 550, + height: 400, + }), + }), + + AgentsTabNarrow: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: ctx => renderEditor(ctx, { + harness: CustomizationHarness.VSCode, + selectedSection: AICustomizationManagementSection.Agents, + width: 550, + height: 400, + }), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/chatArtifacts.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatArtifacts.fixture.ts new file mode 100644 index 00000000000..2c330949566 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/chatArtifacts.fixture.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { Event } from '../../../../base/common/event.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IListService, ListService } from '../../../../platform/list/browser/listService.js'; +import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; +import { ChatArtifactsWidget } from '../../../contrib/chat/browser/widget/chatArtifactsWidget.js'; +import { IChatImageCarouselService } from '../../../contrib/chat/browser/chatImageCarouselService.js'; +import { IChatArtifact, IChatArtifacts, IChatArtifactsService } from '../../../contrib/chat/common/tools/chatArtifactsService.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; + +import '../../../contrib/chat/browser/widget/media/chat.css'; + +function createMockArtifacts(artifacts: IChatArtifact[]): IChatArtifacts { + const obs = observableValue('artifacts', artifacts); + const mutable = observableValue('mutable', true); + return new class extends mock() { + override readonly artifacts = obs; + override readonly mutable = mutable; + override set(a: IChatArtifact[]) { obs.set(a, undefined); } + override clear() { obs.set([], undefined); } + override migrate() { } + }(); +} + +function createMockArtifactsService(artifacts: IChatArtifact[]): IChatArtifactsService { + const instance = createMockArtifacts(artifacts); + return new class extends mock() { + override getArtifacts() { return instance; } + }(); +} + +function renderArtifactsWidget(context: ComponentFixtureContext, artifacts: IChatArtifact[]): void { + const { container, disposableStore } = context; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: context.theme, + additionalServices: (reg) => { + reg.define(IListService, ListService); + reg.defineInstance(IContextViewService, new class extends mock() { }()); + reg.defineInstance(IChatArtifactsService, createMockArtifactsService(artifacts)); + reg.defineInstance(IChatImageCarouselService, new class extends mock() { }()); + reg.defineInstance(IFileService, new class extends mock() { override onDidFilesChange = Event.None; override onDidRunOperation = Event.None; }()); + reg.defineInstance(IFileDialogService, new class extends mock() { }()); + }, + }); + + const widget = disposableStore.add(instantiationService.createInstance(ChatArtifactsWidget)); + widget.render(URI.parse('chat-session:test-session')); + + container.style.width = '400px'; + container.style.padding = '8px'; + container.appendChild(widget.domNode); +} + +function renderInChatInputPart(context: ComponentFixtureContext, artifacts: IChatArtifact[]): void { + const { container, disposableStore } = context; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: context.theme, + additionalServices: (reg) => { + reg.define(IListService, ListService); + reg.defineInstance(IContextViewService, new class extends mock() { }()); + reg.defineInstance(IChatArtifactsService, createMockArtifactsService(artifacts)); + reg.defineInstance(IChatImageCarouselService, new class extends mock() { }()); + reg.defineInstance(IFileService, new class extends mock() { override onDidFilesChange = Event.None; override onDidRunOperation = Event.None; }()); + reg.defineInstance(IFileDialogService, new class extends mock() { }()); + }, + }); + + container.style.width = '500px'; + container.classList.add('monaco-workbench'); + + const session = dom.$('.interactive-session'); + container.appendChild(session); + + const inputPart = dom.h('.interactive-input-part', [ + dom.h('.chat-artifacts-widget-container@artifactsContainer'), + dom.h('.interactive-input-and-side-toolbar', [ + dom.h('.chat-input-container', [ + dom.h('.chat-editor-container@editorContainer'), + ]), + ]), + ]); + session.appendChild(inputPart.root); + + inputPart.editorContainer.style.height = '44px'; + + const widget = disposableStore.add(instantiationService.createInstance(ChatArtifactsWidget)); + widget.render(URI.parse('chat-session:test-session')); + inputPart.artifactsContainer.appendChild(widget.domNode); +} + +function renderArtifactsWidgetExpanded(context: ComponentFixtureContext, artifacts: IChatArtifact[]): void { + renderArtifactsWidget(context, artifacts); + + // Click the header button to expand the widget + const expandButton = context.container.querySelector('.chat-artifacts-expand .monaco-button'); + expandButton?.click(); +} + +// ============================================================================ +// Sample artifacts +// ============================================================================ + +const singleArtifact: IChatArtifact[] = [ + { label: 'Dev Server', uri: 'http://localhost:3000', type: 'devServer' }, +]; + +const multipleArtifacts: IChatArtifact[] = [ + { label: 'Dev Server', uri: 'http://localhost:3000', type: 'devServer' }, + { label: 'Screenshot of login page', uri: 'file:///tmp/screenshot.png', type: 'screenshot' }, + { label: 'Implementation Plan', uri: 'file:///tmp/plan.md', type: 'plan' }, +]; + +const manyArtifacts: IChatArtifact[] = [ + { label: 'Dev Server', uri: 'http://localhost:3000', type: 'devServer' }, + { label: 'Screenshot 1', uri: 'file:///tmp/s1.png', type: 'screenshot' }, + { label: 'Screenshot 2', uri: 'file:///tmp/s2.png', type: 'screenshot' }, + { label: 'Plan', uri: 'file:///tmp/plan.md', type: 'plan' }, + { label: 'API Docs', uri: 'http://localhost:3000/docs', type: undefined }, +]; + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ path: 'chat/artifacts/' }, { + SingleArtifact: defineComponentFixture({ + render: context => renderArtifactsWidget(context, singleArtifact), + }), + + MultipleArtifacts: defineComponentFixture({ + render: context => renderArtifactsWidget(context, multipleArtifacts), + }), + + ManyArtifacts: defineComponentFixture({ + render: context => renderArtifactsWidget(context, manyArtifacts), + }), + + InChatInputSingle: defineComponentFixture({ + render: context => renderInChatInputPart(context, singleArtifact), + }), + + InChatInputMultiple: defineComponentFixture({ + render: context => renderInChatInputPart(context, multipleArtifacts), + }), + + MultipleArtifactsExpanded: defineComponentFixture({ + render: context => renderArtifactsWidgetExpanded(context, multipleArtifacts), + }), + + ManyArtifactsExpanded: defineComponentFixture({ + render: context => renderArtifactsWidgetExpanded(context, manyArtifacts), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/chatInput.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatInput.fixture.ts new file mode 100644 index 00000000000..8a1fb1c92b0 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/chatInput.fixture.ts @@ -0,0 +1,293 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { IMenuService, IMenu, MenuId, MenuItemAction, IMenuItem } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { IDecorationsService } from '../../../services/decorations/common/decorations.js'; +import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { IPathService } from '../../../services/path/common/pathService.js'; +import { IChatWidgetHistoryService } from '../../../contrib/chat/common/widget/chatWidgetHistoryService.js'; +import { IChatContextPickService } from '../../../contrib/chat/browser/attachments/chatContextPickService.js'; +import { IWorkspaceContextService, IWorkspace } from '../../../../platform/workspace/common/workspace.js'; +import { IViewDescriptorService } from '../../../common/views.js'; +import { IChatWidget } from '../../../contrib/chat/browser/chat.js'; +import { IAgentSessionsService } from '../../../contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { IChatAttachmentResolveService } from '../../../contrib/chat/browser/attachments/chatAttachmentResolveService.js'; +import { IChatAttachmentWidgetRegistry } from '../../../contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.js'; +import { IChatContextService } from '../../../contrib/chat/browser/contextContrib/chatContextService.js'; +import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../../contrib/chat/browser/widget/input/chatInputPart.js'; +import { IChatArtifacts, IChatArtifactsService } from '../../../contrib/chat/common/tools/chatArtifactsService.js'; +import { ChatEditingSessionState, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../contrib/chat/common/editing/chatEditingService.js'; +import { IChatRequestDisablement } from '../../../contrib/chat/common/model/chatModel.js'; +import { IChatTodo, IChatTodoListService } from '../../../contrib/chat/common/tools/chatTodoListService.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../contrib/chat/common/constants.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { IChatModeService } from '../../../contrib/chat/common/chatModes.js'; +import { IChatService } from '../../../contrib/chat/common/chatService/chatService.js'; +import { IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js'; +import { ILanguageModelsService } from '../../../contrib/chat/common/languageModels.js'; +import { IChatAgentService } from '../../../contrib/chat/common/participants/chatAgents.js'; +import { ILanguageModelToolsService } from '../../../contrib/chat/common/tools/languageModelToolsService.js'; +import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; +import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IListService, ListService } from '../../../../platform/list/browser/listService.js'; +import { INotebookDocumentService } from '../../../services/notebook/common/notebookDocumentService.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; + +import '../../../contrib/chat/browser/widget/media/chat.css'; + +class FixtureMenuService implements IMenuService { + declare readonly _serviceBrand: undefined; + private readonly _items = new Map(); + constructor( + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICommandService private readonly _commandService: ICommandService, + ) { } + addItem(menuId: MenuId, item: IMenuItem): void { + const key = menuId.id; + let items = this._items.get(key); + if (!items) { + items = []; + this._items.set(key, items); + } + items.push(item); + } + createMenu(id: MenuId): IMenu { + const actions: [string, MenuItemAction[]][] = []; + for (const item of this._items.get(id.id) ?? []) { + const group = item.group ?? ''; + let entry = actions.find(a => a[0] === group); + if (!entry) { + entry = [group, []]; + actions.push(entry); + } + entry[1].push(new MenuItemAction(item.command, item.alt, {}, undefined, undefined, this._contextKeyService, this._commandService)); + } + return { onDidChange: Event.None, dispose() { }, getActions: () => actions }; + } + getMenuActions() { return []; } + getMenuContexts() { return new Set(); } + resetHiddenStates() { } +} + +interface ChatInputFixtureOptions { + readonly artifacts?: readonly { label: string; uri: string; type: 'devServer' | 'screenshot' | 'plan' | undefined }[]; + readonly editingSession?: IChatEditingSession; + readonly todos?: IChatTodo[]; +} + +async function renderChatInput(context: ComponentFixtureContext, fixtureOptions: ChatInputFixtureOptions = {}): Promise { + const { container, disposableStore } = context; + const { artifacts = [], editingSession, todos = [] } = fixtureOptions; + const artifactsObs = observableValue('artifacts', artifacts); + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: context.theme, + additionalServices: (reg) => { + reg.define(IMenuService, FixtureMenuService); + registerWorkbenchServices(reg); + // eslint-disable-next-line local/code-no-dangerous-type-assertions + reg.defineInstance(ITextModelService, new class extends mock() { override async createModelReference() { return { object: { textEditorModel: null }, dispose() { } } as unknown as Awaited>; } }()); + reg.defineInstance(IDecorationsService, new class extends mock() { override onDidChangeDecorations = Event.None; }()); + reg.defineInstance(ITextFileService, new class extends mock() { override readonly untitled = new class extends mock() { override readonly onDidChangeLabel = Event.None; }(); }()); + reg.defineInstance(ILanguageModelsService, new class extends mock() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } }()); + reg.defineInstance(IFileService, new class extends mock() { override onDidFilesChange = Event.None; override onDidRunOperation = Event.None; }()); + reg.defineInstance(IEditorService, new class extends mock() { override onDidActiveEditorChange = Event.None; }()); + reg.defineInstance(IChatAgentService, new class extends mock() { override onDidChangeAgents = Event.None; override getAgents() { return []; } override getActivatedAgents() { return []; } }()); + reg.defineInstance(ISharedWebContentExtractorService, new class extends mock() { }()); + reg.defineInstance(IWorkbenchAssignmentService, new class extends mock() { override async getCurrentExperiments() { return []; } override async getTreatment() { return undefined; } override onDidRefetchAssignments = Event.None; }()); + reg.defineInstance(IChatEntitlementService, new class extends mock() { }()); + reg.defineInstance(IChatModeService, new class extends mock() { override readonly onDidChangeChatModes = Event.None; override getModes() { return { builtin: [], custom: [] }; } override findModeById() { return undefined; } }()); + reg.defineInstance(ILanguageModelToolsService, new class extends mock() { override onDidChangeTools = Event.None; override getTools() { return []; } }()); + reg.defineInstance(IChatService, new class extends mock() { override onDidSubmitRequest = Event.None; }()); + reg.defineInstance(IChatSessionsService, new class extends mock() { override getAllChatSessionContributions() { return []; } override readonly onDidChangeSessionOptions = Event.None; override readonly onDidChangeOptionGroups = Event.None; override readonly onDidChangeAvailability = Event.None; }()); + reg.defineInstance(IChatContextService, new class extends mock() { }()); + reg.defineInstance(IAgentSessionsService, new class extends mock() { override readonly model = new class extends mock() { override readonly onDidChangeSessions = Event.None; }(); }()); + reg.defineInstance(IWorkspaceContextService, new class extends mock() { override onDidChangeWorkspaceFolders = Event.None; override getWorkspace(): IWorkspace { return { id: '', folders: [], configuration: undefined }; } }()); + reg.defineInstance(IWorkbenchLayoutService, new class extends mock() { override onDidChangePartVisibility = Event.None; override onDidChangeWindowMaximized = Event.None; override isVisible() { return true; } }()); + reg.defineInstance(IViewDescriptorService, new class extends mock() { override onDidChangeLocation = Event.None; }()); + reg.defineInstance(IChatAttachmentWidgetRegistry, new class extends mock() { }()); + reg.defineInstance(IChatAttachmentResolveService, new class extends mock() { }()); + reg.defineInstance(IExtensionService, new class extends mock() { override readonly onDidChangeExtensions = Event.None; }()); + reg.defineInstance(IPathService, new class extends mock() { }()); + reg.defineInstance(IChatWidgetHistoryService, new class extends mock() { override getHistory() { return []; } override readonly onDidChangeHistory = Event.None; }()); + reg.defineInstance(IChatContextPickService, new class extends mock() { }()); + reg.defineInstance(IListService, new ListService()); + reg.defineInstance(INotebookDocumentService, new class extends mock() { }()); + reg.defineInstance(IActionWidgetService, new class extends mock() { override show() { } override hide() { } override get isVisible() { return false; } }()); + reg.defineInstance(IProductService, new class extends mock() { }()); + reg.defineInstance(IUpdateService, new class extends mock() { override onStateChange = Event.None; override get state() { return { type: StateType.Uninitialized as const }; } }()); + reg.defineInstance(IUriIdentityService, new class extends mock() { }()); + reg.defineInstance(IChatArtifactsService, new class extends mock() { + override getArtifacts(): IChatArtifacts { + const mutableObs = observableValue('mutable', true); + return new class extends mock() { + override readonly artifacts = artifactsObs; + override readonly mutable = mutableObs; + override set() { } + override clear() { } + override migrate() { } + }(); + } + }()); + reg.defineInstance(IChatTodoListService, new class extends mock() { + override readonly onDidUpdateTodos = Event.None; + override getTodos() { return [...todos]; } + override setTodos() { } + override migrateTodos() { } + }()); + }, + }); + + if (artifacts.length > 0) { + const configService = instantiationService.get(IConfigurationService) as TestConfigurationService; + await configService.setUserConfiguration(ChatConfiguration.ArtifactsEnabled, true); + } + + container.style.width = '500px'; + container.style.backgroundColor = 'var(--vscode-sideBar-background, var(--vscode-editor-background))'; + container.classList.add('monaco-workbench'); + + const session = document.createElement('div'); + session.classList.add('interactive-session'); + container.appendChild(session); + + const menuService = instantiationService.get(IMenuService) as FixtureMenuService; + menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.attachContext', title: '+', icon: Codicon.add }, group: 'navigation', order: -1 }); + menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModePicker', title: 'Agent' }, group: 'navigation', order: 1 }); + menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModelPicker', title: 'GPT-5.3-Codex' }, group: 'navigation', order: 3 }); + menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.configureTools', title: '', icon: Codicon.settingsGear }, group: 'navigation', order: 100 }); + menuService.addItem(MenuId.ChatExecute, { command: { id: 'workbench.action.chat.submit', title: 'Send', icon: Codicon.arrowUp }, group: 'navigation', order: 4 }); + menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openSessionTargetPicker', title: 'Local' }, group: 'navigation', order: 0 }); + menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openPermissionPicker', title: 'Default Approvals' }, group: 'navigation', order: 10 }); + + const options: IChatInputPartOptions = { + renderFollowups: false, + renderInputToolbarBelowInput: false, + renderWorkingSet: !!editingSession, + menus: { executeToolbar: MenuId.ChatExecute, telemetrySource: 'fixture' }, + widgetViewKindTag: 'view', + inputEditorMinLines: 2, + }; + const styles: IChatInputStyles = { + overlayBackground: 'var(--vscode-editor-background)', + listForeground: 'var(--vscode-foreground)', + listBackground: 'var(--vscode-editor-background)', + }; + + try { + const inputPart = disposableStore.add(instantiationService.createInstance(ChatInputPart, ChatAgentLocation.Chat, options, styles, false)); + const mockWidget = new class extends mock() { + override readonly onDidChangeViewModel = new Emitter().event; + override readonly viewModel = undefined; + override readonly contribs = []; + override readonly location = ChatAgentLocation.Chat; + override readonly viewContext = {}; + }(); + + inputPart.render(session, '', mockWidget); + inputPart.layout(500); + await new Promise(r => setTimeout(r, 100)); + inputPart.layout(500); + inputPart.renderArtifactsWidget(URI.parse('chat-session:test-session')); + await inputPart.renderChatTodoListWidget(URI.parse('chat-session:test-session')); + await new Promise(r => setTimeout(r, 50)); + + if (editingSession) { + inputPart.renderChatEditingSessionState(editingSession); + await new Promise(r => setTimeout(r, 50)); + inputPart.layout(500); + } + } catch (e) { + const err = document.createElement('pre'); + err.style.cssText = 'color:red;font-size:11px;white-space:pre-wrap'; + err.textContent = `Render error: ${e instanceof Error ? e.message : String(e)}`; + session.appendChild(err); + } +} + +const sampleArtifacts = [ + { label: 'Dev Server', uri: 'http://localhost:3000', type: 'devServer' as const }, + { label: 'Screenshot', uri: 'file:///tmp/screenshot.png', type: 'screenshot' as const }, + { label: 'Plan', uri: 'file:///tmp/plan.md', type: 'plan' as const }, +]; + +function createMockEditingSession(files: { uri: string; added: number; removed: number }[]): IChatEditingSession { + const entries = files.map(f => { + const entry = new class extends mock() { + override readonly entryId = f.uri; + override readonly modifiedURI = URI.parse(f.uri); + override readonly originalURI = URI.parse(f.uri); + override readonly state = observableValue('state', ModifiedFileEntryState.Modified); + override readonly linesAdded = observableValue('linesAdded', f.added); + override readonly linesRemoved = observableValue('linesRemoved', f.removed); + override readonly lastModifyingRequestId = 'request-1'; + override readonly changesCount = observableValue('changesCount', 1); + override readonly isCurrentlyBeingModifiedBy = observableValue('isCurrentlyBeingModifiedBy', undefined); + override readonly lastModifyingResponse = observableValue('lastModifyingResponse', undefined); + override readonly rewriteRatio = observableValue('rewriteRatio', 0); + override readonly waitsForLastEdits = observableValue('waitsForLastEdits', false); + override readonly reviewMode = observableValue('reviewMode', false); + override readonly autoAcceptController = observableValue('autoAcceptController', undefined); + }(); + return entry; + }); + + return new class extends mock() { + override readonly isGlobalEditingSession = false; + override readonly chatSessionResource = URI.parse('chat-session:test-session'); + override readonly onDidDispose = Event.None; + override readonly state = observableValue('state', ChatEditingSessionState.Idle); + override readonly entries = observableValue('entries', entries); + override readonly requestDisablement = observableValue('requestDisablement', []); + }(); +} + +const sampleTodos: IChatTodo[] = [ + { id: 1, title: 'Set up project structure', status: 'completed' }, + { id: 2, title: 'Implement auth service', status: 'in-progress' }, + { id: 3, title: 'Add unit tests', status: 'not-started' }, +]; + +export default defineThemedFixtureGroup({ path: 'chat/input/' }, { + Default: defineComponentFixture({ render: context => renderChatInput(context) }), + WithArtifacts: defineComponentFixture({ render: context => renderChatInput(context, { artifacts: sampleArtifacts }) }), + WithFileChanges: defineComponentFixture({ + render: context => renderChatInput(context, { editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]) }) + }), + WithTodos: defineComponentFixture({ + render: context => renderChatInput(context, { todos: sampleTodos }) + }), + WithTodosAndFileChanges: defineComponentFixture({ + render: context => renderChatInput(context, { todos: sampleTodos, editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]) }) + }), + WithArtifactsAndFileChanges: defineComponentFixture({ + render: context => renderChatInput(context, { artifacts: sampleArtifacts, editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]) }) + }), + Full: defineComponentFixture({ + render: context => renderChatInput(context, { + artifacts: sampleArtifacts, + editingSession: createMockEditingSession([{ uri: 'file:///workspace/src/fibon.ts', added: 21, removed: 1 }]), + todos: sampleTodos, + }) + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/chatTerminalCollapsible.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatTerminalCollapsible.fixture.ts new file mode 100644 index 00000000000..d4a8c16439d --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/chatTerminalCollapsible.fixture.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { Event } from '../../../../base/common/event.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { mock, upcastPartial } from '../../../../base/test/common/mock.js'; +import type { IChatContentPartRenderContext, InlineTextModelCollection } from '../../../contrib/chat/browser/widget/chatContentParts/chatContentParts.js'; +import type { IChatResponseViewModel } from '../../../contrib/chat/common/model/chatViewModel.js'; +import { ChatTerminalThinkingCollapsibleWrapper } from '../../../contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; + +import '../../../contrib/chat/browser/widget/media/chat.css'; + +function createMockContext(): IChatContentPartRenderContext { + return { + element: new class extends mock() { }(), + elementIndex: 0, + container: document.createElement('div'), + content: [], + contentIndex: 0, + editorPool: undefined!, + codeBlockStartIndex: 0, + treeStartIndex: 0, + diffEditorPool: undefined!, + codeBlockModelCollection: undefined!, + currentWidth: observableValue('currentWidth', 400), + onDidChangeVisibility: Event.None, + inlineTextModels: upcastPartial({}), + }; +} + +function renderCollapsible(context: ComponentFixtureContext, commandText: string, isSandboxWrapped: boolean, isComplete: boolean): void { + const { container, disposableStore } = context; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: context.theme, + }); + + container.style.width = '500px'; + container.style.padding = '8px'; + container.classList.add('monaco-workbench'); + + const session = dom.$('.interactive-session'); + container.appendChild(session); + + const contentElement = dom.$('.chat-terminal-output-placeholder'); + contentElement.textContent = '(terminal output would appear here)'; + contentElement.style.padding = '8px'; + contentElement.style.color = 'var(--vscode-descriptionForeground)'; + + const wrapper = disposableStore.add(instantiationService.createInstance( + ChatTerminalThinkingCollapsibleWrapper, + commandText, + isSandboxWrapped, + contentElement, + createMockContext(), + false, + isComplete, + )); + + session.appendChild(wrapper.domNode); +} + +export default defineThemedFixtureGroup({ path: 'chat/terminalCollapsible/' }, { + 'Ran - simple command': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'ls -lh', false, true), + }), + 'Running - simple command': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'ls -lh', false, false), + }), + 'Ran sandbox - simple command': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'ls -lh', true, true), + }), + 'Running sandbox - simple command': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'ls -lh', true, false), + }), + 'Ran - special chars': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'grep -rn "hello" ./src --include="*.ts"', false, true), + }), + 'Ran sandbox - special chars': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'grep -rn "hello" ./src --include="*.ts"', true, true), + }), + 'Ran - backticks': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'echo `date` && echo `hostname`', false, true), + }), + 'Ran sandbox - backticks': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'echo `date` && echo `hostname`', true, true), + }), + 'Ran sandbox - powershell backticks': defineComponentFixture({ + render: ctx => renderCollapsible(ctx, 'Get-Process | Where-Object {$_.Name -eq `"notepad`"}', true, true), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts index 03c64b694aa..5e155a37d5e 100644 --- a/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts @@ -70,7 +70,7 @@ function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtur export default defineThemedFixtureGroup({ path: 'editor/' }, { CodeEditor: defineComponentFixture({ - labels: { kind: 'screenshot' }, + labels: { kind: 'screenshot', blocksCi: true }, render: (context) => renderCodeEditor(context), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index e5318aaeecf..2a7f7faf22f 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -10,6 +10,7 @@ import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle import { URI } from '../../../../base/common/uri.js'; // eslint-disable-next-line local/code-import-patterns import '../../../../../../build/vite/style.css'; +import '../../../browser/media/style.css'; // Theme import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; @@ -244,10 +245,11 @@ function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet { return lightThemeStyleSheet; } + const scopeSelector = '.' + theme.classNames[0]; const sheet = new CSSStyleSheet(); const css = generateColorThemeCSS( theme, - ':host', + scopeSelector, themingRegistry.getThemingParticipants(), mockEnvironmentService ); @@ -261,20 +263,40 @@ function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet { return sheet; } -/** - * Applies theme styling to a shadow DOM container. - * Adds theme class names and adopts shared stylesheets. - */ -export function setupTheme(container: HTMLElement, theme: ColorThemeData): void { - container.classList.add(...theme.classNames); +let globalStylesInstalled = false; - const shadowRoot = container.getRootNode() as ShadowRoot; - if (shadowRoot.adoptedStyleSheets !== undefined) { - shadowRoot.adoptedStyleSheets = [ - getGlobalStyleSheet(), - getIconsStyleSheetCached(), - getThemeStyleSheet(theme), - ]; +function installGlobalStyles(): void { + if (globalStylesInstalled) { + return; + } + globalStylesInstalled = true; + document.adoptedStyleSheets = [ + ...document.adoptedStyleSheets, + getGlobalStyleSheet(), + getIconsStyleSheetCached(), + getThemeStyleSheet(darkTheme), + getThemeStyleSheet(lightTheme), + ]; +} + +export function setupTheme(container: HTMLElement, theme: ColorThemeData): void { + installGlobalStyles(); + container.classList.add('monaco-workbench', getPlatformClass(), ...theme.classNames); +} + +function getPlatformClass(): string { + const alwaysUseMac = true; + if (alwaysUseMac) { + return 'mac'; + } else { + const ua = navigator.userAgent; + if (ua.includes('Macintosh')) { + return 'mac'; + } + if (ua.includes('Linux')) { + return 'linux'; + } + return 'windows'; } } @@ -515,7 +537,7 @@ type ThemedFixtures = ReturnType; */ export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures { const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({ - isolation: 'shadow-dom', + isolation: 'none', displayMode: { type: 'component' }, properties: [], background: theme === darkTheme ? 'dark' : 'light', diff --git a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts new file mode 100644 index 00000000000..613a14c5ae0 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dimension } from '../../../../base/browser/dom.js'; +import { mainWindow } from '../../../../base/browser/window.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { ImageCarouselEditor } from '../../../contrib/imageCarousel/browser/imageCarouselEditor.js'; +import { ImageCarouselEditorInput } from '../../../contrib/imageCarousel/browser/imageCarouselEditorInput.js'; +import { ICarouselImage, IImageCarouselCollection } from '../../../contrib/imageCarousel/browser/imageCarouselTypes.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; +import '../../../contrib/imageCarousel/browser/media/imageCarousel.css'; + +function createSolidPng(r: number, g: number, b: number, width: number = 64, height: number = 64): VSBuffer { + const canvas = mainWindow.document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d')!; + ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; + ctx.fillRect(0, 0, width, height); + + const dataUrl = canvas.toDataURL('image/png'); + const base64 = dataUrl.split(',')[1]; + return VSBuffer.wrap(Uint8Array.from(atob(base64), c => c.charCodeAt(0))); +} + +function createTestImages(): ICarouselImage[] { + return [ + { id: 'img-1', name: 'Red', mimeType: 'image/png', data: createSolidPng(220, 50, 50), caption: 'A red image' }, + { id: 'img-2', name: 'Green', mimeType: 'image/png', data: createSolidPng(50, 180, 50), caption: 'A green image' }, + { id: 'img-3', name: 'Blue', mimeType: 'image/png', data: createSolidPng(50, 80, 220) }, + { id: 'img-4', name: 'Yellow', mimeType: 'image/png', data: createSolidPng(230, 210, 50), caption: 'A yellow image' }, + { id: 'img-5', name: 'Purple', mimeType: 'image/png', data: createSolidPng(150, 50, 200) }, + ]; +} + +function createMockEditorGroup(): IEditorGroup { + return new class extends mock() { + override windowId = mainWindow.vscodeWindowId; + }(); +} + +async function renderCarousel(context: ComponentFixtureContext, collection: IImageCarouselCollection, startIndex: number = 0): Promise { + const { container, disposableStore, theme } = context; + + container.style.width = '600px'; + container.style.height = '500px'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + }); + + const editor = disposableStore.add( + instantiationService.createInstance(ImageCarouselEditor, createMockEditorGroup()) + ); + editor.create(container); + editor.layout(new Dimension(600, 500)); + + const input = new ImageCarouselEditorInput(collection, startIndex); + await editor.setInput(input, undefined, {}, CancellationToken.None); +} + +function singleSectionCollection(): IImageCarouselCollection { + return { + id: 'fixture-single', + title: 'Test Carousel', + sections: [{ title: 'All Images', images: createTestImages() }], + }; +} + +function multiSectionCollection(): IImageCarouselCollection { + const images = createTestImages(); + return { + id: 'fixture-multi', + title: 'Multi-Section Carousel', + sections: [ + { title: 'Warm Colors', images: [images[0], images[3]] }, + { title: 'Cool Colors', images: [images[2], images[4]] }, + { title: 'Nature', images: [images[1]] }, + ], + }; +} + +function singleImageCollection(): IImageCarouselCollection { + const images = createTestImages(); + return { + id: 'fixture-single-image', + title: 'Single Image', + sections: [{ title: '', images: [images[0]] }], + }; +} + +export default defineThemedFixtureGroup({ path: 'imageCarousel/' }, { + SingleSection: defineComponentFixture({ + render: ctx => renderCarousel(ctx, singleSectionCollection()), + }), + SingleSectionMiddleImage: defineComponentFixture({ + render: ctx => renderCarousel(ctx, singleSectionCollection(), 2), + }), + MultipleSections: defineComponentFixture({ + render: ctx => renderCarousel(ctx, multiSectionCollection()), + }), + SingleImage: defineComponentFixture({ + render: ctx => renderCarousel(ctx, singleImageCollection()), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts index e9298fcf4d1..e06984438ec 100644 --- a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts @@ -19,6 +19,8 @@ import { InlineCompletionsSource, InlineCompletionsState } from '../../../../edi import { InlineEditItem } from '../../../../editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.js'; import { TextModelValueReference } from '../../../../editor/contrib/inlineCompletions/browser/model/textModelValueReference.js'; import { JumpToView } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/jumpToView.js'; +import { GutterIndicatorMenuContent } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.js'; +import { InlineSuggestionGutterMenuData } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; import { IUserInteractionService, MockUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; import '../../../../editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.css'; @@ -276,6 +278,58 @@ function createLongDistanceEditor(options: { controller?.model?.get(); } +function renderGutterMenu({ container, disposableStore, theme }: ComponentFixtureContext): void { + container.style.width = '250px'; + container.style.height = '280px'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + }, + }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + 'const x = 1;', + URI.parse('inmemory://gutter-menu.ts'), + 'typescript' + )); + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + document.createElement('div'), + { minimap: { enabled: false } }, + { contributions: [] } satisfies ICodeEditorWidgetOptions + )); + editor.setModel(textModel); + + const editorObs = observableCodeEditor(editor); + const menuData = new InlineSuggestionGutterMenuData( + undefined, + 'Copilot', + [], + undefined, + undefined, + undefined, + ); + + const content = disposableStore.add( + instantiationService.createInstance( + GutterIndicatorMenuContent, + editorObs, + menuData, + () => { }, + ).toDisposableLiveElement() + ); + + container.style.background = 'var(--vscode-editorHoverWidget-background)'; + container.style.border = '2px solid var(--vscode-editorHoverWidget-border)'; + container.style.borderRadius = '3px'; + container.style.color = 'var(--vscode-editorHoverWidget-foreground)'; + container.appendChild(content.element); +} + export default defineThemedFixtureGroup({ path: 'editor/' }, { HintsToolbar: defineComponentFixture({ labels: { kind: 'screenshot' }, @@ -307,4 +361,8 @@ export default defineThemedFixtureGroup({ path: 'editor/' }, { await writeFile(outputPath, processed);`, }), }), + GutterMenu: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: renderGutterMenu, + }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/peekReference.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/peekReference.fixture.ts new file mode 100644 index 00000000000..a9b2b7f5230 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/peekReference.fixture.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { mock } from '../../../../base/test/common/mock.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { LayoutData, ReferenceWidget } from '../../../../editor/contrib/gotoSymbol/browser/peek/referencesWidget.js'; +import { ReferencesModel } from '../../../../editor/contrib/gotoSymbol/browser/referencesModel.js'; +import * as peekView from '../../../../editor/contrib/peekView/browser/peekView.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { IListService, ListService } from '../../../../platform/list/browser/listService.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; + +import '../../../../editor/contrib/peekView/browser/media/peekViewWidget.css'; +import '../../../../editor/contrib/gotoSymbol/browser/peek/referencesWidget.css'; +import '../../../../base/browser/ui/codicons/codiconStyles.js'; + +const SAMPLE_CODE = `import { readFile, writeFile } from 'fs'; + +function processFile(path: string): Promise { + return new Promise((resolve, reject) => { + readFile(path, 'utf8', (err, data) => { + if (err) { + reject(err); + return; + } + resolve(data.toUpperCase()); + }); + }); +} + +async function main() { + const result = await processFile('./input.txt'); + await writeFile('./output.txt', result); + console.log('Done processing file'); +} + +main(); +`; + +function renderPeekReference({ container, disposableStore, theme }: ComponentFixtureContext): void { + container.style.width = '700px'; + container.style.height = '400px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + reg.define(IListService, ListService); + reg.defineInstance(peekView.IPeekViewService, new class extends mock() { + declare readonly _serviceBrand: undefined; + override addExclusiveWidget(_editor: ICodeEditor, _widget: peekView.PeekViewWidget) { } + }); + reg.defineInstance(ITextModelService, new class extends mock() { + declare readonly _serviceBrand: undefined; + override async createModelReference(): Promise { + throw new Error('Not implemented in fixture'); + } + override canHandleResource() { return false; } + override registerTextModelContentProvider() { return { dispose: () => { } }; } + }); + }, + }); + + const uri = URI.parse('inmemory://peek-fixture.ts'); + const textModel = disposableStore.add(createTextModel( + instantiationService, + SAMPLE_CODE, + uri, + 'typescript' + )); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: [] + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + editor.focus(); + + const layoutData: LayoutData = { ratio: 0.7, heightInLines: 10 }; + + const referenceWidget = instantiationService.createInstance( + ReferenceWidget, + editor, + true, + layoutData, + ); + disposableStore.add(referenceWidget); + + const range = { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 21 }; + referenceWidget.setTitle('processFile'); + referenceWidget.setMetaTitle('3 references'); + referenceWidget.show(range); + + const links = [ + { uri, range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 21 } }, + { uri, range: { startLineNumber: 16, startColumn: 26, endLineNumber: 16, endColumn: 37 } }, + { uri, range: { startLineNumber: 20, startColumn: 1, endLineNumber: 20, endColumn: 5 } }, + ]; + + const model = new ReferencesModel(links, 'processFile'); + disposableStore.add(model); + referenceWidget.setModel(model); +} + +export default defineThemedFixtureGroup({ + PeekReferences: defineComponentFixture({ + render: renderPeekReference, + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts index 6e87b84b4d0..306b921cac5 100644 --- a/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts @@ -119,6 +119,8 @@ async function renderPromptFilePickerFixture({ container, disposableStore, theme return promptsState.extensionPromptFiles.filter(file => file.type === type); case PromptsStorage.plugin: return []; + default: + return []; } } diff --git a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts index a5aada2ea26..8e04b43bc15 100644 --- a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts @@ -34,7 +34,7 @@ interface SuggestFixtureOptions extends ComponentFixtureContext { editorOptions?: IEditorOptions; } -async function renderSuggestWidget(options: SuggestFixtureOptions): Promise { +function renderSuggestWidget(options: SuggestFixtureOptions): void { const { container, disposableStore, theme } = options; container.style.width = options.width ?? '500px'; container.style.height = options.height ?? '300px'; diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts index d24e26899f7..33a66a37a86 100644 --- a/src/vs/workbench/test/browser/quickAccess.test.ts +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -6,12 +6,12 @@ import assert from 'assert'; import { Registry } from '../../../platform/registry/common/platform.js'; import { IQuickAccessRegistry, Extensions, IQuickAccessProvider, QuickAccessRegistry } from '../../../platform/quickinput/common/quickAccess.js'; -import { IQuickPick, IQuickPickItem, IQuickInputService } from '../../../platform/quickinput/common/quickInput.js'; +import { IQuickPick, IQuickPickItem, IQuickInputService, IKeyMods, IQuickPickDidAcceptEvent } from '../../../platform/quickinput/common/quickInput.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { TestServiceAccessor, workbenchInstantiationService, createEditorPart } from './workbenchTestServices.js'; import { DisposableStore, toDisposable, IDisposable } from '../../../base/common/lifecycle.js'; import { timeout } from '../../../base/common/async.js'; -import { PickerQuickAccessProvider, FastAndSlowPicks } from '../../../platform/quickinput/browser/pickerQuickAccess.js'; +import { PickerQuickAccessProvider, FastAndSlowPicks, IPickerQuickAccessItem } from '../../../platform/quickinput/browser/pickerQuickAccess.js'; import { URI } from '../../../base/common/uri.js'; import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; @@ -450,4 +450,135 @@ suite('QuickAccess', () => { } await part.activeGroup.closeAllEditors(); }); + + //#region attach dispatch tests + + interface ITestAttachPickItem extends IPickerQuickAccessItem { + label: string; + accept?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void; + attach?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void; + } + + let attachTestAcceptCalled = false; + let attachTestAttachCalled = false; + let attachTestAttachKeyMods: IKeyMods | undefined; + + class AttachTestQuickPickProvider extends PickerQuickAccessProvider { + constructor() { + super('attach'); + } + + protected _getPicks(): ITestAttachPickItem[] { + return [{ + label: 'Test Item', + accept: () => { + attachTestAcceptCalled = true; + }, + attach: (keyMods) => { + attachTestAttachCalled = true; + attachTestAttachKeyMods = keyMods; + } + }]; + } + } + + class AttachTestNoAttachProvider extends PickerQuickAccessProvider { + constructor() { + super('noattach'); + } + + protected _getPicks(): ITestAttachPickItem[] { + return [{ + label: 'No Attach Item', + accept: () => { + attachTestAcceptCalled = true; + } + }]; + } + } + + const attachProviderDescriptor = { ctor: AttachTestQuickPickProvider, prefix: 'attach', helpEntries: [] }; + const noAttachProviderDescriptor = { ctor: AttachTestNoAttachProvider, prefix: 'noattach', helpEntries: [] }; + + function resetAttachState() { + attachTestAcceptCalled = false; + attachTestAttachCalled = false; + attachTestAttachKeyMods = undefined; + } + + test('quick pick access - accept without modifier keys calls accept, not attach', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(attachProviderDescriptor)); + resetAttachState(); + + accessor.quickInputService.quickAccess.show('attach'); + await accessor.quickInputService.accept(); + + assert.strictEqual(attachTestAcceptCalled, true); + assert.strictEqual(attachTestAttachCalled, false); + + disposables.dispose(); + restore(); + }); + + test('quick pick access - accept with ctrlCmd calls attach instead of accept', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(attachProviderDescriptor)); + resetAttachState(); + + accessor.quickInputService.quickAccess.show('attach'); + await accessor.quickInputService.accept({ ctrlCmd: true, alt: false, shift: false }); + + assert.strictEqual(attachTestAcceptCalled, false); + assert.strictEqual(attachTestAttachCalled, true); + assert.deepStrictEqual(attachTestAttachKeyMods, { ctrlCmd: true, alt: false, shift: false }); + + disposables.dispose(); + restore(); + }); + + test('quick pick access - accept with alt calls attach instead of accept', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(attachProviderDescriptor)); + resetAttachState(); + + accessor.quickInputService.quickAccess.show('attach'); + await accessor.quickInputService.accept({ ctrlCmd: false, alt: true, shift: false }); + + assert.strictEqual(attachTestAcceptCalled, false); + assert.strictEqual(attachTestAttachCalled, true); + assert.deepStrictEqual(attachTestAttachKeyMods, { ctrlCmd: false, alt: true, shift: false }); + + disposables.dispose(); + restore(); + }); + + test('quick pick access - accept with modifier keys but no attach method calls accept', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const restore = (registry as QuickAccessRegistry).clear(); + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(noAttachProviderDescriptor)); + resetAttachState(); + + accessor.quickInputService.quickAccess.show('noattach'); + await accessor.quickInputService.accept({ ctrlCmd: true, alt: false, shift: false }); + + assert.strictEqual(attachTestAcceptCalled, true); + assert.strictEqual(attachTestAttachCalled, false); + + disposables.dispose(); + restore(); + }); + + //#endregion }); diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 3a60b99ae91..f9e3a45bb4f 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -9,7 +9,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../base/common/map.js'; +import { ResourceMap, ResourceSet } from '../../../base/common/map.js'; import { Schemas } from '../../../base/common/network.js'; import { observableValue } from '../../../base/common/observable.js'; import { join } from '../../../base/common/path.js'; @@ -380,7 +380,8 @@ export class TestWorkspaceTrustManagementService extends Disposable implements I constructor( - private trusted: boolean = true + private trusted: boolean = true, + private trustedUris: ResourceSet = new ResourceSet() ) { super(); } @@ -406,11 +407,11 @@ export class TestWorkspaceTrustManagementService extends Disposable implements I } getUriTrustInfo(uri: URI): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve({ trusted: this.trustedUris.has(uri), uri }); } async setTrustedUris(folders: URI[]): Promise { - throw new Error('Method not implemented.'); + this.trustedUris = new ResourceSet(folders); } async setUrisTrust(uris: URI[], trusted: boolean): Promise { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index be64fd940aa..1d89cb13a56 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -212,6 +212,7 @@ import './contrib/inlineChat/browser/inlineChat.contribution.js'; import './contrib/mcp/browser/mcp.contribution.js'; import './contrib/chat/browser/chatSessions/chatSessions.contribution.js'; import './contrib/chat/browser/contextContrib/chatContext.contribution.js'; +import './contrib/imageCarousel/browser/imageCarousel.contribution.js'; // Interactive import './contrib/interactive/browser/interactive.contribution.js'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 68821267b86..5a5c36698c1 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -91,6 +91,7 @@ import '../platform/userDataProfile/electron-browser/userDataProfileStorageServi import './services/auxiliaryWindow/electron-browser/auxiliaryWindowService.js'; import '../platform/extensionManagement/electron-browser/extensionsProfileScannerService.js'; import '../platform/webContentExtractor/electron-browser/webContentExtractorService.js'; +import '../platform/agentHost/electron-browser/agentHostService.js'; import './services/browserView/electron-browser/playwrightWorkbenchService.js'; import './services/process/electron-browser/processService.js'; import './services/power/electron-browser/powerService.js'; @@ -191,6 +192,9 @@ import './contrib/mcp/electron-browser/mcp.contribution.js'; // Policy Export import './contrib/policyExport/electron-browser/policyExport.contribution.js'; +// Keybindings Export +import './contrib/keybindingsExport/electron-browser/keybindingsExport.contribution.js'; + //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index aee7366ed53..939822a241a 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -174,4 +174,7 @@ import './contrib/remote/browser/remoteStartEntry.contribution.js'; // Process Explorer import './contrib/processExplorer/browser/processExplorer.web.contribution.js'; +// Browser View +import './contrib/browserView/browser/browserView.contribution.js'; + //#endregion diff --git a/src/vscode-dts/vscode.proposed.authIssuers.d.ts b/src/vscode-dts/vscode.proposed.authIssuers.d.ts index 44962260113..70b3094f7e2 100644 --- a/src/vscode-dts/vscode.proposed.authIssuers.d.ts +++ b/src/vscode-dts/vscode.proposed.authIssuers.d.ts @@ -15,7 +15,7 @@ declare module 'vscode' { export interface AuthenticationProviderSessionOptions { /** * When specified, the authentication provider will use the provided authorization server URL to - * authenticate the user. This is only used when a provider has `supportsAuthorizationServers` set + * authenticate the user. This is only used when a provider has `supportedAuthorizationServers` set */ authorizationServer?: Uri; } @@ -23,7 +23,7 @@ declare module 'vscode' { export interface AuthenticationGetSessionOptions { /** * When specified, the authentication provider will use the provided authorization server URL to - * authenticate the user. This is only used when a provider has `supportsAuthorizationServers` set + * authenticate the user. This is only used when a provider has `supportedAuthorizationServers` set */ authorizationServer?: Uri; } diff --git a/src/vscode-dts/vscode.proposed.browser.d.ts b/src/vscode-dts/vscode.proposed.browser.d.ts new file mode 100644 index 00000000000..a15d432b3b7 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.browser.d.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // @kycutler https://github.com/microsoft/vscode/issues/300319 + + /** + * An integrated browser page displayed in an editor tab. + */ + export interface BrowserTab { + /** The current URL of the page. */ + readonly url: string; + + /** The current page title. */ + readonly title: string; + + /** The page icon (favicon or a default globe icon). */ + readonly icon: IconPath; + + /** Create a new CDP session that exposes this browser tab. */ + startCDPSession(): Thenable; + + /** Close this browser tab. */ + close(): Thenable; + } + + /** + * A CDP (Chrome DevTools Protocol) session that provides a bidirectional message channel. + * + * Create a session via {@link BrowserTab.startCDPSession}. + */ + export interface BrowserCDPSession { + /** Fires when a CDP message is received from an attached target. */ + readonly onDidReceiveMessage: Event; + + /** Fires when this session is closed. */ + readonly onDidClose: Event; + + /** Send a CDP request message to an attached target. */ + sendMessage(message: unknown): Thenable; + + /** Close this session and detach all targets. */ + close(): Thenable; + } + + /** Options for {@link window.openBrowserTab}. */ + export interface BrowserTabShowOptions { + /** + * The view column to show the browser in. Defaults to {@link ViewColumn.Active}. + * Use {@linkcode ViewColumn.Beside} to open next to the current editor. + */ + viewColumn?: ViewColumn; + + /** When `true`, the browser tab will not take focus. */ + preserveFocus?: boolean; + + /** When `true`, the browser tab will open in the background. */ + background?: boolean; + } + + export namespace window { + /** The currently open browser tabs. */ + export const browserTabs: readonly BrowserTab[]; + + /** Fires when a browser tab is opened. */ + export const onDidOpenBrowserTab: Event; + + /** Fires when a browser tab is closed. */ + export const onDidCloseBrowserTab: Event; + + /** The currently active browser tab. */ + export const activeBrowserTab: BrowserTab | undefined; + + /** Fires when {@link activeBrowserTab} changes. */ + export const onDidChangeActiveBrowserTab: Event; + + /** Fires when a browser tab's state (url, title, or icon) changes. */ + export const onDidChangeBrowserTabState: Event; + + /** + * Open a browser tab at the given URL. + * + * @param url The URL to navigate to. + * @param options Controls where and how the browser tab is shown. + * @returns The {@link BrowserTab} representing the opened page. + */ + export function openBrowserTab(url: string, options?: BrowserTabShowOptions): Thenable; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatDebug.d.ts b/src/vscode-dts/vscode.proposed.chatDebug.d.ts index ed91aa806a1..06729600819 100644 --- a/src/vscode-dts/vscode.proposed.chatDebug.d.ts +++ b/src/vscode-dts/vscode.proposed.chatDebug.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 3 +// version: 4 declare module 'vscode' { /** @@ -595,13 +595,77 @@ declare module 'vscode' { constructor(requestName: string); } + /** + * Structured hook execution content for a resolved chat debug event, + * containing the hook type, command, input, output, and result for rich rendering. + */ + export class ChatDebugEventHookContent { + /** + * The type of hook that was executed (e.g., "PreToolUse", "PostToolUse", "Stop"). + */ + hookType: string; + + /** + * The shell command that was executed. + */ + command?: string; + + /** + * The outcome of the hook execution. + */ + result?: ChatDebugHookResult; + + /** + * How long the hook took to complete, in milliseconds. + */ + durationInMillis?: number; + + /** + * The serialized JSON input passed to the hook via stdin. + */ + input?: string; + + /** + * The serialized output (stdout/stderr) returned by the hook. + */ + output?: string; + + /** + * An error message, if the hook failed. + */ + errorMessage?: string; + + /** + * The raw exit code from the hook process, if it failed. + */ + exitCode?: number; + + /** + * Create a new ChatDebugEventHookContent. + * @param hookType The type of hook that was executed. + */ + constructor(hookType: string); + } + + /** + * The result of a hook execution. + */ + export enum ChatDebugHookResult { + /** The hook executed successfully (exit code 0). */ + Success = 0, + /** The hook returned a blocking error (exit code 2). */ + Error = 1, + /** The hook returned a non-blocking warning (other non-zero exit codes). */ + NonBlockingError = 2 + } + /** * Union of all resolved event content types. * Extensions may also return {@link ChatDebugUserMessageEvent} or * {@link ChatDebugAgentResponseEvent} from resolve, which will be * automatically converted to structured message content. */ - export type ChatDebugResolvedEventContent = ChatDebugEventTextContent | ChatDebugEventMessageContent | ChatDebugEventToolCallContent | ChatDebugEventModelTurnContent | ChatDebugUserMessageEvent | ChatDebugAgentResponseEvent; + export type ChatDebugResolvedEventContent = ChatDebugEventTextContent | ChatDebugEventMessageContent | ChatDebugEventToolCallContent | ChatDebugEventModelTurnContent | ChatDebugEventHookContent | ChatDebugUserMessageEvent | ChatDebugAgentResponseEvent; /** * Union of all chat debug event types. Each type is a class, diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 839499ef55f..73335535096 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -188,10 +188,15 @@ declare module 'vscode' { */ readonly modelId?: string; + /** + * The mode instructions that were active for this request, if any. + */ + readonly modeInstructions2?: ChatRequestModeInstructions; + /** * @hidden */ - constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined, modelId: string | undefined); + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined, modelId: string | undefined, modeInstructions2: ChatRequestModeInstructions | undefined); } export class ChatResponseTurn2 { @@ -400,6 +405,13 @@ declare module 'vscode' { * will immediately follow up with a new request in the same conversation. */ readonly yieldRequested: boolean; + + /** + * The resource URI identifying the chat session this context belongs to. + * Available when the context is provided for title generation, summarization, + * or other session-scoped operations. Extracted from the session's history entries. + */ + readonly sessionResource?: Uri; } // #endregion diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index b19b106205b..c20711a3305 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -17,6 +17,15 @@ declare module 'vscode' { * `undefined` if the request was initiated by other functionality in the editor. */ readonly requestInitiator: string; + + /** + * Per-model configuration provided by the user. This contains values configured + * in the user's language models configuration file, validated against the model's + * {@linkcode LanguageModelChatInformation.configurationSchema configurationSchema}. + */ + readonly modelConfiguration?: { + readonly [key: string]: any; + }; } /** @@ -67,6 +76,14 @@ declare module 'vscode' { readonly statusIcon?: ThemeIcon; + /** + * An optional JSON schema describing the configuration options for this model. + * When set, users can specify per-model configuration in their language models + * configuration file. The configured values are merged into the request options + * when sending chat requests to this model. + */ + readonly configurationSchema?: LanguageModelConfigurationSchema; + /** * When set, this model is only shown in the model picker for the specified chat session type. * Models with this property are excluded from the general model picker and only appear @@ -98,6 +115,28 @@ declare module 'vscode' { export type LanguageModelResponsePart2 = LanguageModelResponsePart | LanguageModelDataPart | LanguageModelThinkingPart; + /** + * A [JSON Schema](https://json-schema.org) describing configuration options for a language model. + * Each property in `properties` defines a configurable option using standard JSON Schema fields + * plus additional display hints. + */ + export type LanguageModelConfigurationSchema = { + readonly properties?: { + readonly [key: string]: Record & { + /** + * Human-readable labels for enum values, shown instead of the raw values. + * Must have the same length and order as `enum`. + */ + readonly enumItemLabels?: string[]; + /** + * The group this property belongs to. When set to `'navigation'`, the property + * is shown as a primary action in the model picker. + */ + readonly group?: string; + }; + }; + }; + export interface LanguageModelChatProvider { provideLanguageModelChatInformation(options: PrepareLanguageModelChatModelOptions, token: CancellationToken): ProviderResult; provideLanguageModelChatResponse(model: T, messages: readonly LanguageModelChatRequestMessage[], options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken): Thenable; @@ -115,4 +154,16 @@ declare module 'vscode' { readonly [key: string]: any; }; } + + export interface ChatRequest { + /** + * Per-model configuration provided by the user. Contains resolved values based on the model's + * {@linkcode LanguageModelChatInformation.configurationSchema configurationSchema}, + * with user overrides applied on top of schema defaults. + * + * This is the same data that is sent as {@linkcode ProvideLanguageModelChatResponseOptions.configuration} + * when the model is invoked via the language model API. + */ + readonly modelConfiguration?: { readonly [key: string]: any }; + } } diff --git a/src/vscode-dts/vscode.proposed.chatSessionCustomizations.d.ts b/src/vscode-dts/vscode.proposed.chatSessionCustomizations.d.ts new file mode 100644 index 00000000000..11275ed107f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatSessionCustomizations.d.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Types used by the chatSessionsProvider proposal for chat customizations. + +declare module 'vscode' { + + /** + * Well-known customization type identifiers. + * + * Extensions may use these as the {@link ChatSessionCustomizationItemGroup.id id} + * of a {@link ChatSessionCustomizationItemGroup} to place items into standard + * sections in the management UI. + * + * TODO: How granular should we be? Consider removing the sub-instruction + * types (ContextInstructions, OnDemandInstructions) and collapsing to a + * single 'instructions' type. + */ + export enum ChatSessionCustomizationType { + Agents = 'agents', + Skills = 'skills', + AgentInstructions = 'agentInstructions', + ContextInstructions = 'contextInstructions', + OnDemandInstructions = 'onDemandInstructions', + Prompts = 'prompts', + } + + /** + * Where a customization item originates from. + * + * Controls default behaviour in the management UI (grouping, delete-ability). + * + * TODO: Should this be inferred by core itself depending on the URI + * scheme/path rather than declared by the extension? + */ + export enum ChatSessionCustomizationStorageLocation { + /** From the current workspace (`.github/` folder, workspace root, etc.) */ + Workspace = 1, + /** From user-level configuration (`~/.copilot/`, `~/.config/`, etc.) */ + User = 2, + /** From an extension's contribution */ + Extension = 3, + /** From an installed plugin */ + Plugin = 4, + /** Built into the session provider itself */ + BuiltIn = 5, + } + + /** + * A single customization item such as an agent, skill, instruction, or prompt. + */ + export interface ChatSessionCustomizationItem { + /** + * Display label for the item. + */ + readonly label: string; + + /** + * Optional description shown as secondary text or tooltip. + */ + readonly description?: string; + + /** + * URI pointing to the underlying resource + * (`.agent.md`, `.instructions.md`, `SKILL.md`, etc.). + * Also serves as the unique identity for this item. + */ + readonly uri: Uri; + + /** + * Where this item comes from. The management UI uses this to + * group items under "Workspace", "User", "Extensions", etc. + */ + readonly storageLocation: ChatSessionCustomizationStorageLocation; + + /** + * Optional icon for the item. Overrides the default icon derived + * from the customization type. + */ + readonly icon?: ThemeIcon; + } + + /** + * A named group of customization items of a single type. + * + * Use a well-known {@link ChatSessionCustomizationType} as the + * {@link id} to place items into a standard management UI section. + */ + export interface ChatSessionCustomizationItemGroup { + /** + * Identifier for this group. Use a value from + * {@link ChatSessionCustomizationType} to map to a built-in section, + * or a custom string for extension-defined sections. + */ + readonly id: string; + + /** + * The items in this group. + */ + readonly items: ChatSessionCustomizationItem[]; + + /** + * Commands shown in the toolbar / "New" dropdown for this group. + * + * @example A "New Agent" command that opens a scaffold wizard. + */ + readonly commands?: Command[]; + + /** + * Commands shown in the context menu for individual items. + * Each command receives the item's {@link ChatSessionCustomizationItem.uri uri} + * as its first argument. + * + * @example A "Run Prompt" command, a "Disable Skill" command. + */ + readonly itemCommands?: Command[]; + } + + /** + * Provides customization items for a chat session type. + * + * Registered via {@link chat.registerChatSessionCustomizationsProvider}. + * The provider is called when the management UI needs to display + * customizations, and re-called whenever + * {@link onDidChangeCustomizations} fires. + */ + export interface ChatSessionCustomizationsProvider { + /** + * Fired when customization items have changed and the UI should + * re-fetch them. + */ + readonly onDidChangeCustomizations: Event; + + /** + * Provide the current customization groups. + * + * @param token A cancellation token. + * @returns An array of customization groups, or a thenable that resolves to one. + */ + provideCustomizations(token: CancellationToken): ProviderResult; + + } +} diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index b9b2846f493..8131b4c90ed 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -57,6 +57,18 @@ declare module 'vscode' { * @returns A new controller instance that can be used to manage chat session items for the given chat session type. */ export function createChatSessionItemController(chatSessionType: string, refreshHandler: ChatSessionItemControllerRefreshHandler): ChatSessionItemController; + + /** + * Registers a {@link ChatSessionCustomizationsProvider customizations provider} for a chat session type. + * + * The provider supplies customization items (agents, skills, instructions, prompts) + * that appear in the Customizations management UI for the given session type. + * + * @param chatSessionType The chat session type to provide customizations for. + * @param provider The customizations provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerChatSessionCustomizationsProvider(chatSessionType: string, provider: ChatSessionCustomizationsProvider): Disposable; } /** @@ -99,6 +111,8 @@ declare module 'vscode' { readonly prompt: string; readonly command?: string; }; + + readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; } /** @@ -106,6 +120,20 @@ declare module 'vscode' { */ export type ChatSessionItemControllerNewItemHandler = (context: ChatSessionItemControllerNewItemHandlerContext, token: CancellationToken) => Thenable; + /** + * Extension callback invoked to fork an existing chat session item managed by a {@linkcode ChatSessionItemController}. + * + * The handler should create a new session on the provider's backend and + * return the new {@link ChatSessionItem} representing the forked session. + * + * @param sessionResource The resource of the chat session being forked. + * @param request The request turn that marks the fork point. The forked session includes all turns + * upto this request turn and excludes this request turn itself. If undefined, fork the full session. + * @param token A cancellation token. + * @returns The forked session item. + */ + export type ChatSessionItemControllerForkHandler = (sessionResource: Uri, request: ChatRequestTurn2 | undefined, token: CancellationToken) => Thenable | ChatSessionItem; + /** * Manages chat sessions for a specific chat session type */ @@ -143,6 +171,14 @@ declare module 'vscode' { */ newChatSessionItemHandler?: ChatSessionItemControllerNewItemHandler; + /** + * Invoked when an existing chat session is forked. + * + * When both this handler and {@linkcode ChatSession.forkHandler} are registered, + * this handler takes precedence. + */ + forkHandler?: ChatSessionItemControllerForkHandler; + /** * Fired when an item's archived state changes. */ @@ -397,6 +433,22 @@ declare module 'vscode' { // TODO: Revisit this to align with code. // TODO: pass in options? readonly requestHandler: ChatRequestHandler | undefined; + + /** + * Handles a request to fork the session. + * + * The handler should create a new session on the provider's backend and + * return the new {@link ChatSessionItem} representing the forked session. + * + * @deprecated Use {@linkcode ChatSessionItemController.forkHandler} instead. This remains supported for backwards compatibility. + * + * @param sessionResource The resource of the chat session being forked. + * @param request The request turn that marks the fork point. The forked session includes all turns + * upto this request turn and excludes this request turn itself. If undefined, fork the full session. + * @param token A cancellation token. + * @returns The forked session item. + */ + readonly forkHandler?: ChatSessionItemControllerForkHandler; } /** @@ -447,10 +499,13 @@ declare module 'vscode' { * * @param resource The URI of the chat session to resolve. * @param token A cancellation token that can be used to cancel the operation. + * @param context Additional context for the chat session. * * @return The {@link ChatSession chat session} associated with the given URI. */ - provideChatSessionContent(resource: Uri, token: CancellationToken): Thenable | ChatSession; + provideChatSessionContent(resource: Uri, token: CancellationToken, context: { + readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; + }): Thenable | ChatSession; /** * @param resource Identifier of the chat session being updated. @@ -483,10 +538,11 @@ declare module 'vscode' { * * @param scheme The uri-scheme to register for. This must be unique. * @param provider The provider to register. + * @param defaultChatParticipant The default {@link ChatParticipant chat participant} used in sessions provided by this provider. * * @returns A disposable that unregisters the provider when disposed. */ - export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; + export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, defaultChatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; } export interface ChatContext { diff --git a/src/vscode-dts/vscode.proposed.findFiles2.d.ts b/src/vscode-dts/vscode.proposed.findFiles2.d.ts index 8e7c11874b4..af324e90719 100644 --- a/src/vscode-dts/vscode.proposed.findFiles2.d.ts +++ b/src/vscode-dts/vscode.proposed.findFiles2.d.ts @@ -71,6 +71,12 @@ declare module 'vscode' { * For more info, see the setting description for `search.followSymlinks`. */ followSymlinks?: boolean; + + /** + * Whether glob patterns should be matched case-insensitively. + * Defaults to `false`. + */ + caseInsensitive?: boolean; } /** diff --git a/src/vscode-dts/vscode.proposed.findTextInFiles2.d.ts b/src/vscode-dts/vscode.proposed.findTextInFiles2.d.ts index c7c7c9507a5..4575ba17d0c 100644 --- a/src/vscode-dts/vscode.proposed.findTextInFiles2.d.ts +++ b/src/vscode-dts/vscode.proposed.findTextInFiles2.d.ts @@ -91,6 +91,12 @@ declare module 'vscode' { */ followSymlinks?: boolean; + /** + * Whether glob patterns should be matched case-insensitively. + * Defaults to `false`. + */ + caseInsensitive?: boolean; + /** * Interpret files using this encoding. * See the vscode setting `"files.encoding"` diff --git a/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts index 5fa6524edda..04a50dd823c 100644 --- a/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts +++ b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts @@ -3,21 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 1 + declare module 'vscode' { // https://github.com/microsoft/vscode/issues/288777 @DonJayamanne + /** + * Represents a single MCP server exposed by the gateway via its own HTTP endpoint. + */ + export interface McpGatewayServer { + /** + * The human-readable label of the MCP server. + */ + readonly label: string; + + /** + * The address of the HTTP MCP server endpoint. + * External processes can connect to this URI to interact with this MCP server. + */ + readonly address: Uri; + } + /** * Represents an MCP gateway that exposes MCP servers via HTTP. - * The gateway provides an HTTP endpoint that external processes can connect - * to in order to interact with MCP servers known to the editor. + * Each known MCP server gets its own HTTP endpoint. The gateway + * dynamically tracks server additions and removals. */ export interface McpGateway extends Disposable { /** - * The address of the HTTP MCP server endpoint. - * External processes can connect to this URI to interact with MCP servers. + * The MCP servers currently exposed by the gateway. + * Each server has its own HTTP endpoint address. */ - readonly address: Uri; + readonly servers: readonly McpGatewayServer[]; + + /** + * Event that fires when the set of gateway servers changes. + * This can be due to MCP servers being added, removed, or restarted. + */ + readonly onDidChangeServers: Event; } /** @@ -42,14 +66,16 @@ declare module 'vscode' { export const onDidChangeMcpServerDefinitions: Event; /** - * Starts an MCP gateway that exposes MCP servers via an HTTP endpoint. + * Starts an MCP gateway that exposes MCP servers via HTTP endpoints. * - * The gateway creates a localhost HTTP server that external processes (such as - * CLI-based agent loops) can connect to in order to interact with MCP servers - * that the editor knows about. + * The gateway creates a localhost HTTP server where each MCP server known + * to the editor gets its own endpoint. External processes (such as CLI-based + * agent loops) can connect to these endpoints to interact with individual + * MCP servers. * * The HTTP server is shared among all gateways and is automatically torn down - * when the last gateway is disposed. + * when the last gateway is disposed. The gateway dynamically tracks server + * additions and removals via {@link McpGateway.onDidChangeServers}. * * @returns A promise that resolves to an {@link McpGateway} if successful, * or `undefined` if no Node process is available (e.g., in serverless web environments). diff --git a/src/vscode-dts/vscode.proposed.taskRunOptions.d.ts b/src/vscode-dts/vscode.proposed.taskRunOptions.d.ts new file mode 100644 index 00000000000..4f874a3354f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.taskRunOptions.d.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * Controls when a task should be run. + */ + export enum TaskRunOn { + /** + * The task is not run automatically. + */ + Default = 1, + + /** + * The task runs when a folder is opened. + */ + FolderOpen = 2, + + /** + * The task runs when an Agent Session worktree is created. + */ + WorktreeCreated = 3, + } + + export interface RunOptions { + /** + * Controls when a task is run automatically. + */ + runOn?: TaskRunOn; + } +} \ No newline at end of file diff --git a/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts b/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts index e4bc845c46a..c7c1a7b45c2 100644 --- a/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ declare module 'vscode' { - // @anthonykim1 @tyriar https://github.com/microsoft/vscode/issues/227467 + // @anthonykim1 https://github.com/microsoft/vscode/issues/227467 export interface TerminalShellIntegrationEnvironment { /** diff --git a/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts b/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts new file mode 100644 index 00000000000..070cf51ef40 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @alexr00 https://github.com/microsoft/vscode/issues/302393 + +declare module 'vscode' { + + export interface LanguageModelToolConfirmationMessages { + /** + * When set, a button will be shown allowing the user to approve this particular + * combination of tool and arguments. The value is shown as the label for the + * approval option. + * + * For example, a tool that reads files could set this to `"Allow reading 'foo.txt'"`, + * so that the user can approve that specific file without approving all invocations + * of the tool. + */ + approveCombination?: string | MarkdownString; + } +} diff --git a/test/automation/package-lock.json b/test/automation/package-lock.json index ceeff39f6fb..cbc02be2a4a 100644 --- a/test/automation/package-lock.json +++ b/test/automation/package-lock.json @@ -474,10 +474,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png index 6e97bb89568..a5e5f2c31d5 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb56239cc915c18dbdf70d98049cc8386350c6e394b988a2df86df95ef10b52c -size 7064 +oid sha256:b7155f9553b5a2a73f3b6c688f9e9d28eba4672baea301954de9b625870a4903 +size 7147 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png index b8635ead60e..5156ddb6df1 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:163f0620ca91d6a7636ec58362e7dbc53a338fd26d2c9577ddb893c880bf86aa -size 7053 +oid sha256:c45d756c3260556a43f3ef64c7b317c57c8a05ca346707fd8973530ef3a6c4e0 +size 7000 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png index 654a095b444..28bc45bc01a 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5d405d46064d7ae8cf9917587c51db8b80528590d4c9718729460781aa25ff9 -size 8657 +oid sha256:dfb4ba128abed2fc39d2e831f49bcbb1627db497d99784c379213ef8bf556868 +size 8873 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png index 1350551e929..0dbd75ca331 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02e137eff8bd38674c35f3d4ab472ff380bfa0c31e0f261e64a66822665c98a3 -size 8717 +oid sha256:b2296ea0896451f871ad0b26a1a467409925c14f2ec0691b67eb5dd4700d7504 +size 8566 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png index 9aa34fcadd4..f1bf597ab55 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:029ca626c8b89d7efce7d86b401774373d473282fa29b45f2a0b6eff314090da -size 8737 +oid sha256:8b5106a3cad9097615fb7605781c5d55f6fa2476dc3d11e95f7def05028b181c +size 8757 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png index 11afd7ebade..745f60ffbcc 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98519a1e1420101c2efb7fedf417432aa9509b3614d0df4fd51e57b02f791a9c -size 8684 +oid sha256:48b59d3cb5d0824f8ac11cdd501ce2312d9105d8ec647a046649dc5e076a0c75 +size 8528 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png index 23cae64ce72..64057dc612b 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3619142a6234cafb5b20bae5007daf1af201bb58f8b0f1e7e404621a77d74123 -size 12355 +oid sha256:2ce24147c0d898af9c50e12a81471b028eac44642964b34e1e07a9cc206475a3 +size 12428 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png index fbe36047f39..15047bf50bf 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:717467794a22315bb378be412b331db00635d0171043deb50fa6e50a3caf9af3 -size 12363 +oid sha256:50ec39d48de2e7b5ae5403ee682af6967efbf1b3053b0df25a1ed24920bfcab2 +size 12116 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png index 3f081322682..191c180c457 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a68b3828da8ddd4254a8ff14d740d07ed9463012a6646d466ad052f917080462 -size 9167 +oid sha256:f0198937bce59b0f46d45041fddcfda6d2ad5bd98a58aa94d1cc1bfa29051ef9 +size 9258 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png index e3e96fe51a8..d0e497d654f 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29a3581395521fe377cb76135f9df237ba8d6d241b5bcee4707a4c92e560e410 -size 9169 +oid sha256:e54e12979392e0a1818581203d3160c79c622888e987f1e8edda0593c9141fb9 +size 8991 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png index c6025168a8c..9b05aa9866f 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:349e1054718733508b1e8ccab0c08039e13319458a1a4e730d98531c9f8065a3 -size 7889 +oid sha256:178269938c3395dba69e6d7fc495648b0f66485842c93969b940f70130da6c80 +size 7937 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png index c9bdeeb188d..4329f23b9ce 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49dd6690cb406f928925ad0000624128efc5e60999a6472f5c14f5d34a048b8a -size 7940 +oid sha256:5fa69fb1ac5c2d5568195ee032c59e051514560d8bcea7ab8637eb8cedc49f3b +size 7715 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png index 29f4c5e28a7..0d4f330ee7c 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfdc23fd51094966957b9487c71aa7dc69d9ace05bc9315adf6e8ae296de3d89 -size 7338 +oid sha256:29a3db08cf07dab1daabc6b5a9562e830aceece92a4dc9e396a6fc5d80cb54f2 +size 7381 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png index 7684550a5f6..0030227e180 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0fc1e6917fd33d4c46dd70b74d787bc0c2bc2b2d6f38c1d1c8923ca84b11009 -size 7439 +oid sha256:15b588c73f00a3402e6581b32cad3c995ec8cb5b3b15f2dcc9430ec6d0cedd24 +size 7106 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png index 40cf1fbcec9..e1a15a9d049 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6827c363512a28c506670d9bce96efe463bb2e7b16864c1d3ef78bdb27c5d8b9 -size 7915 +oid sha256:431d04f280a0d2d972b241e1b4e9aad49ade79c2a9010a9cdaa214bf04d1a344 +size 7971 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png index 035820555cc..40dae0d8e21 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d90a5b77afd766a79dc78ec15369ae6fd8d37791f3b365cd51293b86089a268 -size 8005 +oid sha256:fbe33f3f7006a2b54a9ec0982d6593e282f45a3a5a23f7611e518d4ef8eb4c0a +size 7682 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png index 843641053a1..85e36b633a1 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10b958efa32aa1dd506894c1663639bfd8252907b3640e4d2c6fb1893f0798dc -size 7497 +oid sha256:5a869368e44352e48025091169edcf760b5a3e9758658752fffe21b00e1bc0dd +size 7641 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png index 4f679b73ace..4672153cfef 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:083d37a41eb68a5f0841c4461d4ee9df3e5e093b7295d069877fce8933c30581 -size 7548 +oid sha256:3fad41c3140ba3017ef9d61b1e45794f9736788cb9879412e1b15f4322562bc3 +size 7377 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png index 18b0f2f3393..99d17012794 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcb0046c2d6bf62981df0f57af2f04bc21abff8f2c25bda0bce72577c8825234 -size 3955 +oid sha256:32f1037934a2628a1080e4b46a0ae6e32701d9d197cde35b173e494cbe86a08d +size 3779 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png index 0518b7db987..62421de8a28 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6955c3424c2907b6b8472c60e5c5cee6ef104dd8482fd932e0c83ba611c3522b -size 3883 +oid sha256:c9537093a98494c154e50782245bbce419bf5cc0c3527e304f12f8c4be0e71cc +size 3566 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png index 2e9a78f3e9c..8d883b9fc01 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:761643f249dddaaa1e7f307b3372463e252f67ed30cdd860052ca2b1b9534601 -size 4136 +oid sha256:0a230dd58732488dc7aa091c0b799c7b7a6134785a1dc4f874ec22b248c39388 +size 3951 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png index c4edc35a169..69c3e705804 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed79c20b8c4adb9c4253ebed499554e3d8e898b0b5f9d389c8ce865345b85ad6 -size 4059 +oid sha256:ca40bb99c6af4be97d88d36a7d3f089b6f4b5031047e807fde258c254b22e51d +size 3831 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png index 2f4f1029af5..5c54f5e0562 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:211c5915f952e93eede04b6ff57b552ccfd7a524ec17ded20819f531e07289de -size 4323 +oid sha256:4ec9c318b01e50a9fe1c4bf062a1d883687f53828498019bf120004559761fbf +size 4607 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png index b5a83bc3227..ae323fac953 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76981d631106433e066dd7b6354337c3bbba1018587beb26ebd9d662f25093c0 -size 4440 +oid sha256:415881574ed5f256d335c35aa4ca99eb14d4d9f16852d44936d2bd2d6ecae966 +size 4146 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png index 64427aeaa23..26cd16b27e4 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd6f5753251dfd4ebad48ac881d2a18baeeb6dac683d8c991b23676d80e79f7d -size 4627 +oid sha256:dc60ffff830121bcebbca2f484ecba0757ca57489f232014351bc12a3e531331 +size 4671 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png index ff344e500fe..94678888209 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46d1010d543349b526f2b6cdfc17177fdda11cc994a6522c6a827c883755e21b -size 4720 +oid sha256:fc8875b454581c3e45add2aa004b3758088826658c88fee7733b1e35440775a1 +size 4234 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png index 2ffb6d1dfa9..85f37cf7cd2 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a357eca1ff0edae842d2137e53c4648a8a24dda806056f7cbb047ea02bc05250 -size 4402 +oid sha256:0ec88636c30d6279b632039d867421e54f8c3245d7d7b9244b1cf3519c2e2c48 +size 4649 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png index c7f3c6cdfdb..2568fe5e3a6 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4127bb1d38d3c49f0393afcf4d04415f328cccbe8473de7e0894b671f5c6ec51 -size 4475 +oid sha256:52540d6c240b195bcf79b9b162e90ef3c77647c9aa535392c6100c216f824329 +size 4187 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png index 94ecc186613..764411c8d68 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11e38388dbe35d160b60aa8d8b1b45b2a71c9604caed3b610b318f4fc5beb5c0 -size 3746 +oid sha256:8158b9d796710de28f6427cfe7ddc944154f52d565a0b1102732a4f46c75faa0 +size 3807 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png index 6c780125a43..ecf28e6ae58 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:768a1bfc002b37facc1d7e7c4a096148134b2f037f6739611b0ebdac8694515e -size 3741 +oid sha256:5fe4c3e49a571ad08a17725afae750b7c065168c03a3ac6cd078ccfedb097838 +size 3500 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png index 9379588d303..20e498166d5 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ef5c71cd24e7527c325436f778498f4ce843c4118fb6df9132d152d195b77b5 -size 4131 +oid sha256:8cf33e304c8b36ccb6c26b0d287fb15b2be47205a05a510393d52a9ce616af43 +size 4249 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png index cc372129852..b5e52517c31 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:461481ef366395b7a3283de9dbac92581b182420f6439d15af3c5ad1b95f315f -size 4236 +oid sha256:58a4540fcfd24a31f589feb40e102f3469b6fada043fd8e3fc774bf924cdb5bb +size 3869 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png index e7b1e95cc9b..d53e5e56755 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29611b9811d052d0c311abf299aaf4009eb4cf9adc39b50a362f23dbb30e3012 -size 4281 +oid sha256:66033618caf00d282c615c6f8a3f17b7df7d21d3ad9a1dddb43ef500f6580fec +size 4430 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png index 53e514e305b..142e3aae679 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cafcc1fb16a92c55106474771945cc7063152922633db3d3acc8f455677e93a2 -size 4303 +oid sha256:18bc4073dae0b8cff55751aebb25c96eaab45c4d7d67f86f67d2cf4073190e04 +size 4094 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png index 0f129c39bf1..2b894203c70 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f18d15f95ce04361389e3685c0d04d5b5846ee7ac97f192ad75605879e3ef711 -size 3952 +oid sha256:0f4897aea097bf3c15dfa6a204edf057f4f31477df9dd9f944d09a0db69e9d9a +size 4349 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png index b2c232971db..a6371f6cf22 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:823534baac31274e97febf864f4c70430672f573a35abe360c406f0ef8394563 -size 3984 +oid sha256:cc6730a4307dcc431da8121a7c29ca541050c4eb699c79c88a749765594adb8d +size 3952 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png index 60ae250bdb3..9bb2506cb64 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99a4986f49d637f978fdef48c75c7307b1d2fe60c0a301682481a2d9271fe9c0 -size 3473 +oid sha256:245c47554af6bf0bd7693437926e7e842eaded1278da97a99e16e597f9bdfdf1 +size 3640 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png index 77c3257a44b..c30b0777d4d 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e0c6543a97ca59eeb9bced9530976e6697e31df4cc0e392dae5721cdef90a35 -size 3590 +oid sha256:13170bf59063ac0341790b635ba177d7ce9437ba98d3b55395da00755d804431 +size 3358 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png index cf64cff29d7..7ddf0f6d258 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39e4ce2cd4020f790d3a2f54ab29da693ce34b35a82a4ef9ebbc7e685b6c5f29 -size 3942 +oid sha256:8f8df8fa86d9c7fa1bb42af13743cdc6821d4035bb81e3a902eb2c00db8a6f6d +size 3756 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png index 4e2ed61233f..df11ab1bbf6 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c0310d172ee83a1f0260a7731e321a8e193dbb4c58afa0115bf3e2246b623db -size 3997 +oid sha256:28b20f0e2b9bb4d0eb0745bf3705b10de5d046a931577693589281b8c7f13833 +size 3412 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png index 75e19d71278..31d466d3f32 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9a68bfda118629ddd5095f2fdd202c5baaccbc6b92d1d9584ef5053113af328 -size 4693 +oid sha256:ba81ad8340da42129ad9b3c71bd2965477367d0b9d2f61df7f417d727704958e +size 4999 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png index bc5cbfc0de7..f3bc8cf4a55 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:616343f0b523bd7a88c02349345baedebbccb3251b34030903e5afe54c14ee0d -size 4793 +oid sha256:80dd6f30b8b9ae1b434bfa7939e7cbd2fd92c28f3c9b91b3e4f4673dd97f8306 +size 4554 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png index 50cbb3efca4..27c882816fe 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea3e3728408c21a161655ad68304442ea348f4c95b613e5abc5be491073d5a11 -size 3767 +oid sha256:2b75c5544a73bc8df9bef473a3030d2528b240e5276ad8fb32c8ef83c747f82d +size 4166 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png index 82d68f0fd0d..8cacceeb4fe 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78eb1956b1a37699a678f73d79cb6c1d7b4deda79e857c03017910054fdbae90 -size 3815 +oid sha256:4d3072457cd7ed569a80ba450d823edab281f4496c4167c9e36232f9bfb8daff +size 3856 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png index 60acf6fe15b..65a4274bee0 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67e30acb57253a7a9c02e93e8120019eb1afc9932a99c59a241968ae75ee3752 -size 896 +oid sha256:876f8152573adcbc44b60900c08d4cae718ca1c3c99f11aae7b0c01a6634f3a6 +size 865 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png index 44015ee3881..63acc3bc308 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d6a95e55db9415c49043745f6eb5682e20137d0d487edb6323ef101c2e46016 -size 870 +oid sha256:4583e8349f5864c44a89eb0caad9f1efa52a9277c266e8e8c7f5f1e11d1028e9 +size 840 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png index 130f5f1c40a..6795aadfaf5 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:379ebb29923b4874351469a1a60a7e4bc76397c85e61741dc86361dd123f68d0 -size 1032 +oid sha256:ad5889a65e602d67b932662f001e31a82853d074754bcc9db3587d0a3aae46a5 +size 1061 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png index cc2acbb5916..ab7ccf0b34e 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:109ad01629c5fb42759102c4a9fdf3b9e4f69452d06d6b2f6b478be418261e01 -size 1013 +oid sha256:0c841749dad41b2c33857ac796b6a10ba9b9176e1f851666beb714a2cfc7f464 +size 990 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png index da3fbefb55c..fab3cb475f2 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d1c9f3fba50deacb99898e7e403239716b918e4901258b155f3c87558938219 -size 634 +oid sha256:4896fbbce4d6a2f65b3decde8092b6137b3bdb2b8e4b2250d5c802668d9f0c93 +size 619 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png index 1fd51cdd988..f50f76ae585 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9083bcd9e6dd0fddf1b86389abbf2c8be5db4c84a80870acee1ebb6209b3734c -size 610 +oid sha256:07cdda58e1e786f4812b6ef572c288615909f77757190ce996f4a88f9f3a4f07 +size 605 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png index 5a3f629e0d9..b617367429f 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fb24f8743a805c51eed4cb0822e6f031f1663ebf3860fa0f555fc9105d6aa47 -size 655 +oid sha256:fb59c25f4d1095ee959ce9603512838390abffbb09fcc4ea5d4f899629409b06 +size 662 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png index 9a01d0f11bd..6da888c5da1 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b33d4a2df45aadb2ddd03a4e78107993407390a0fcdcd551cc5e46cfe3bfd79 -size 629 +oid sha256:9de100ef9cb870c52565e7e6bdc8e33668c815d1ac6447a54091f1574fa414c1 +size 626 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png index 7df9b9dc3e3..b28777175a0 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8dba102faa5ab22be300c2f3b144ccb9fd7d191b7ce345934aac0cf2a22a4cf2 -size 700 +oid sha256:606e21dd29a336b824c9606f2139d1672ad8683f7b0a78e71cb847485af47f28 +size 711 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png index 97bfabf75bb..460b5f78684 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0896ebcac18fc2c67051318eccf952a4af9cded093bb8ed22da0d4f369a90fd -size 691 +oid sha256:d883e537873c84feda5ad8b2dd103cda295e7947669a893aa25e04a2f4627558 +size 676 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png index 44f782bea73..533ae169541 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf4cc56f6e7472b91ee5291f7b7963c88c575152c01914af66b79d3f983072a1 -size 1034 +oid sha256:5e1a8c0f8f530a929fa0276fb710e023ed3d3a9bbe7badaec4853d11ed0f28e0 +size 1012 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png index fd7a76b1e55..a6d8cb0633e 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f11009c1529cb97619d437c5563764d6cf9deed1db7d7b539506a8500d58694d -size 1011 +oid sha256:5d09751261d5ad4ec543b2fa3756e32ceed4ebe5b0a420005200df8deca41e66 +size 990 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png index e8a6dcc8dde..62003cbb881 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dfad96ea810926d39db4d8f689f66c717fd4a2b51de380010046e7610ec14a4 -size 4819 +oid sha256:0d0c710cf4ea293f1c12e431a86a7169795f065a25c66bbdb18ac2a1de694781 +size 4563 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png index 7605ff11511..820d3231991 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b6b40e3ab4d73221357a136dcac51eb0081c344a98930dd5e738662137af955 -size 4886 +oid sha256:b9bcf5596376755def0f736d3ce1a22977cea78099e1557bd8bca3158e657a46 +size 4216 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png index 5e3bbe18e1e..aece8b7bbcd 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:768613c86e446829341642343180cd84a0376dfafae7399dc2a50cf3b0e21575 -size 3900 +oid sha256:5e0b8668f09de7976d171422ce13dd3abe403d493d19c77a497dd59f77cbd865 +size 4014 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png index 3b1b9440317..2a7d7aaccee 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d471d8eeb3bcf0a285a3b80a7dac70d5258c7fee903185377bddad1199562259 -size 4018 +oid sha256:01c1071638aa340fc5b6ae1d67494d7aff2497ec55303f80f03a6b377712db47 +size 3795 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png index 842e5652928..35220dbe21c 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83b83d1b89bbee53b1e7f81990b30e8f2124b2099e0da948bcc0e807d2593f1e -size 5389 +oid sha256:21b720d79db121c1a84de56e9da7799104079448df1f686bb22cf836ed81ef65 +size 5363 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png index a15244fa8b9..3a1ba03c765 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39cd38f065604d4c1ea6564d0da9cd5436e7587547583783408c7e7df3812f80 -size 5507 +oid sha256:0e3e5e39880fc09156347722c64ee6aec236d88bb905343a3b025fdb4d386933 +size 4724 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png index 760959402ec..ac2854cc50f 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ea6dc5746ca3dba73ee8a96df90fd2c4f34625d58ac9d616134ba5d2ff5f87 -size 3473 +oid sha256:db11f8e8859a5c183f377c4c316a3741020c8fedf284e5520b1b744e28de1627 +size 3969 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png index 14d28a1f999..24fa1c62212 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d502f79bc59e00f539745df156fa704cc3db11c16653633943b6edf26401e25 -size 3617 +oid sha256:2c192a041414782a95fe785e3987d649798deddd109bddd14369e7bf170834e9 +size 3780 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png index 10de39113fe..e4ebc023ec3 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e652cd030f299556fa1fc6455533c38ac953cae0bd472248aed8034d7f19a12e -size 3129 +oid sha256:c66acc3e198c4e80a048e909ab32842dc9bc78f0a07c582366c7a295ae97debf +size 3512 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png index 980b2d57aba..50a0a40ca7d 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a189aa2bcce54111a8c078985ca611efde6e10dde26bb227bf1afafa2d065cc0 -size 3250 +oid sha256:057ba5970b621449cca39908ad2ad1451d44308cbaa7daa37b4cafe46909b550 +size 3327 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png index 0a497294064..56b2df20542 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c44f892307b93c5bb48b836d47c5c66a318e299ed59ae96ca8107eecb5ad1012 -size 5621 +oid sha256:ed73e9654db907d45f23b763d63fa7bbfed7141e28a0d72df93b09d15954e043 +size 5946 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png index 86f7c7e3145..8b3a0d35713 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82245981f37c430eab84149196ccc9769fc90347b1b8fc28942dcdd7d3a5357c -size 5711 +oid sha256:11afe0163951c25415ce22c96fb8b91fca4c386e4ff328a4eac4d1a446558803 +size 5359 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png index 456cc163ea3..94312b08c74 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b702e865b2a24a3ebf2028662a7aa1f9dc4176e90761aa50627736a65f4a8000 -size 5368 +oid sha256:ea9492fa76b7067a2750a0ba9952e2bb7c90ed781e01947cad7183b284cf5988 +size 5838 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png index efa1d0dbda7..2d7727df428 100644 --- a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cf08481a25b64169c03ee6321326f2108ed37a08b4ade5e75c7b3ff13e63ce7 -size 5393 +oid sha256:eb283eebd9c949a2b219b0ecf6f5e4b120de418395477ea21fb95c643104f308 +size 5312 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Dark.png index a74b5296d1a..b76b81f3ec2 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b60cfef71eacaad36a4e96f73f5417b62454834c682ea8d5753376730e6bee96 -size 7564 +oid sha256:0296fba7f63e6a159c17070fbaacceadacb11c94385c5669ceede625a7bf43da +size 6552 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Light.png index 813f2881117..8dac7be977a 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/ActionBar/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c958ce1152e12f156e4c4b8621f504c0eaa3fc3fc2917f21d7e41ab9b6a380 -size 7687 +oid sha256:cfe2befb6e478ce4334695ddd953f53c306460bcd9efe365f05652a9289c08dc +size 6765 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Dark.png index c1bcd6e1aa3..2a9b7a05f06 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f76c6c4205d17a529e16519b7cbc96008f9c99c1d0f444c849f1e7483c6cd694 -size 6834 +oid sha256:acf2db300158ce2154b12e4cf5db67ff2616f304f0b3c33bf04006e0a27e9d62 +size 7142 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Light.png index 8ba87df3aa8..b97748e480c 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/ButtonBar/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f7f6aa3b88072a5091f13518d46b52811fddffe928af9451d7e34b60a6d24e0 -size 6827 +oid sha256:ca0b996b3af6e086147230e1524c3b7a0633322777323cd5c966ca6a5a4a6840 +size 7266 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Dark.png index 0d199f49f95..8bf6178d3c9 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ef5657663669cbf98c1bb45d00f2f80c34a261d63db46aea381b64c0dc0b915 -size 9641 +oid sha256:d411e54737298ae538fdb28003f08f052bf9e751dd377624dde04092647a4a82 +size 9050 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Light.png index fe6af01eca0..2e33cf75f47 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/Buttons/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29fbbb20bd36eabf1e23a659872b66bcc3ba7ecbec5d2f2f2cfa63ddf46b92b2 -size 9661 +oid sha256:781cc65d8c818c23a4b6122a6439ea144cc8bca2b2f3a9b0a73f52db40dcabce +size 9124 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Dark.png index 9dff836eb82..67d4a51208a 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e77fe6e326387bc7013ead48ed9e8c3dc7fd80b795c29e5d4dd48cf6998d3c44 -size 4424 +oid sha256:2f8842572f67432757afbcfcf08a64e4e6270437ff493191b638b40498cbdd16 +size 3167 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Light.png index d7035b0ed58..0349a3fe691 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/CountBadges/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:769ec6f7ab75359694478e000a407b4497d080e064fd5bbaf13699183aceb2a0 -size 4258 +oid sha256:41cdbf685d4e9157206fe37152efa74b86ccea4f0030e40e04f5381b02436613 +size 3053 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Dark.png index ee0b9f27b97..e4869ae73e9 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3800748a22233bf6fcd52467bbb0e7bc2e7627625d81910d598bbef3900d9766 -size 18284 +oid sha256:b7c053271845275bcea9706011b1bdcda1d8f0f31bf47d58844196e3623c1c9e +size 12420 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Light.png index 3798f28702d..38ba0912461 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/HighlightedLabels/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21952b54f0a4dbf85dcb8e46e3bba9331be8c9068f08fb1fcc28e94562a69b79 -size 17757 +oid sha256:af33528dd520bd936e40d7c4443ea398bab1381d8fe3566c6d44b751f7b5074a +size 11650 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Dark.png index 738aea249bc..6a7ee7c8d7b 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb7ed5ba73e484c563806e3e419f688580fe08490efb9883e86144440387997d -size 7097 +oid sha256:92cb33481db59756768979f9c09ebfdb1be33efcf8d311db72720da6bf618596 +size 5592 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Light.png index d247eb3f93d..d183c2cf186 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/InputBoxes/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49d5f350123acc511ad93f9710bad4ea02f35e0455249782db854c22b1bb731d -size 6869 +oid sha256:8fcc51d5e549ff8ded93cb43cfdae31e8871e8e1f6f6574e346256690d05cf13 +size 5411 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Dark.png index c1901de3f12..7d4d98ddd91 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fde3798f12ed5555c865d8be18ed7648a8229a38630cd74423f026818341ff35 -size 8951 +oid sha256:bb18ac66005d1947438967ab77e8c55cfd55864c25b09594f212290f335786b5 +size 7948 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Light.png index 94d47c7eb4a..ef77c9443f6 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/ProgressBars/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:609dd0f1d3d2a0a558960f2f83a743570d1b7cd6f53f40e09326fcd7ed83bcd7 -size 9062 +oid sha256:fdf30bcbc30af906b9398396c3c7f952bedd9df0134c1d6f2eacbfe175e1e90d +size 8019 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Dark.png b/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Dark.png index 4c155947921..c47018f88db 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:917b0856dd3a96b94166d431d633a0fa5fc32428bb468de9058a56fce31bfe79 -size 7010 +oid sha256:3a000b80bc413322bd1c52aa5c1de9480de55e4d42dbd296a45b3bdaa1596bef +size 5214 diff --git a/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Light.png b/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Light.png index 4345d2aa11d..0943f944a72 100644 --- a/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Light.png +++ b/test/componentFixtures/.screenshots/baseline/baseUI/Toggles/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:039a38c6c47e488c24d7fb39516d70655edbdc661491b5175aa49898754ac26b -size 7030 +oid sha256:689f48259d2530a33385606f0ddfabf234f7be1ca1f9c89e1d95db6ad987817c +size 5226 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png index bfe8d842cf7..b30f1c858b0 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0f41bf819fed7de2b99f15776cdb0d353f9bfaa4526dbb857f12be7c1343881 -size 15185 +oid sha256:4548c6c6ff6392627ad501ef934cb1b908aa9b53eadc2caff19e13e20dcb6217 +size 12451 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png index 914374bffd9..24d49dea550 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50328c742ab8f5d52b66bc5e693277733be6adb62262925c16527e92b52bf47a -size 14309 +oid sha256:ffd91fd0e12f87bcef876984ed35d4130d2d551dad425ad5f3384741ab35cf56 +size 11664 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png index 5657241a83e..15274f77cb2 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8387aba7570e867f1b1fb8b6257944f1458e866ceee9c7d81b7547e82c730c66 -size 13527 +oid sha256:3767494b7dcf659368bfe373f7591abe386de46553042aa6e5ea1e5f049cae44 +size 11337 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png index b09c6acc05a..c7f7ebefd61 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ed5257c12ad2a6d648fdb598748241239409219c8cc318f8be27bf377452b8d -size 12674 +oid sha256:7553211572af5a5b64d29a51ce002f07ac8bf5084673a857fa423f46ba8d46aa +size 10675 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png index 7093082f758..ad62d35d0fd 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84ff989328d86ec68ab4b2771e8798051794c85c80e830e6061bf7b538dbe342 -size 1998 +oid sha256:7f60d4a23e8184648572e9a80154f05fae6f5e4e341e310dd6387381e1b6b316 +size 1927 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png index e490a13bc02..4722dd65d3b 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d590d4f26fb679fc1c0be2031660fc0568d0d5512e8f7272c81ad6932cde7079 -size 1994 +oid sha256:757de136addd6e36a327482fb9f33fe7850ded285c4a8f745867f03bd81a94c2 +size 1934 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png index 5df4b562cf9..72f4132f4cb 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfc063153b0a031ec15142d02b494acf778323b3f77c2d4a470936c8f52ac481 -size 7895 +oid sha256:35a2d9b3b600981a71cceed2fcc87772ee1b8efc7d16a759162df3910b8e4a98 +size 7788 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png index 942afa17055..a3cddb712e9 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f61d72a6e32d7f82c198c827d999462dbb84f3154220ff9b96b2339b9c2ee4f7 -size 7832 +oid sha256:078f9a3d9b6fd0cea0ea2ee502029bb27d13fe2f4ef90c9cef6f1d195127f6d2 +size 7809 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png index 993c9cdea97..da54644eb02 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4b1725f22fe74e252757ef5e3011112c86831087eb0762518295af64f085da -size 1604 +oid sha256:16f3bf8a2fcb4f732adf867554ef1d8ad756591fbda8c3a3329111738ba38975 +size 1599 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png index fa997f05c87..9becbabb0ef 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecf877a05b5bab1f506ef7db0369e535434ef4ec712ae6cfc1e34aa62defd492 -size 1567 +oid sha256:63c0407750358648e89440a66c342924086247733ad73204a3bc30c1b5919abc +size 1563 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png index b09bad6fccd..4ad0ce9fb04 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0ab968d64c65fbd57da2751037b3795dd44edfdb4288e1b8164affce136d694 -size 4924 +oid sha256:86829dbd9060453ae29bc9e022d24c00ca33291dcbcf15430101b87e0290d806 +size 4487 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png index b55193ca7a0..cf5dd0b343c 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:715427f1205744485b04ca1e1c2371116d53cc0078ba7b59b39ce492c64499ae -size 4901 +oid sha256:c6fd0287c9e957ccb47457fe8ee06b2a35d31917b0c4618398c4407a96a39e2f +size 4499 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png index 4ce32cefd91..a6c594392ae 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5674c1f5e9cfbf497eec8dccb23abb9bace0742e74804ea752b2fd652bdb75a3 -size 3541 +oid sha256:f41bca316a62f9e6b4a45f9ab52004e33a3d7633cf2fe74316c94fad065dfc42 +size 3409 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png index 65dca4cf368..fd5b914a87e 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:857f0f66eac110ae84befc845a8557fd85d64d3c9b3c38fb3f1390b98e5eea47 -size 3567 +oid sha256:8cc2b39a68f97e066c74f53d7f1589033c8b801fc9f20c979a4eb0c9241298a7 +size 3437 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png index eed451005b5..1a74c453a7e 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bf0c0deb7567010db027a90da11dbe87b2e0e0e1a19b80bde4ae389363451d3 -size 15762 +oid sha256:9f9491ea389218f646808fc7f11aa25bf9cf9973b48fd2a404f44a781ee021d5 +size 13530 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png index 5822d248ad9..25a4a3486d1 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aa1238a25cb27637f4e6505dbbdcb2a4d732a45d6238a46f55205bb9db1227f -size 16001 +oid sha256:838773c4b9f916bf076af9f710fd60f371ec8abeb24e5af8dc363db69079e351 +size 12920 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png index 41af30d168d..25a077bdea6 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d37f98e5c7c4534b0500f0ee559fccea0136b53bafd5ae69cd5c5005fd1bbad -size 9914 +oid sha256:540ecb136760147c8d5d427be8a0445c2020ef5ab366dd1c9e4c0da54372eb5e +size 5974 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png index b59acaf8d25..74628bf1a2b 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70cb69e09936cfa033136995be7ab2d25ddc73fa4043004953d4c7b685adff1c -size 9803 +oid sha256:874c880ad7d52b14cb437fdd108d8fe2781cb41b79aecc3008a60ea226ab0999 +size 5714 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png index 111aefc0557..fd6ea668206 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1e88af6f572f98d7110e3efd9c7980446aeee5dc46bc30ae033130788da222e -size 24999 +oid sha256:db20f45257b1dc1321fb9906812d1965e965dfcc096fae4a5ee093af078a5844 +size 21169 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png index f74bfcb0b0d..6f2e7f7cec2 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89e45041078c82051075e4882b4a8beffae1298d3610270c9d95049c5f05c5b9 -size 25094 +oid sha256:276b9280d1a6d9500926b072aaac912ead29b2dfb6f38332b7057817b1be5a5a +size 20127 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png index 55185e54f2a..053ec0fc1f8 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15e7ffe28aa3ddcaf263df3d829825c6941274a67d454c3416ebdfdc56c7b2fb -size 25463 +oid sha256:47480092c6e46b9af050f664e6156d132252a1d43d3ce241b69b373756485d95 +size 21437 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png index 1457a5c4359..01d13a8f192 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f33c81825ee8ebb528dfdc7dacd1689dd28dedf2250faaf38ca21db82f9bd25 -size 25513 +oid sha256:228b6563cb9f0e2cc1efb981ab823f414b25dc06b530070af80dd4e78ecad7ec +size 20360 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png index a4e786b07a0..f66987a37fe 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29edbbb4350570a92063105121d358bb82557d02e297783ac9aa94ca5857db99 -size 10082 +oid sha256:5cd7d7f6c846a23cf34a9d4a2204495923dd2ca1c4fdc5d8f48150b405847b73 +size 7406 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png index fc33b965edf..58079c76d69 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aafbf7aab60ca0474cd01a36192fa4faeab7eeb3b9e7d432121f1ae977c1441e -size 10033 +oid sha256:9d32a9b6807e8eccf067013ce4cca2a679a2783b9c1d57ee1b171c6e5d1bd89e +size 7151 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png index bd57128f488..358170b8b1b 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:183fd97931b5f9edd0cdf7c6374328f6fa12ac728246c4156b8416f68f9d6960 -size 1954 +oid sha256:21e504fa558a9cdcb756919be7cf39f78b8e3f6e0ef268719d7366dea22f1733 +size 1838 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png index 3c0dbd6ae6b..a597d6ad4c9 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa5471c7656b76e40d6e7123ee8506aeb9a4b42a7157b9bf24a02e117017b6cc -size 1918 +oid sha256:64884765b4979723c9c11c865dd9eed8a45f6d8df2da7364cacbbde90d08254a +size 1737 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png index b5cab20b4ff..4187393e741 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e725fcfce7606554463ee6aa335a3530a6e37935b2a163e78d940b3557e58d6 -size 17933 +oid sha256:f082c2f8ff6772febd71d1d04c932ed26c3760799bf6e0b9ed3191aa0a085e49 +size 15746 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png index 12a8743c86e..63b54b3cae3 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90b08fe37901ea04d0b815f73014168cb2ec0643c4e7f8fa8993783384e884b3 -size 16311 +oid sha256:89588294990c295d22fd45cae3916d4342a1fe26210116d600075cf2452a6a30 +size 14650 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png index b12335959e1..650755c7e48 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4bf1714a905c76cc16aae7c5b7f20c6418018e0820e946831348797c13bb2a4 -size 27743 +oid sha256:cf787d7ed08805462038075743eccbff95518fd891f5399900a202c202e10f80 +size 20326 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png index 2d24452c304..0bc863e25c7 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:153095490477eaa37e4a0648c41e30e7ba8d62dd699a6e3cf3ec04399bbfa91a -size 27090 +oid sha256:4bc43560b510189bae18713e06ed09a9bc2b277ba44c2ef15e1a9295df9f6e0d +size 19276 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png index aeab5d11124..66252c9a152 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7b2e11c4f828e0fbf8ca8ef0c7607965aa1b974280851806a038a45f04b9de9 -size 23556 +oid sha256:f02c53ad60359f1b1ae7b3607f0346c4752cc4c5719fd047de1a314c4838fbc8 +size 17921 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png index 6f13702b87e..55ca04e4eaf 100644 --- a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb5027255ce762a6de6990d20d43a0aae0365ce3e2ca69e374b9b8791a4d758e -size 23201 +oid sha256:967a62b25a0e9dffa793061a4f1a695988c63e984bb7657cac5b730b81507ba8 +size 16991 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - backticks/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - backticks/Dark.png new file mode 100644 index 00000000000..85c40d12f6b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - backticks/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ae8dab0fa799afaadf186c93436bca9ac39c36b99b8f9846ed0d7284494be93 +size 2288 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - backticks/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - backticks/Light.png new file mode 100644 index 00000000000..cfb849214ea --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - backticks/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89f27caedd1e45d22e90243691bcc0153195d121dc2c2bc58be4a1d50ace828f +size 2229 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - simple command/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - simple command/Dark.png new file mode 100644 index 00000000000..539388c741b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - simple command/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:793aa216b116d5b028adf16db5e8780f7b844bfbdd38657f943c6427de3e320b +size 945 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - simple command/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - simple command/Light.png new file mode 100644 index 00000000000..e2b960147c1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - simple command/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6500291e4787ee83dbe70cf44c1b6cb0c5c96f4311cf865cba26ce86a92581b4 +size 938 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - special chars/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - special chars/Dark.png new file mode 100644 index 00000000000..3b72ed4acaa --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - special chars/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e88d70bf076040be9311a96a39717699e59e2bd86a9b2ca7e32b2424bfbf273 +size 2442 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - special chars/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - special chars/Light.png new file mode 100644 index 00000000000..0abd61d3e4e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran - special chars/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f267b5737a1cbfeddd000065fa66735c058cdaa090b60445f583db282eb4787d +size 2359 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - backticks/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - backticks/Dark.png new file mode 100644 index 00000000000..a37c755179f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - backticks/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae563a7c9f939d3fefa4a30e91e1a514ae0de7b881813827110b62def6bd05ef +size 3061 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - backticks/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - backticks/Light.png new file mode 100644 index 00000000000..7d27474905c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - backticks/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d87074216734a0e6a15683060271590bb6ec594dca7ca84c6ae3b0180df2a66 +size 2988 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - powershell backticks/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - powershell backticks/Dark.png new file mode 100644 index 00000000000..77d8e86be22 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - powershell backticks/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dce5773e95961f082c034816bc63fbf79b8cedecaec9750840fe483d4855f32f +size 4476 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - powershell backticks/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - powershell backticks/Light.png new file mode 100644 index 00000000000..04745d73624 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - powershell backticks/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a05a111b62dbdd2f355f31f08a2c5c6d3731a1ce9607d95e66c42e092a1f099 +size 4354 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - simple command/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - simple command/Dark.png new file mode 100644 index 00000000000..9eb5f8b26b1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - simple command/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eda8e2a9eeb0b53fee694039861e868d40b711c4f26b95748605f0a14a8a7811 +size 1759 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - simple command/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - simple command/Light.png new file mode 100644 index 00000000000..8abf08a2728 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - simple command/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d71509185136eced7961758fde339f267f38966a4e35939c8fa11fde64e641e2 +size 1757 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - special chars/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - special chars/Dark.png new file mode 100644 index 00000000000..975089fc9b6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - special chars/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a089a8a290b9ccb52d42cdd987ca8ccccdbb0a47394e82f0d6e40b447663a7 +size 3156 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - special chars/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - special chars/Light.png new file mode 100644 index 00000000000..71f232f4d62 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Ran sandbox - special chars/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:927527224e48e702d81b44beedc1a2395c638dbaaf952d1a1c926d576d2bf15f +size 3152 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running - simple command/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running - simple command/Dark.png new file mode 100644 index 00000000000..263fa55733e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running - simple command/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3647c8c57337952dd845f9151b3ddfbb074857e4036d76dd6c7d7beda734c676 +size 1170 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running - simple command/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running - simple command/Light.png new file mode 100644 index 00000000000..04b6944c1a2 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running - simple command/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0008e3885e2e6bf569757ace983c635feb9da86d470f2455e4a6884d7d22b6e +size 1139 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running sandbox - simple command/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running sandbox - simple command/Dark.png new file mode 100644 index 00000000000..760ab24627d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running sandbox - simple command/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1f340aaeaa112ab63fa6a6048fc76618097114ffbf9ada8a4e8f7736df5c5dd +size 1932 diff --git a/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running sandbox - simple command/Light.png b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running sandbox - simple command/Light.png new file mode 100644 index 00000000000..7f869c9bceb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/terminalCollapsible/chatTerminalCollapsible/Running sandbox - simple command/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3a5c44027dd8659083344aa7f15acd02d2e59d53913b4ca58e5345204470f0d +size 1893 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png index 524b085f8b0..a722a0daf26 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab8379f105e879912a52e2d25c88e26870df79bd220a6789a3f61e010d2f6788 -size 14944 +oid sha256:806cccdfab0052158ba943d3139bf39807d7b6a855d387148a12f07f88a65f1f +size 14217 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png index cd0c2d36a62..d3bcd4a1eef 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e641aa3e70e91cfbaf2db48fd1a095850a422b55b0acbeef4b2fdd74d07674e -size 14446 +oid sha256:53d5c9626190a0dbd4eadacd494e3485ea748034feb7b5493ed51987725d4a11 +size 13663 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png index 7527ac7cf76..c896f0f6d3e 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94aa1fc6a75d1506459f1a0bfe4d29723350a59e3d4c5002a4916461245bcd6b -size 6517 +oid sha256:95b3400efd3ad39f145f37885869bb2a606a3ebab204a78ceebc3dd9fb846644 +size 5871 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png index 0de7a3d04ab..086e1c16529 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e4676c9b49dec849b275057f4e8a06a3acbb7fb54304d9fb9509ff7277459d -size 6110 +oid sha256:9b1da4121639870beec498d27d3dd6ac3f315670f25af7220b4f14bc2bf2465c +size 5551 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png index 339aeb80c75..6f988de10b1 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e3d8467ca11776454f33e2ec15a411a36c1e4d8777a6cf12f991f70bb33c0f9 -size 38205 +oid sha256:5e2ea376b2b00f3ef7b6a223b97209bcddb9d75503ae1b1c2725ea1aa0d494d6 +size 39168 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png index 25a9038c8be..039a6f4c77c 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e216ab674575eb1368649d026b85470d7b2c8b9ba1ebc06d3d7acefa3f246214 -size 36998 +oid sha256:ac71623268edd1c88ba0b968a2782ea5a8c7536378b834cc8bc223b21b59a2d7 +size 38516 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png index 48dbd2f491c..cd55ef1b5bb 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9df123fb04b4c6ecd4337bb19af8626b095c3fd5f149c0bcb2d46d216fa392ad -size 33087 +oid sha256:755c46c3ecbfb04fc0d119b555e6af8c204c6be3e20271e369bbe68382ea739a +size 34071 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png index 2c81c27c782..73cf3dc9ea6 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:531f9d16ecde8fc7b868eafc0541f04dfa476065095119ef2b02be2ee85a4788 -size 32732 +oid sha256:c983a5f7f517c193193c4b2858c4eea26fe9721e6272a03b3033d1b82fa76ad8 +size 33922 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png index 027b13b8042..a99c832f975 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa6ec0238ac1ac4974548b73e205b758e910d0604d1c28e0f26231e1df8129f2 -size 31006 +oid sha256:1d3919a74773f2b31a5ab2cb1e3284f4421a917a8355eea52cf56b68b0f5a4d1 +size 31928 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png index 82991aea52e..bbf9c90880a 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:416cf9214bac2a0e275f9cf83c41e656e94de14ee870886f55caae230eb8871d -size 30907 +oid sha256:6a5190f9b9aaa5efa8591f957d8a7be12f230052c444406e319cff949b275afd +size 31849 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png index d55a1ec7dfc..3f620d66bc2 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e8973161acbdd94816922272fcb0406263e58f624d86c0d7980c329a65a1b24 -size 11278 +oid sha256:ba96e5a35d38cacbbf7b34ef1707079d134d6e0d7b4347e0b6243c320005844a +size 11295 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png index 5a87432a3d4..3e9d7899424 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6d3166596fd41609c4a622d7f7267fd6f3f9605d893efc34aac65ef7881ad71 -size 11203 +oid sha256:684f0f9eebb9410dcebd7385bc9f8c7cb8b30793beda3234931b6f56f46a88cb +size 11230 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png index 9da6404a655..6912c85f337 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f39335e6fa03078f804f848b634329c9391e4d8fe099d0fa1e0d4808ba4eb3a7 -size 11117 +oid sha256:8f6c831f0f3a01d94ef922827522db7e14b6c98affb37e0dcf277344de3b3a20 +size 11114 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png index 42acebe80c0..173f683bc87 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c70bb66d053f20dccf9075399a801dceaa92d5830642e8f2afc9f89c81d9315c -size 11005 +oid sha256:84ceffc2825212d33b4f647dfc6aea6b7bdcd91651b169fc1747d9a7168d0853 +size 11018 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png index 4f1806f3f3e..47ef7429fcb 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a54d85c0fcf622977f412e8fc2953c973f1da427c91d319d0708b6e758969f3f -size 10280 +oid sha256:155a4b0bcfcb919fd56ba0953e0f91299a7899d92b919d5407e7489c03e32a7c +size 10273 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png index ad3fa141565..2986af76404 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d494bbb9ebfbf2156c5d7a21f39c52e14313a5218af083cd91f2dbc6199f62da -size 10195 +oid sha256:5cc8592c7878181b62449b0e2f908e7f625439edbe235495a15baa0fc8494f11 +size 10208 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png index 1dbc63aea44..776313e9769 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e4226c71af8ae21a5f0c13ba521b50518afb4a39831e832d725ebe0289344ff -size 10266 +oid sha256:8714bd5675ec655e37f3599ae9797b4346b1f1f331183f15a3eba3acf14891cd +size 10508 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png index 224da35d3dd..c30fcba59ea 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:518ecbddf013bbc749b62a8a74bad39d7683332ebe0d9f2a4b7b13ad54fcabab -size 10104 +oid sha256:695f962e05129f1acf7f49f0599d7edfede15ad546ffb928b1da6818c59d5b46 +size 10634 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png index 1dbc63aea44..776313e9769 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e4226c71af8ae21a5f0c13ba521b50518afb4a39831e832d725ebe0289344ff -size 10266 +oid sha256:8714bd5675ec655e37f3599ae9797b4346b1f1f331183f15a3eba3acf14891cd +size 10508 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png index 224da35d3dd..c30fcba59ea 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:518ecbddf013bbc749b62a8a74bad39d7683332ebe0d9f2a4b7b13ad54fcabab -size 10104 +oid sha256:695f962e05129f1acf7f49f0599d7edfede15ad546ffb928b1da6818c59d5b46 +size 10634 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png index 37bbb987300..ab3b6ba6a39 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c3e7be1da3065615074aa5678fd7ca38dd7b3906573fcd555ee9e249d9d645f -size 15252 +oid sha256:dc5640f9a8dd06f0636af35aec67440d4c69d8ef4dc5d5c4dd160f7cbf1fcba2 +size 15197 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png index ea8fe1b7068..8e2c5e279d4 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:054c0dcab2a00526f7fe63440a7e782618f03b9396f40e6a31b8e28e7ea59145 -size 14846 +oid sha256:9de4e72712e9c670d9544025018d9922be7cd5c7c7b6b9e288dc948b9c4f93ba +size 14778 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png index 19c633f2edd..ccd9391de5a 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3923f85be8bcdbfcdecfd80b07abdbd71763edc379de4e6a5e946f20f160db5c -size 55650 +oid sha256:c575354210f36f418e4316d8da6e877a087212d4c0124055f720391f177c6ede +size 55167 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png index 823489ce7fc..5abbd81b1fb 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e81911488f22e9517b9e2f8bd7b8bc7a14d7fff1b2d012c2368b840d4bcb49cf -size 55028 +oid sha256:f1bbed581cc5ad6622fe4ae67a739ff0ab549b23c78565e637aadab74030888a +size 54554 diff --git a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png index 26eeb8a8751..c8cc1a132e9 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1191cc4ad1b12b31b28e94fa6305b2c91a3ea399580f04cd93b5387c8b58b89d -size 20408 +oid sha256:4d862c3e93ec28926822c5edd98094413937d25a806e119fe855b8c969969fd7 +size 20387 diff --git a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png index cd7a92aa849..f95d450b9bd 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e18bb8e20cf85b06825dea19140a88a9fd0d9ec844db939d8550956e9049a2f2 -size 19982 +oid sha256:6646a61f0998615948bdc538f5b216fdfc06640bc47d60e9f641ec2462e37e93 +size 19922 diff --git a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png index 2b11099af61..f476163487d 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc8f9f9295ae0e9b7f2c9215e788c0a0516d7c667f30f61fbadcfafdb13bb2e6 -size 21470 +oid sha256:ef331243e3846acebf347c2c99ca1ec5f7179c1085cef3f6262d8d1ef67cd23d +size 21438 diff --git a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png index 909659b1b5f..7de20ff04a5 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d8a254da836fa81f53516bbc992394aeb91f7297cba4d13a8c5485ab3d46d7e -size 20862 +oid sha256:2ef30dba70cb58120a1f6e3b99eb052bab3a64bc3e80927fae2e46677e8640ca +size 20684 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png index cd464aa178c..2b656165679 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b0b1f65ed8a0963da2b4f7f64b3d06683895a57d8101856f711742954cedc23 -size 23358 +oid sha256:b5894955a9077a4fc8631f7763b51d55b6ed414108b22ae0e2c99aaaabf75521 +size 23441 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png index b293ccc538f..f927d07964a 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f348cbe0e5d401778f4f532b6254d981f9ac98220b290d8d5401dcd4526410 -size 22475 +oid sha256:3276f547029bcdcdc2a80c7f4b5b62ef4ca42ca2a7b3a11baa2d0369b8a34d20 +size 22785 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png index 8d9e892d9e3..444955cc243 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0222974a073f5b12431b1ebc4f498ce8a795b860016520e103a35d6a68401d98 -size 13689 +oid sha256:8c7bf8217becab4b850dc86c1793f2647491221f8309d26b5e2e7b08e5e18d05 +size 13947 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png index 5e93e621faf..5ce407d85b1 100644 --- a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:998dde573af3df84cb836248c71f2bc141eb6845a7c407852f1a02969b14dec5 -size 13538 +oid sha256:25072863d5f87822359f4ee4cd3d29a1b23a8a1c03882b7fb4051e31ec4a12a2 +size 13979 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png index f6117771773..63489bb2a4f 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a -size 3899 +oid sha256:01d1475af7291413c9d2957c58fc5a9991a29f95c4281bf5da5506059baa4d91 +size 4510 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png index f8d4a437c6f..8ead2afd156 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e -size 3830 +oid sha256:aa736ba9d464782b1eeeda889a98b6651a9038d57de15d8856952b105f9950c4 +size 4417 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png index 586892704bb..4ea83494e4a 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3da890451d346196f63be699e367ad2eccf46091167ac005d4eb5adb8169d12d -size 3778 +oid sha256:5a825e7835cad227b0ec6cbeae7ee867b10c4c51df7c8ebf99665ff40831f805 +size 4343 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png index d5f5baa7a48..1c09ee84b88 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc0b866a49984f2e17b2da44d15fa4525a21519010e4df5a0651ffcc88b594f0 -size 3674 +oid sha256:baa5aca747de294f2afd09d0bb7072172e0c6d8e9c22298046ff94f487c5f52f +size 4018 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png index a628d1aae9c..e223127b8bf 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60be48fef4a63200229dda97c1f8664e9410d76be3a113926355867c8f58c3fa -size 3815 +oid sha256:06860ace3520e44b62f18aef1fdccd897f5079f81206a1e0f59271d28b19495b +size 4465 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png index 0f6db97e2ab..9e2a19441c9 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44d8727fcac81f4669821e279f370cde522e2be40758d2d2975a9331466e6d49 -size 3716 +oid sha256:134c92f9a0e7c6f7d87d9afe81576c6d06c581b8c1cf211047fd541682892af2 +size 4291 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png index f6117771773..63489bb2a4f 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a -size 3899 +oid sha256:01d1475af7291413c9d2957c58fc5a9991a29f95c4281bf5da5506059baa4d91 +size 4510 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png index f8d4a437c6f..8ead2afd156 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e -size 3830 +oid sha256:aa736ba9d464782b1eeeda889a98b6651a9038d57de15d8856952b105f9950c4 +size 4417 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png index 9c33813aae3..b9e1d379c1b 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c282cd99bdd694629caf8bd310013e3aec0f5b25dd5376ac7c1bb897a1b4388 -size 1593 +oid sha256:1634c9ee9bec76056e5a85932eefed9f89447d38dbae2dcdc36a0a929b696d6f +size 1408 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png index 753352cf64f..2e134805689 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1380daf9f875ed2502b0669af7844bae000168bcba00d89ea5d6634f590637c1 -size 1606 +oid sha256:0bad6a456b7520b7eee2a23d7816b69fd66c0b868d45a0e3782f9ff4331ab691 +size 1443 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png index f6117771773..63489bb2a4f 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a -size 3899 +oid sha256:01d1475af7291413c9d2957c58fc5a9991a29f95c4281bf5da5506059baa4d91 +size 4510 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png index f8d4a437c6f..8ead2afd156 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e -size 3830 +oid sha256:aa736ba9d464782b1eeeda889a98b6651a9038d57de15d8856952b105f9950c4 +size 4417 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png index aa6921a198c..abad162c657 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:263cc0cd0dd5fe9eb7839cd0ff5c1698c26b76fd162c6f20cd9bf048094cb308 -size 4299 +oid sha256:427bcfd7e72eb43ba4d38a324c3da70050aaba9ed7b0f79ad86f5467f8fd5b14 +size 4765 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png index 7a19687be82..3c7467dd53d 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:651452ec039ff26c1fce42ccd541d0ce70f39a993457939be3bcd3f1d67ec4cb -size 4202 +oid sha256:6ce038b894141fa171eac65aa4f07680e1650d395f26a66761ea694bc2bb862c +size 4528 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png index 586892704bb..4ea83494e4a 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3da890451d346196f63be699e367ad2eccf46091167ac005d4eb5adb8169d12d -size 3778 +oid sha256:5a825e7835cad227b0ec6cbeae7ee867b10c4c51df7c8ebf99665ff40831f805 +size 4343 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png index d5f5baa7a48..1c09ee84b88 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc0b866a49984f2e17b2da44d15fa4525a21519010e4df5a0651ffcc88b594f0 -size 3674 +oid sha256:baa5aca747de294f2afd09d0bb7072172e0c6d8e9c22298046ff94f487c5f52f +size 4018 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png index 3dea19001f2..b0da23e3478 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c6eba3c086a1b7c69b8f1bff63ccd43bbe6bad82a049e8b32230e4ce7ffde07 -size 1367 +oid sha256:2aef300924a82a7d8c3a3d20966735bcb3bb5f2c5c8ee64a25437bf40ef27e1e +size 1468 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png index 3e8db51c450..133e883f14f 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b782263b3ffed17f79b95444cc6f479e9408ed252f5d1c227b4c7f741858b82 -size 1240 +oid sha256:6837a6e2e9accc5c116a816baea4189a8a23e29b6dbb568126e6e01a0394b259 +size 1336 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png index cca6f012d85..55b01d53d82 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa8e4d7c2bffaa4b3fd149ce1316b78ba9ac40ce5878a8867dd6422342c5a18f -size 3977 +oid sha256:699342ab20e24415ebe5c1fd08377c9cb5414efd21d9aca804bc60e04ae62b4a +size 4581 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png index c0152452dbf..5813b4e6204 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13659a984dabca598789efd2b5a3f0753c5d8d8ba705b018c046ee1ebf9d861b -size 3855 +oid sha256:fd77cb2bccbfa5fdbb2fcdbcc8aa250da2acaccb40636f8336432c3a8bcbab9f +size 4393 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/FirstOfThree/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/FirstOfThree/Dark.png new file mode 100644 index 00000000000..de96173bf77 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/FirstOfThree/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a5a33f280f362201bd0b47d0af0201db80578de3467836e2d833138722a2662 +size 2943 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/FirstOfThree/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/FirstOfThree/Light.png new file mode 100644 index 00000000000..7d731f19048 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/FirstOfThree/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:478534318aa35e1d76fb1ace9de48e1812a477642215bac6776fd8ba73d32085 +size 3240 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/LastOfThree/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/LastOfThree/Dark.png new file mode 100644 index 00000000000..5abb7698faa --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/LastOfThree/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7a6bd14c478e40d6b525b5e707a881b2cf196d1fac3e6d35e2f8820279b9291 +size 3021 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/LastOfThree/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/LastOfThree/Light.png new file mode 100644 index 00000000000..317d8e2daa4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/LastOfThree/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a66b8d7c8034a952c55a9d29589badfc7523d0e6834b32e394574de985cb1e05 +size 3317 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MiddleOfThree/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MiddleOfThree/Dark.png new file mode 100644 index 00000000000..2928c7703c4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MiddleOfThree/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:773b90978844edda2e9c46f410a05dbc33dc4df3352132a2e69e8ea98a158283 +size 3003 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MiddleOfThree/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MiddleOfThree/Light.png new file mode 100644 index 00000000000..f73c2878e0d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MiddleOfThree/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a69f1e323e95095ae59c534ecf00cd05c781aa48d9489d978364c5782347bba3 +size 3301 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MixedFourComments/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MixedFourComments/Dark.png new file mode 100644 index 00000000000..705e048acfe --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MixedFourComments/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0eb476b276b44aba0b229fae1e46c7fcd711dfdc394e38e2afb12cf4907f6d5 +size 2966 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MixedFourComments/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MixedFourComments/Light.png new file mode 100644 index 00000000000..66f5a2083ff --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/MixedFourComments/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337c0c5757f374533c9ce1d68baaf8dbaec9a87d52693fd0a25b21e65ca936c4 +size 3246 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ReviewOnlyTwoComments/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ReviewOnlyTwoComments/Dark.png new file mode 100644 index 00000000000..d2ddf405142 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ReviewOnlyTwoComments/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad7ade6d0457628337fd15f7bf2af269b3e7cf847381a0c731373d43e7f1638a +size 1678 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ReviewOnlyTwoComments/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ReviewOnlyTwoComments/Light.png new file mode 100644 index 00000000000..4d8f1169e4d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ReviewOnlyTwoComments/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:995c80c3e0dd4d4b39b1bf3a5ff39f0241808b051c757b7f701398e60ee68454 +size 1927 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/SingleFeedback/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/SingleFeedback/Dark.png new file mode 100644 index 00000000000..dd639503f61 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/SingleFeedback/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abeabcef4f94e7f813e87473265b897f6670a6e75b6bb8522373af0d75ea5c80 +size 2841 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/SingleFeedback/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/SingleFeedback/Light.png new file mode 100644 index 00000000000..ce86be68b94 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/SingleFeedback/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79ad5a5603bacb47d19fafbb5faaaa0a8d8dad8dd8dfdbd344d4757b08eb311b +size 3131 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ZeroOfZero/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ZeroOfZero/Dark.png new file mode 100644 index 00000000000..f6b8bdf579a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ZeroOfZero/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03ee9b3c7bb32a4e48fd7668e5bd6363261814553c78e278bacff11919327f82 +size 1731 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ZeroOfZero/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ZeroOfZero/Light.png new file mode 100644 index 00000000000..80215b1c880 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorOverlayWidget/ZeroOfZero/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92d9237d2185896c69ba039004fab8b13da3da9b96de14c0094ad4bed75da0ce +size 1997 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedMultiComment/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedMultiComment/Dark.png new file mode 100644 index 00000000000..b6b68198d16 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedMultiComment/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f31ed8603c49544a639bf347e4fd5487e759b8c0633c6c5c0132a0bd01d8d3b +size 21728 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedMultiComment/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedMultiComment/Light.png new file mode 100644 index 00000000000..34bc9d1d7a6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedMultiComment/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:938e0d7793ffc294e9d9cb20a66ea68c7d4e6d80d0cdfd3efabc0f86706ffe8d +size 21747 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedSingleComment/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedSingleComment/Dark.png new file mode 100644 index 00000000000..04a0ee89745 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedSingleComment/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:103cb85f051ef1d6b78bcfc6ae52c2e47d0b02fcc26ec79e043cf659936f3079 +size 21499 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedSingleComment/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedSingleComment/Light.png new file mode 100644 index 00000000000..d91fc2ba533 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/CollapsedSingleComment/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e3f743ca0db76ed89bf7bae963937a1fe7f80cbe1cfee270fdb9278479ebbc +size 21511 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedAllSourcesMixed/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedAllSourcesMixed/Dark.png new file mode 100644 index 00000000000..8b028accee5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedAllSourcesMixed/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d68bb0ed9daae712035f7b409755c98c90ae350209c79a5d158c4794c7fb17ee +size 39273 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedAllSourcesMixed/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedAllSourcesMixed/Light.png new file mode 100644 index 00000000000..90fdcb2a79b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedAllSourcesMixed/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aef346aa29334aa0a913428cf9ae9718b0c48e947699196d35af1fe423ce101a +size 39484 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedFeedback/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedFeedback/Dark.png new file mode 100644 index 00000000000..37897eab608 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedFeedback/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed79dc250ae3a21919868b75e87ddcb5e3541fc8175effb30b75ee1f43f2c747 +size 32493 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedFeedback/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedFeedback/Light.png new file mode 100644 index 00000000000..5b9989573e7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedFeedback/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e81e7cec7d10df749e911c2a820217ca2f9b4e2ad74723a0c34b406a428c845 +size 32556 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedPRReview/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedPRReview/Dark.png new file mode 100644 index 00000000000..d544804d039 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedPRReview/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b840c9615892bba45dfad426b8d89fd3bc846b77828202c8268b0afbedc7d99 +size 40832 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedPRReview/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedPRReview/Light.png new file mode 100644 index 00000000000..41140a8fd7f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedPRReview/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90c649d3652033051bae32af21426ab56d396dc6f95c416d68001de389a968d7 +size 40737 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedReviewComment/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedReviewComment/Dark.png new file mode 100644 index 00000000000..8aa66f35343 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedReviewComment/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d40696547bef16609fbff1b36fdf88cd242f45b4529de5beb6c235168c3a2f4 +size 32304 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedReviewComment/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedReviewComment/Light.png new file mode 100644 index 00000000000..9f9555ca732 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedFocusedReviewComment/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f067c3710e82fb33c6884331944ecc054c2db96d05c0f09b27a3433fbb58e918 +size 32420 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMixedComments/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMixedComments/Dark.png new file mode 100644 index 00000000000..fa88d5199a3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMixedComments/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:156060baa8c3ceeb34276bd66364db534ecde67ef874839735bc58fe03412545 +size 31499 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMixedComments/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMixedComments/Light.png new file mode 100644 index 00000000000..0440191de71 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMixedComments/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1af58120b25a4356142ef9b658e1937ecfa379bc18b51a82abf707209f9c555f +size 31651 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMultiComment/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMultiComment/Dark.png new file mode 100644 index 00000000000..fcd00f3e1dd --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMultiComment/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:754354efe48cd19db11a23859a7d168b7722ffe7c94a2d1d6a83e860dd1d8bc1 +size 31690 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMultiComment/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMultiComment/Light.png new file mode 100644 index 00000000000..7dd84faba71 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedMultiComment/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47ccacf1fe7f746aedd45ce7d81135ff7d58ba7924afea1dc1e4e44b36765e36 +size 31961 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedPRReviewOnly/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedPRReviewOnly/Dark.png new file mode 100644 index 00000000000..539c0edab2f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedPRReviewOnly/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8c72535a1d1d8f19519e8a865ffb6bc2a2b3488125ccadc5d0c2958c7c0f1d3 +size 33251 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedPRReviewOnly/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedPRReviewOnly/Light.png new file mode 100644 index 00000000000..78d20258926 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedPRReviewOnly/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b91f439c5fc5200cab8bf877ad06573cf923befffd8a9451a87a1f2c10c1cf51 +size 33529 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewOnly/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewOnly/Dark.png new file mode 100644 index 00000000000..57c3d4537fa --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewOnly/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf0f8e48d5166f74620046e71d020e565a283e27a6f4ada23007dccd0177c59f +size 30241 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewOnly/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewOnly/Light.png new file mode 100644 index 00000000000..aa7b5af0cb0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewOnly/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94d176b14582786b122c68a1d6e4760b0a1849f93a1301cfd146b66c441fca23 +size 30543 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewSuggestion/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewSuggestion/Dark.png new file mode 100644 index 00000000000..9ff5cb97d6c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewSuggestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8329c6b260e5aed1be14f8b6211b0eb575ac932ac41ee6b1f8ed9309ccc1f203 +size 34533 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewSuggestion/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewSuggestion/Light.png new file mode 100644 index 00000000000..6dad7fcfeb4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedReviewSuggestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5694640859e028e0c80d2aaff4c46e05ca4e7a6b65f0ef8da2c32954ca938312 +size 34621 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedSingleComment/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedSingleComment/Dark.png new file mode 100644 index 00000000000..51753f0276c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedSingleComment/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d116e7929903a5ba516208842d47cef6affbec42c1490117a763adb2e8c0a872 +size 24215 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedSingleComment/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedSingleComment/Light.png new file mode 100644 index 00000000000..a4c463f6da4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/ExpandedSingleComment/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fbcce1ce76886c46077b6a8de3a8a2f2cde7956bc4ae872bfbad6d72ca957e5 +size 24326 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/HiddenWidget/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/HiddenWidget/Dark.png new file mode 100644 index 00000000000..aee5a08426b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/HiddenWidget/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:513832a1332a89716c33be68569771f5e416a230c02935fa6df44b3f75d7edcc +size 20061 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/HiddenWidget/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/HiddenWidget/Light.png new file mode 100644 index 00000000000..2b785522258 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/agentFeedback/agentFeedbackEditorWidget/HiddenWidget/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00a56c45837bf2559b1e029a362e87d16faefff17c55a3b332248b8e40d5852a +size 19760 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png index dc686609d7f..f397dfdec23 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6332afcc844f37a800f11ba7b49f8d5ef61f160f9ea285a34fcf08df462c8de -size 1749 +oid sha256:168b95b9f8ac66d3eb19912c5c79d97688fa4ad56656b84722bd6cf7cd7eec05 +size 1794 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png index dbca2be966b..63f849c6e3b 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c567d79fce21467c90bdf833d5d387c1a7e91070fa908d7fdb53595a0adfcb1 -size 1686 +oid sha256:13cd78e9f0f1197cb37ef285e847b7b501a1c2146841a61e39e6bdb01f636343 +size 1756 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png index 3c121b4057c..6d9e4cbe59a 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b217c46a4e2cfd993def8baf8e2fb2808a825d0b38fad553ec82a65ee1148e72 -size 1936 +oid sha256:20a6ea07c40832684cd3e5e9ca086fc9603a836054cec26f18fb68f99d93bf89 +size 2004 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png index 08073d4119a..62077ea01aa 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86dd22c02f0f4d1f4a46ce55f8546dfe70032a199ffa10683a9daa47cecfad59 -size 1889 +oid sha256:cdf33d525b651e9f85dabad808fe35b25584d9e53c9d93967abfb63dae26e02e +size 1962 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png index 162ecbb0952..3adc18ea52d 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bdc6ca3ffa8ea88ed6184379239b81d0ecd90cfc56dc3cecd84be616d3c22c0 -size 8122 +oid sha256:da681012d7da925e334696e43541dad364db36b4607d810a35c823d331358e76 +size 8976 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png index 03dd01d13f4..8a248678bf7 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5ff42ad20ea3c0fa7e43d1db6fac737890f149942caff51e3e0e1d041a3c015 -size 7865 +oid sha256:ca2aaf92aaa0c8ab0c1068313ba6bb0922a4ebcfad1ae44828c65aea8ce659a9 +size 8618 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png index 2bebcf187a9..1b65bcbfdb4 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2a797c09dcd81f2128eb937eb53f1f7a28131978a7f654dc95092b39172d098 -size 9388 +oid sha256:5e9fa01877aff43ace14d6c03f057cab0fac342d07b3017e8de59fea426ab6b4 +size 10157 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png index 556520e8b8b..0216382acc0 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a26cc493b80a889baa2495307abd147405ef256c0713a84fcae26cb5918b39e4 -size 9131 +oid sha256:bc66d87503f03fa9a1b6763212fc6a8841d09cb623b72ad138d49c72144811c9 +size 9807 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png index 621b8b35147..e439e70fd98 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b835e8c51e723ecb0a11cb167b81a76b4a4938895a1b05291ac5fd609446923 -size 8341 +oid sha256:251b0f2350912e67651fca69af92089dabfd366916b50d87239203d6e0d9d984 +size 9174 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png index b5d3ff1d64e..8b8010b0966 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae32840ac5637faa899f21c94acdc7e2f5c065d705e2a3b103a23a381849dfde -size 8065 +oid sha256:fc9cf8244298eaea71c646e3a8c09e2bf72ccbb045d85ee65c2eb1a4a7a54c9c +size 8826 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png index d06f5a35857..fa3764a9150 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:125f1d570fdad186e94fc580d44e8560b6ecc3de58e3adc86d23045bdda21a8d -size 7784 +oid sha256:913b6334d09b4f99fb00e1c5cccb47b74026742119291b4bdf39dca0c3cb099d +size 8211 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png index 9ce0500c3ba..c4b93d3705c 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afb19ee8b136f65d80f8aa7de11cf983926458999b153568d6fe6303ab5c0236 -size 7692 +oid sha256:372891fecb0ed1ea2fcb60eaadd981f8681b09a6dcf4aad6bab743c28d0cb82e +size 7707 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png index 01d70e70f0a..a4a3caa86d9 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28bdc58d5f8ec2b28df3b6762fddb4dcc9477fd3dfe98af5a7d4c311ebf4e3c1 -size 7218 +oid sha256:97d9a5c1eb6d9b1b37f5eb5f2b30bcbba39101d1ad6cbb1b70acc0269e3e0687 +size 7720 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png index 4ffae5781dc..b2e00200acc 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:977c95e98e10ec21093424a2d4038131b9d95c69d27ef225b501529c4308e497 -size 7076 +oid sha256:cac2963771db45a00f0162ea36d32bf42ea9c887e7f08ae5cf50c65384fa11cc +size 7181 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png index 571d58ad4c3..ec767a77ce1 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:595c42b464f87696ead04be8a367c1ba36dd75ec5115fa00ad7ee2ddc80c7644 -size 6875 +oid sha256:8e53afcae102ac647a2c8fd20de8f68b636d13dfdb9ef9340f1ded2c855e1c68 +size 7141 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png index 3c0f774ad10..7e991c5d862 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d393b6215ebc0f299a2a7e21076835c6b3ee9035b94917ddc5a7d298222c1b3 -size 6706 +oid sha256:b94f6af1d6fb348229cc6fcfec55c7218827eb6c62bb7fa0471cbbdd577e0ba9 +size 6659 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png index acbc58e86db..4bba36e4a16 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abdf653a0b09dd50655217b4c05e55cbb7a1f53ab8768809125d4eff48cace22 -size 7523 +oid sha256:7db6b80ce8d24c206ee1de9fe5c8a4c0ee210a9cbae3dedaef115c4a1a7656d4 +size 7933 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png index 7755b21fe51..2d34ffbd691 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:269f58f05dd9f4cea1b4f09a92f6f58b2cc55345e9252b05297b7328489dd0bf -size 7303 +oid sha256:c68d46fd11691453d3a366a63912303eb8f52612751d6f4a208d5fbad724283f +size 7484 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png index a574cc4e101..345dde1c6bb 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:631998b418a3af1a791470fb4b557e2b3fcd35d66d50e6c98e5c70d578b016cb -size 7367 +oid sha256:1982070f60aa37656afe537df68caa7c995fa56189977fbb79d81ad691d49e2c +size 7661 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png index 9f89acdebc1..38af8f9fb1a 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f96bd7a669274b547229bce41ffc400983dc4656d97ecf820fc94d65df1a418 -size 7078 +oid sha256:2a4b9a8a19eb9216366b3f5867aa97eb2218889d4e12eb73ab8272791cbae013 +size 7167 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png index 1b1a0fe693f..bd5e8e87e2e 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e15ec5f380f9bc357fc6ddbd02b954930815bbdabd0d86c52b9e05550ad3a21c -size 7101 +oid sha256:dea8a075eaf748a1e3b00e5f489d67ac610a87f938be2aa0563d2e57c4a6c762 +size 7151 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png index 0fd86f37325..56b4f8c9141 100644 --- a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f50b8ee6944db9878190b9ec57fa87c4032d6697cb3827db3fa651f25aa38183 -size 6980 +oid sha256:a211dd61eb632e8a55815c392206174150cb5cc37561d95147376fbb75d5c58d +size 6717 diff --git a/test/componentFixtures/component-explorer-config.schema.json b/test/componentFixtures/component-explorer-config.schema.json deleted file mode 100644 index 3d129dc3b7e..00000000000 --- a/test/componentFixtures/component-explorer-config.schema.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "screenshotDir": { - "description": "Directory for storing screenshots (default: .screenshots)", - "type": "string" - }, - "sessions": { - "minItems": 1, - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique session name" - }, - "source": { - "anyOf": [ - { - "type": "string", - "const": "current" - }, - { - "type": "object", - "properties": { - "worktree": { - "type": "object", - "properties": { - "ref": { - "type": "string", - "description": "Git ref (branch, tag, or commit) to check out" - }, - "name": { - "description": "Directory name for the worktree (default: component-explorer-baseline)", - "type": "string" - }, - "install": { - "anyOf": [ - { - "type": "string", - "const": "auto" - }, - { - "type": "string", - "const": "npm" - }, - { - "type": "string", - "const": "pnpm" - }, - { - "type": "string", - "const": "yarn" - }, - { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "Custom install command to run in the worktree" - } - }, - "required": [ - "command" - ], - "additionalProperties": false - }, - { - "type": "boolean", - "const": false - } - ], - "description": "Dependency install strategy for the worktree" - } - }, - "required": [ - "ref" - ], - "additionalProperties": false, - "description": "Git worktree configuration for a baseline session" - } - }, - "required": [ - "worktree" - ], - "additionalProperties": false - } - ], - "description": "Session source: \"current\" for the working tree, or a worktree config for a baseline" - }, - "viteConfig": { - "description": "Path to vite config file, relative to this config (overrides top-level viteConfig)", - "type": "string" - } - }, - "required": [ - "name" - ], - "additionalProperties": false, - "description": "A component explorer session" - }, - "description": "List of explorer sessions" - }, - "compare": { - "type": "object", - "properties": { - "baseline": { - "type": "string", - "description": "Session name to use as the baseline for comparisons" - }, - "current": { - "type": "string", - "description": "Session name to use as the current version for comparisons" - } - }, - "required": [ - "baseline", - "current" - ], - "additionalProperties": false, - "description": "Screenshot comparison configuration" - }, - "viteConfig": { - "description": "Default vite config file path, relative to this config (default: vite.config.ts)", - "type": "string" - }, - "vite": { - "type": "object", - "properties": { - "hmr": { - "type": "object", - "properties": { - "allowedPaths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Glob patterns for files that keep HMR; everything else triggers a full reload" - } - }, - "required": [ - "allowedPaths" - ], - "additionalProperties": false, - "description": "Vite HMR configuration" - } - }, - "additionalProperties": false, - "description": "Vite configuration overrides" - }, - "redirection": { - "type": "object", - "properties": { - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, - "description": "Port for the redirection HTTP server" - }, - "host": { - "description": "Host to bind the redirection server to (default: localhost)", - "type": "string" - } - }, - "required": [ - "port" - ], - "additionalProperties": false, - "description": "HTTP redirection server that redirects to the session URL" - }, - "$schema": { - "type": "string", - "description": "URL of the JSON Schema for this config file" - } - }, - "required": [ - "sessions" - ], - "additionalProperties": false, - "description": "Component Explorer configuration" -} diff --git a/test/componentFixtures/component-explorer.json b/test/componentFixtures/component-explorer.json index 2f24a100b02..cc0a4596269 100644 --- a/test/componentFixtures/component-explorer.json +++ b/test/componentFixtures/component-explorer.json @@ -1,11 +1,17 @@ { - "$schema": "./component-explorer-config.schema.json", + "$schema": "../../node_modules/@vscode/component-explorer-cli/dist/component-explorer-config.schema.json", "screenshotDir": ".screenshots", "sessions": [ { "name": "current" } ], + "worktree": { + "maxSlots": 1, + "setup": { + "command": "echo 'test'" + } + }, "redirection": { "port": 5337 }, diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 311b01f21bf..25b41f55f79 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -702,9 +702,9 @@ } }, "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/test/monaco/.npmrc b/test/monaco/.npmrc index a9c57709666..efe233421c4 100644 --- a/test/monaco/.npmrc +++ b/test/monaco/.npmrc @@ -1,2 +1,3 @@ legacy-peer-deps="true" timeout=180000 +min-release-age="1" diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 513d33eeb34..3e3df8a1775 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -8,11 +8,79 @@ "name": "test-monaco", "version": "1.0.0", "license": "MIT", + "dependencies": { + "postcss": "^8.5.6" + }, "devDependencies": { "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "warnings-to-errors-webpack-plugin": "^2.3.0" + "css-loader": "^6.9.1", + "file-loader": "^6.2.0", + "style-loader": "^3.3.2", + "warnings-to-errors-webpack-plugin": "^2.3.0", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@types/chai": { @@ -21,6 +89,42 @@ "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", "dev": true }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/junit-report-builder": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", @@ -28,6 +132,333 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", + "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -80,6 +511,91 @@ "playwright": ">1.0.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -106,6 +622,122 @@ "node": "*" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -118,6 +750,220 @@ "node": ">=0.12" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -128,6 +974,174 @@ "node": "*" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/junit-report-builder": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz", @@ -143,6 +1157,58 @@ "node": ">=16" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -166,6 +1232,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -176,6 +1272,104 @@ "mustache": "bin/mustache" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -189,9 +1383,229 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -202,6 +1616,242 @@ "semver": "bin/semver.js" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -211,6 +1861,61 @@ "node": ">=4" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/warnings-to-errors-webpack-plugin": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/warnings-to-errors-webpack-plugin/-/warnings-to-errors-webpack-plugin-2.3.0.tgz", @@ -220,6 +1925,230 @@ "webpack": "^2.2.0-rc || ^3 || ^4 || ^5" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/test/monaco/package.json b/test/monaco/package.json index c7373919431..89902f2304f 100644 --- a/test/monaco/package.json +++ b/test/monaco/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "compile": "node ../../node_modules/typescript/bin/tsc", - "bundle-webpack": "node ../../node_modules/webpack/bin/webpack --config ./webpack.config.js --bail", + "bundle-webpack": "webpack --config ./webpack.config.js --bail", "esm-check": "node esm-check/esm-check.js", "test": "node runner.js" }, @@ -14,6 +14,14 @@ "@types/chai": "^4.2.14", "axe-playwright": "^2.1.0", "chai": "^4.2.0", - "warnings-to-errors-webpack-plugin": "^2.3.0" + "css-loader": "^6.9.1", + "file-loader": "^6.2.0", + "style-loader": "^3.3.2", + "warnings-to-errors-webpack-plugin": "^2.3.0", + "webpack": "^5.105.0", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "postcss": "^8.5.6" } } diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts index 62eee018d06..02d94340375 100644 --- a/test/sanity/src/context.ts +++ b/test/sanity/src/context.ts @@ -34,6 +34,7 @@ export class TestContext { private static readonly authenticodeInclude = /^.+\.(exe|dll|sys|cab|cat|msi|jar|ocx|ps1|psm1|psd1|ps1xml|pssc1)$/i; private static readonly versionInfoInclude = /^.+\.(exe|dll|node|msi)$/i; private static readonly versionInfoExclude = /^(dxil\.dll|ffmpeg\.dll|msalruntime\.dll)$/i; + private static readonly dpkgLockError = /dpkg frontend lock was locked by another process|unable to acquire the dpkg frontend lock|could not get lock \/var\/lib\/dpkg\/lock-frontend/i; private readonly tempDirs = new Set(); private readonly wslTempDirs = new Set(); @@ -141,10 +142,27 @@ export class TestContext { public error(message: string): never { const line = `[${new Date().toISOString()}] ERROR: ${message}`; this.consoleOutputs.push(line); - console.error(line); + console.error(`##vso[task.logissue type=error]${line}`); throw new Error(message); } + /** + * Logs a warning message with a timestamp. + */ + public warn(message: string) { + const line = `[${new Date().toISOString()}] WARNING: ${message}`; + this.consoleOutputs.push(line); + console.warn(`##vso[task.logissue type=warning]${line}`); + } + + /** + * Returns a promise that resolves after the specified delay in milliseconds. + * @param delay The delay in milliseconds to wait before resolving the promise. + */ + private timeout(delay: number) { + return new Promise(resolve => setTimeout(resolve, delay)); + } + /** * Creates a new temporary directory and returns its path. */ @@ -220,7 +238,7 @@ export class TestContext { fs.rmSync(dir, { recursive: true, force: true }); this.log(`Deleted temp directory: ${dir}`); } catch (error) { - this.log(`Failed to delete temp directory: ${dir}: ${error}`); + this.warn(`Failed to delete temp directory: ${dir}: ${error}`); } } this.tempDirs.clear(); @@ -229,7 +247,7 @@ export class TestContext { try { this.deleteWslDir(dir); } catch (error) { - this.log(`Failed to delete WSL temp directory: ${dir}: ${error}`); + this.warn(`Failed to delete WSL temp directory: ${dir}: ${error}`); } } this.wslTempDirs.clear(); @@ -247,8 +265,8 @@ export class TestContext { for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { const delay = Math.pow(2, attempt - 1) * 1000; - this.log(`Retrying fetch (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`); - await new Promise(resolve => setTimeout(resolve, delay)); + this.warn(`Retrying fetch (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`); + await this.timeout(delay); } try { @@ -266,7 +284,7 @@ export class TestContext { return response as Response & { body: NodeJS.ReadableStream }; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); - this.log(`Fetch attempt ${attempt + 1} failed: ${lastError.message}`); + this.warn(`Fetch attempt ${attempt + 1} failed: ${lastError.message}`); } } @@ -643,6 +661,58 @@ export class TestContext { return result; } + /** + * Runs a command with sudo if not running as root, and ensures it succeeds. + * @param command The command to run. + * @param args Optional arguments for the command. + * @returns The result of the spawnSync call. + */ + private runSudoNoErrors(command: string, ...args: string[]): SpawnSyncReturns { + if (this.isRootUser) { + return this.runNoErrors(command, ...args); + } else { + return this.runNoErrors('sudo', command, ...args); + } + } + + /** + * Runs a dpkg command with retries if the frontend lock is busy, and ensures it succeeds. + */ + private async runDpkgNoErrors(...args: string[]) { + const command = this.isRootUser ? 'dpkg' : 'sudo'; + const commandArgs = this.isRootUser ? args : ['dpkg', ...args]; + const maxRetries = 5; + let lastError: string | undefined; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + if (attempt > 0) { + const delay = Math.pow(2, attempt - 1) * 1000; + this.log(`Retrying dpkg command (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`); + await this.timeout(delay); + } + + const result = this.run(command, ...commandArgs); + if (result.error !== undefined) { + lastError = `Failed to run command: ${result.error.message}`; + break; + } + + if (result.status === 0) { + return; + } + + lastError = `Command exited with code ${result.status}: ${result.stderr}`; + const output = `${result.stdout}${result.stderr}`; + if (!TestContext.dpkgLockError.test(output)) { + break; + } + + this.log(`dpkg lock is busy, waiting for the other package manager process to finish`); + } + + this.error(lastError ?? `Command failed after ${maxRetries} attempts because the dpkg frontend lock remained busy`); + } + /** * Kills a process and all its child processes. * @param pid The process ID to kill. @@ -727,7 +797,7 @@ export class TestContext { this.runNoErrors(uninstallerPath, '/silent'); this.log(`Uninstalled VS Code from ${appDir} successfully`); - await new Promise(resolve => setTimeout(resolve, 2000)); + await this.timeout(2000); if (fs.existsSync(appDir)) { this.error(`Installation directory still exists after uninstall: ${appDir}`); } @@ -738,13 +808,9 @@ export class TestContext { * @param packagePath The path to the DEB file. * @returns The path to the installed VS Code executable. */ - public installDeb(packagePath: string): string { + public async installDeb(packagePath: string): Promise { this.log(`Installing ${packagePath} using DEB package manager`); - if (this.isRootUser) { - this.runNoErrors('dpkg', '-i', packagePath); - } else { - this.runNoErrors('sudo', 'dpkg', '-i', packagePath); - } + await this.runDpkgNoErrors('-i', packagePath); this.log(`Installed ${packagePath} successfully`); const name = this.getLinuxBinaryName(); @@ -761,14 +827,10 @@ export class TestContext { const packagePath = path.join('/usr/share', name, name); this.log(`Uninstalling DEB package ${packagePath}`); - if (this.isRootUser) { - this.runNoErrors('dpkg', '-r', name); - } else { - this.runNoErrors('sudo', 'dpkg', '-r', name); - } + await this.runDpkgNoErrors('-r', name); this.log(`Uninstalled DEB package ${packagePath} successfully`); - await new Promise(resolve => setTimeout(resolve, 1000)); + await this.timeout(1000); if (fs.existsSync(packagePath)) { this.error(`Package still exists after uninstall: ${packagePath}`); } @@ -781,11 +843,7 @@ export class TestContext { */ public installRpm(packagePath: string): string { this.log(`Installing ${packagePath} using RPM package manager`); - if (this.isRootUser) { - this.runNoErrors('rpm', '-i', packagePath); - } else { - this.runNoErrors('sudo', 'rpm', '-i', packagePath); - } + this.runSudoNoErrors('rpm', '-i', packagePath); this.log(`Installed ${packagePath} successfully`); const name = this.getLinuxBinaryName(); @@ -802,14 +860,10 @@ export class TestContext { const packagePath = path.join('/usr/bin', name); this.log(`Uninstalling RPM package ${packagePath}`); - if (this.isRootUser) { - this.runNoErrors('rpm', '-e', name); - } else { - this.runNoErrors('sudo', 'rpm', '-e', name); - } + this.runSudoNoErrors('rpm', '-e', name); this.log(`Uninstalled RPM package ${packagePath} successfully`); - await new Promise(resolve => setTimeout(resolve, 1000)); + await this.timeout(1000); if (fs.existsSync(packagePath)) { this.error(`Package still exists after uninstall: ${packagePath}`); } @@ -822,11 +876,7 @@ export class TestContext { */ public installSnap(packagePath: string): string { this.log(`Installing ${packagePath} using Snap package manager`); - if (this.isRootUser) { - this.runNoErrors('snap', 'install', packagePath, '--classic', '--dangerous'); - } else { - this.runNoErrors('sudo', 'snap', 'install', packagePath, '--classic', '--dangerous'); - } + this.runSudoNoErrors('snap', 'install', packagePath, '--classic', '--dangerous'); this.log(`Installed ${packagePath} successfully`); // Snap wrapper scripts are in /snap/bin, but actual Electron binary is in /snap//current/usr/share/ @@ -844,14 +894,10 @@ export class TestContext { const packagePath = path.join('/snap/bin', name); this.log(`Uninstalling Snap package ${packagePath}`); - if (this.isRootUser) { - this.runNoErrors('snap', 'remove', name); - } else { - this.runNoErrors('sudo', 'snap', 'remove', name); - } + this.runSudoNoErrors('snap', 'remove', name); this.log(`Uninstalled Snap package ${packagePath} successfully`); - await new Promise(resolve => setTimeout(resolve, 1000)); + await this.timeout(1000); if (fs.existsSync(packagePath)) { this.error(`Package still exists after uninstall: ${packagePath}`); } @@ -1091,7 +1137,7 @@ export class TestContext { await page.screenshot({ path: screenshotPath, fullPage: true }); this.log(`Screenshot saved to: ${screenshotPath}`); } catch (e) { - this.log(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`); + this.warn(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`); } } diff --git a/test/sanity/src/desktop.test.ts b/test/sanity/src/desktop.test.ts index c2d65c157db..297a939f19c 100644 --- a/test/sanity/src/desktop.test.ts +++ b/test/sanity/src/desktop.test.ts @@ -95,7 +95,7 @@ export function setup(context: TestContext) { context.test('desktop-linux-deb-arm64', ['linux', 'arm64', 'deb', 'desktop'], async () => { const packagePath = await context.downloadTarget('linux-deb-arm64'); if (!context.options.downloadOnly) { - const entryPoint = context.installDeb(packagePath); + const entryPoint = await context.installDeb(packagePath); await testDesktopApp(entryPoint); await context.uninstallDeb(); } @@ -104,7 +104,7 @@ export function setup(context: TestContext) { context.test('desktop-linux-deb-armhf', ['linux', 'arm32', 'deb', 'desktop'], async () => { const packagePath = await context.downloadTarget('linux-deb-armhf'); if (!context.options.downloadOnly) { - const entryPoint = context.installDeb(packagePath); + const entryPoint = await context.installDeb(packagePath); await testDesktopApp(entryPoint); await context.uninstallDeb(); } @@ -113,7 +113,7 @@ export function setup(context: TestContext) { context.test('desktop-linux-deb-x64', ['linux', 'x64', 'deb', 'desktop'], async () => { const packagePath = await context.downloadTarget('linux-deb-x64'); if (!context.options.downloadOnly) { - const entryPoint = context.installDeb(packagePath); + const entryPoint = await context.installDeb(packagePath); await testDesktopApp(entryPoint); await context.uninstallDeb(); } diff --git a/test/sanity/src/uiTest.ts b/test/sanity/src/uiTest.ts index 65f8b14a49e..96733481e96 100644 --- a/test/sanity/src/uiTest.ts +++ b/test/sanity/src/uiTest.ts @@ -138,7 +138,7 @@ export class UITest { const extensionItem = page.locator('.extension-list-item').getByText(/^GitHub Pull Requests$/); const messageContainer = page.locator('.extensions-viewlet .message-container:not(.hidden)').first(); - for (let attempt = 0; attempt < 3; attempt++) { + for (let attempt = 0; attempt < 5; attempt++) { const result = await Promise.race([ extensionItem.waitFor().then(() => 'found' as const), messageContainer.waitFor().then(() => 'message' as const), @@ -149,20 +149,33 @@ export class UITest { } const message = await messageContainer.locator('.message').innerText(); - this.context.log(`Marketplace message: ${message} (attempt ${attempt + 1}/3), clicking Refresh`); + this.context.log(`Marketplace message: ${message} (attempt ${attempt + 1}/5), clicking Refresh`); await page.getByRole('button', { name: 'Refresh' }).click(); - await messageContainer.waitFor({ state: 'hidden', timeout: 30_000 }); + await page.waitForTimeout(5_000); } await extensionItem.waitFor(); - this.context.log('Clicking Install on the first extension in the list'); - const installButton = page.locator('.extension-action:not(.disabled)', { hasText: /Install/ }).first(); - await installButton.waitFor(); - await installButton.click(); + for (let attempt = 0; attempt < 3; attempt++) { + try { + this.context.log(`Clicking Install on the first extension in the list (attempt ${attempt + 1}/3)`); + const installButton = page.locator('.extension-action:not(.disabled)', { hasText: /Install/ }).first(); + await installButton.click(); - this.context.log('Waiting for extension to be installed'); - await page.getByRole('button', { name: 'Uninstall' }).first().waitFor({ timeout: 5 * 60_000 }); + this.context.log('Waiting for extension to be installed'); + const uninstallButton = page.getByRole('button', { name: 'Uninstall' }).first(); + const installed = await uninstallButton.waitFor({ timeout: 5 * 60_000 }).then(() => true, () => false); + if (installed) { + return; + } + } catch (error) { + this.context.log(`Extension install attempt ${attempt + 1}/3 failed: ${error instanceof Error ? error.message : String(error)}`); + } + + this.context.log('Extension install may have failed, retrying'); + } + + throw new Error('Failed to install extension after 3 attempts'); } /** diff --git a/test/smoke/extensions/vscode-smoketest-ext-host/extension.js b/test/smoke/extensions/vscode-smoketest-ext-host/extension.js index e69899a6b29..c1219497701 100644 --- a/test/smoke/extensions/vscode-smoketest-ext-host/extension.js +++ b/test/smoke/extensions/vscode-smoketest-ext-host/extension.js @@ -16,6 +16,16 @@ let deactivateMarkerFile; * @param {vscode.ExtensionContext} context */ function activate(context) { + // Record extension host pid on every activation so smoke tests can validate + // that a new extension host process was started after a restart action. + try { + const pid = String(process.pid); + const activationPidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid-on-activate.txt'); + fs.writeFileSync(activationPidFile, pid, 'utf-8'); + } catch { + // Ignore errors in smoke helper setup. + } + // This is used to verify that the extension host process is properly killed // when window reloads even if the extension host is blocked // Refs: https://github.com/microsoft/vscode/issues/291346 @@ -23,12 +33,9 @@ function activate(context) { vscode.commands.registerCommand('smoketest.getExtensionHostPidAndBlock', (delayMs = 100, durationMs = 60000) => { const pid = process.pid; - // Write PID file to workspace folder if available, otherwise temp dir + // Write PID file to temp dir to avoid polluting workspace search results // Note: filename must match name in extension-host-restart.test.ts - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const pidFile = workspaceFolder - ? path.join(workspaceFolder, 'vscode-ext-host-pid.txt') - : path.join(os.tmpdir(), 'vscode-ext-host-pid.txt'); + const pidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid.txt'); setTimeout(() => { fs.writeFileSync(pidFile, String(pid), 'utf-8'); @@ -57,13 +64,8 @@ function activate(context) { context.subscriptions.push( vscode.commands.registerCommand('smoketest.setupGracefulDeactivation', () => { const pid = process.pid; - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const pidFile = workspaceFolder - ? path.join(workspaceFolder, 'vscode-ext-host-pid-graceful.txt') - : path.join(os.tmpdir(), 'vscode-ext-host-pid-graceful.txt'); - deactivateMarkerFile = workspaceFolder - ? path.join(workspaceFolder, 'vscode-ext-host-deactivated.txt') - : path.join(os.tmpdir(), 'vscode-ext-host-deactivated.txt'); + const pidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid-graceful.txt'); + deactivateMarkerFile = path.join(os.tmpdir(), 'vscode-ext-host-deactivated.txt'); // Write PID file immediately so test knows the extension is ready fs.writeFileSync(pidFile, String(pid), 'utf-8'); diff --git a/test/smoke/src/areas/extensions/extension-host-restart.test.ts b/test/smoke/src/areas/extensions/extension-host-restart.test.ts index 6528e61abe3..b26ccad39da 100644 --- a/test/smoke/src/areas/extensions/extension-host-restart.test.ts +++ b/test/smoke/src/areas/extensions/extension-host-restart.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import { Application, Logger } from '../../../../automation'; import { installAllHandlers, timeout } from '../../utils'; @@ -30,7 +31,7 @@ export function setup(logger: Logger) { this.timeout(60_000); const app = this.app as Application; - const pidFile = path.join(app.workspacePathOrFolder, 'vscode-ext-host-pid.txt'); + const pidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid.txt'); if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); @@ -78,8 +79,8 @@ export function setup(logger: Logger) { this.timeout(60_000); const app = this.app as Application; - const pidFile = path.join(app.workspacePathOrFolder, 'vscode-ext-host-pid-graceful.txt'); - const markerFile = path.join(app.workspacePathOrFolder, 'vscode-ext-host-deactivated.txt'); + const pidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid-graceful.txt'); + const markerFile = path.join(os.tmpdir(), 'vscode-ext-host-deactivated.txt'); // Clean up any existing files if (fs.existsSync(pidFile)) { @@ -139,5 +140,79 @@ export function setup(logger: Logger) { logger.log('Extension host was properly terminated after graceful deactivation'); }); + + it('kills blocked extension host on restart extension host (issue #296681)', async function () { + this.timeout(90_000); + + const app = this.app as Application; + const pidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid.txt'); + const activationPidFile = path.join(os.tmpdir(), 'vscode-ext-host-pid-on-activate.txt'); + + if (fs.existsSync(pidFile)) { + fs.unlinkSync(pidFile); + } + + await app.workbench.quickaccess.runCommand('smoketest.getExtensionHostPidAndBlock'); + + let retries = 0; + while (!fs.existsSync(pidFile) && retries < 20) { + await timeout(500); + retries++; + } + + if (!fs.existsSync(pidFile)) { + throw new Error('PID file was not created - extension may not have activated'); + } + + const oldPid = parseInt(fs.readFileSync(pidFile, 'utf-8'), 10); + logger.log(`Old extension host PID: ${oldPid}`); + + if (fs.existsSync(activationPidFile)) { + fs.unlinkSync(activationPidFile); + } + + await app.workbench.quickaccess.runCommand('Developer: Restart Extension Host', { keepOpen: true }); + + const maxWaitMs = 10_000; + const pollIntervalMs = 500; + let waitedMs = 0; + + let newPid: number | undefined; + while (waitedMs < maxWaitMs) { + if (fs.existsSync(activationPidFile)) { + const pidText = fs.readFileSync(activationPidFile, 'utf-8').trim(); + const parsedPid = parseInt(pidText, 10); + if (!Number.isNaN(parsedPid) && parsedPid !== oldPid) { + newPid = parsedPid; + break; + } + } + + await timeout(pollIntervalMs); + waitedMs += pollIntervalMs; + } + + if (!newPid) { + throw new Error(`New extension host PID was not observed after restart (waited ${maxWaitMs}ms)`); + } + + if (newPid === oldPid) { + throw new Error(`Extension host PID did not change after restart (pid: ${oldPid})`); + } + + logger.log(`New extension host PID observed: ${newPid}`); + + waitedMs = 0; + while (processExists(oldPid) && waitedMs < maxWaitMs) { + await timeout(pollIntervalMs); + waitedMs += pollIntervalMs; + } + + if (processExists(oldPid)) { + throw new Error(`Old extension host ${oldPid} still running after restart (waited ${maxWaitMs}ms)`); + } + + logger.log(`Extension host restarted successfully (old: ${oldPid}, new: ${newPid})`); + }); }); }